// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2012-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import #import #import "MyIdentityStore.h" #import "ProtocolDefines.h" #import "NaClCrypto.h" #import "NSData+Base32.h" #import "CryptoUtils.h" #import "NaClCrypto.h" #import "ThreemaError.h" #import "AppGroup.h" #import "UserSettings.h" #import "ValidationLogger.h" #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelVerbose; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif static const NSString *keychainLabel = @"Threema identity 1"; @implementation MyIdentityStore { NSData *secretKey; BOOL secretKeyInKeychain; BOOL keychainLocked; } @synthesize identity; @synthesize serverGroup; @synthesize publicKey; static MyIdentityStore *instance; + (MyIdentityStore*)sharedMyIdentityStore { @synchronized (self) { if (!instance) instance = [[MyIdentityStore alloc] init]; } return instance; } + (void)resetSharedInstance { instance = nil; } - (id)init { self = [super init]; if (self) { OSStatus status = [self loadFromKeychain]; if (identity == nil) { if (status == errSecInteractionNotAllowed) { keychainLocked = YES; } else { /* This can happen when a backup is restored to a new phone, and we have our NSUserDefaults but no keychain item. Make sure the identity-specific defaults are wiped to avoid confusion later on */ DDLogVerbose(@"No identity - clearing identity-specific user defaults"); [self removeIdentityUserDefaults]; } } [self migrateProfilePicture]; } return self; } - (BOOL)isProvisioned { /* check prerequisites */ if ([self isInvalidIdentity]) { return false; } if (self.pendingCreateID) { return false; } return true; } - (BOOL)isKeychainLocked { return keychainLocked; } - (BOOL)isInvalidIdentity { return identity == nil || publicKey == nil || serverGroup == nil || (secretKey == nil && !secretKeyInKeychain); } - (void)generateKeyPairWithSeed:(NSData*)seed { NSData *newPublicKey, *newSecretKey; DDLogInfo(@"Generating key pair"); [[NaClCrypto sharedCrypto] generateKeyPairPublicKey:&newPublicKey secretKey:&newSecretKey withSeed:seed]; identity = nil; serverGroup = nil; publicKey = newPublicKey; secretKey = newSecretKey; secretKeyInKeychain = NO; } - (OSStatus)loadFromKeychain { NSMutableDictionary *keychainDict = [NSMutableDictionary dictionary]; [keychainDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; [keychainDict setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes]; [keychainDict setObject:keychainLabel forKey:(__bridge id)kSecAttrLabel]; CFDictionaryRef resultRef; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(keychainDict), (CFTypeRef *)&resultRef); if (status == noErr) { NSDictionary *result = (__bridge_transfer NSDictionary *)resultRef; /* sanity check on identity and public key; backup/restore to a new iPhone can produce strange results */ NSString *tmpIdentity = [result objectForKey:(__bridge id)(kSecAttrAccount)]; NSData *tmpPublicKey = [result objectForKey:(__bridge id)(kSecAttrGeneric)]; if (tmpIdentity.length != kIdentityLen || tmpPublicKey.length != kNaClCryptoPubKeySize) { DDLogError(@"Got bad identity or key from keychain; ignoring"); return status; } identity = tmpIdentity; publicKey = tmpPublicKey; serverGroup = [result objectForKey:(__bridge id)(kSecAttrService)]; secretKey = nil; secretKeyInKeychain = YES; DDLogInfo(@"Loaded identity %@ from keychain", identity); } else { if (status != errSecItemNotFound) { [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Keychain: Error accessing keychain, status: %i", (int)status]]; DDLogError(@"Error accessing keychain, status: %i", (int)status); } } return status; } - (void)storeInKeychain { if (identity == nil || publicKey == nil || secretKey == nil || serverGroup == nil) { DDLogError(@"Not enough data to store in keychain"); return; } NSMutableDictionary *queryDict = [NSMutableDictionary dictionary]; [queryDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; [queryDict setObject:keychainLabel forKey:(__bridge id)kSecAttrLabel]; /* check if we already have a keychain item and need to update */ CFDictionaryRef resultRef; if (SecItemCopyMatching((__bridge CFDictionaryRef)(queryDict), (CFTypeRef *)&resultRef) == noErr) { if (SecItemDelete((__bridge CFDictionaryRef)(queryDict)) != noErr) { DDLogError(@"Couldn't delete keychain item"); } } /* add new item */ NSMutableDictionary *addDict = [NSMutableDictionary dictionary]; [addDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; [addDict setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible]; [addDict setObject:publicKey forKey:(__bridge id)kSecAttrGeneric]; [addDict setObject:identity forKey:(__bridge id)kSecAttrAccount]; [addDict setObject:serverGroup forKey:(__bridge id)kSecAttrService]; [addDict setObject:keychainLabel forKey:(__bridge id)kSecAttrLabel]; [addDict setObject:secretKey forKey:(__bridge id)kSecValueData]; if (SecItemAdd((__bridge CFDictionaryRef)(addDict), NULL) != noErr) { DDLogError(@"Couldn't add keychain item"); } else { secretKey = nil; secretKeyInKeychain = YES; } self.privateIdentityInfoLastUpdate = [NSDate date]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationCreatedIdentity object:nil]; } - (void)updateConnectionRights { OSStatus status = [self loadFromKeychain]; if (status != errSecInteractionNotAllowed) { secretKey = [self _obtainSecretKey]; if (secretKey != nil) { [self deleteFromKeychain]; [self storeInKeychain]; } } } - (void)deleteFromKeychain { NSMutableDictionary *queryDict = [NSMutableDictionary dictionary]; [queryDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; [queryDict setObject:keychainLabel forKey:(__bridge id)kSecAttrLabel]; if (SecItemDelete((__bridge CFDictionaryRef)(queryDict)) != noErr) { DDLogError(@"Couldn't delete keychain item"); } secretKeyInKeychain = NO; } - (void)destroy { [self deleteFromKeychain]; [self removeIdentityUserDefaults]; [[UserSettings sharedUserSettings] setPushDecrypt:NO]; [[UserSettings sharedUserSettings] setAskedForPushDecryption:NO]; [[UserSettings sharedUserSettings] setSafeConfig:nil]; self.tempSafePassword = nil; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationDestroyedIdentity object:nil]; } - (void)removeIdentityUserDefaults { [[AppGroup userDefaults] removeObjectForKey:@"PushFromName"]; NSFileManager *fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:[self profilePicturePath]]) { NSError *error; [fileManager removeItemAtPath:[self profilePicturePath] error:&error]; if (error) { } } [[AppGroup userDefaults] removeObjectForKey:@"ProfilePicture"]; [[AppGroup userDefaults] removeObjectForKey:@"LinkedEmail"]; [[AppGroup userDefaults] removeObjectForKey:@"LinkEmailPending"]; [[AppGroup userDefaults] removeObjectForKey:@"LinkedMobileNo"]; [[AppGroup userDefaults] removeObjectForKey:@"LinkMobileNoPending"]; [[AppGroup userDefaults] removeObjectForKey:@"LinkMobileNoVerificationId"]; [[AppGroup userDefaults] removeObjectForKey:@"LinkMobileNoStartDate"]; [[AppGroup userDefaults] removeObjectForKey:@"PrivateIdentityInfoLastUpdate"]; [[AppGroup userDefaults] removeObjectForKey:@"LastSentFeatureMask"]; [[AppGroup userDefaults] removeObjectForKey:@"RevocationPasswordSetDate"]; [[AppGroup userDefaults] removeObjectForKey:@"RevocationPasswordLastCheck"]; [[AppGroup userDefaults] removeObjectForKey:@"PendingCreateID"]; [[AppGroup userDefaults] removeObjectForKey:@"CreateIDEmail"]; [[AppGroup userDefaults] removeObjectForKey:@"CreateIDPhone"]; [[AppGroup userDefaults] removeObjectForKey:@"FirstName"]; [[AppGroup userDefaults] removeObjectForKey:@"LastName"]; [[AppGroup userDefaults] removeObjectForKey:@"CSI"]; [[AppGroup userDefaults] removeObjectForKey:@"Category"]; [[AppGroup userDefaults] removeObjectForKey:@"CompanyName"]; [[AppGroup userDefaults] removeObjectForKey:@"DirectoryCategories"]; [[AppGroup userDefaults] removeObjectForKey:@"LastWorkUpdateRequest"]; [[AppGroup userDefaults] removeObjectForKey:@"LastWorkUpdateDate"]; [[AppGroup userDefaults] removeObjectForKey:@"MessageDrafts"]; [[AppGroup userDefaults] removeObjectForKey:@"PushNotificationEncryptionKey"]; [[AppGroup userDefaults] removeObjectForKey:@"MatchToken"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)pushFromName { return [[AppGroup userDefaults] objectForKey:@"PushFromName"]; } - (void)setPushFromName:(NSString *)pushFromName { [[AppGroup userDefaults] setObject:pushFromName forKey:@"PushFromName"]; [[AppGroup userDefaults] synchronize]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationProfileNicknameChanged object:nil]; } - (NSMutableDictionary *)profilePicture { return [NSMutableDictionary dictionaryWithContentsOfFile:[self profilePicturePath]]; } - (void)setProfilePicture:(NSMutableDictionary *)profilePicture { if (!profilePicture) { [[AppGroup userDefaults] removeObjectForKey:@"ProfilePicture"]; NSFileManager *fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:[self profilePicturePath]]) { NSError *error; [fileManager removeItemAtPath:[self profilePicturePath] error:&error]; if (error) { } } } else { [profilePicture writeToFile:[self profilePicturePath] atomically:YES]; } [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationProfilePictureChanged object:nil]; } - (NSString *)linkedEmail { return [[AppGroup userDefaults] objectForKey:@"LinkedEmail"]; } - (void)setLinkedEmail:(NSString *)linkedEmail { [[AppGroup userDefaults] setObject:linkedEmail forKey:@"LinkedEmail"]; [[AppGroup userDefaults] synchronize]; } - (BOOL)linkEmailPending { return [[AppGroup userDefaults] boolForKey:@"LinkEmailPending"]; } - (void)setLinkEmailPending:(BOOL)linkEmailPending { [[AppGroup userDefaults] setBool:linkEmailPending forKey:@"LinkEmailPending"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)linkedMobileNo { return [[AppGroup userDefaults] objectForKey:@"LinkedMobileNo"]; } - (void)setLinkedMobileNo:(NSString *)linkedMobileNo { [[AppGroup userDefaults] setObject:linkedMobileNo forKey:@"LinkedMobileNo"]; [[AppGroup userDefaults] synchronize]; } - (BOOL)linkMobileNoPending { return [[AppGroup userDefaults] boolForKey:@"LinkMobileNoPending"]; } - (void)setLinkMobileNoPending:(BOOL)linkMobileNoPending { [[AppGroup userDefaults] setBool:linkMobileNoPending forKey:@"LinkMobileNoPending"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)linkMobileNoVerificationId { return [[AppGroup userDefaults] stringForKey:@"LinkMobileNoVerificationId"]; } - (void)setLinkMobileNoVerificationId:(NSString *)linkMobileNoVerificationId { [[AppGroup userDefaults] setObject:linkMobileNoVerificationId forKey:@"LinkMobileNoVerificationId"]; [[AppGroup userDefaults] synchronize]; } - (NSDate *)linkMobileNoStartDate { return [[AppGroup userDefaults] objectForKey:@"LinkMobileNoStartDate"]; } - (void)setLinkMobileNoStartDate:(NSDate *)linkMobileNoStartDate { [[AppGroup userDefaults] setObject:linkMobileNoStartDate forKey:@"LinkMobileNoStartDate"]; [[AppGroup userDefaults] synchronize]; } - (NSDate *)privateIdentityInfoLastUpdate { return [[AppGroup userDefaults] objectForKey:@"PrivateIdentityInfoLastUpdate"]; } - (void)setPrivateIdentityInfoLastUpdate:(NSDate *)privateIdentityInfoLastUpdate { [[AppGroup userDefaults] setObject:privateIdentityInfoLastUpdate forKey:@"PrivateIdentityInfoLastUpdate"]; [[AppGroup userDefaults] synchronize]; } - (NSInteger)lastSentFeatureMask { return [[AppGroup userDefaults] integerForKey:@"LastSentFeatureMask"]; } - (void)setLastSentFeatureMask:(NSInteger)lastSentFeatureMask { [[AppGroup userDefaults] setInteger:lastSentFeatureMask forKey:@"LastSentFeatureMask"]; [[AppGroup userDefaults] synchronize]; } - (NSDate *)revocationPasswordSetDate { return [[AppGroup userDefaults] objectForKey:@"RevocationPasswordSetDate"]; } - (void)setRevocationPasswordSetDate:(NSDate *)revocationPasswordSetDate { [[AppGroup userDefaults] setObject:revocationPasswordSetDate forKey:@"RevocationPasswordSetDate"]; [[AppGroup userDefaults] synchronize]; } - (NSDate *)revocationPasswordLastCheck { return [[AppGroup userDefaults] objectForKey:@"RevocationPasswordLastCheck"]; } - (void)setLicenseLastCheck:(NSDate *)licenseLastCheck { [[AppGroup userDefaults] setObject:licenseLastCheck forKey:@"LicenseLastCheck"]; [[AppGroup userDefaults] synchronize]; } - (NSDate *)licenseLastCheck { return [[AppGroup userDefaults] objectForKey:@"LicenseLastCheck"]; } - (void)setLicenseLogoLightUrl:(NSString *)licenseLogoLightUrl { [[AppGroup userDefaults] setObject:licenseLogoLightUrl forKey:@"LicenseLogoLightUrl"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)licenseLogoLightUrl { return [[AppGroup userDefaults] objectForKey:@"LicenseLogoLightUrl"]; } - (void)setLicenseLogoDarkUrl:(NSString *)licenseLogoDarkUrl { [[AppGroup userDefaults] setObject:licenseLogoDarkUrl forKey:@"LicenseLogoDarkUrl"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)licenseLogoDarkUrl { return [[AppGroup userDefaults] objectForKey:@"LicenseLogoDarkUrl"]; } - (void)setLicenseSupportUrl:(NSString *)licenseSupportUrl { [[AppGroup userDefaults] setObject:licenseSupportUrl forKey:@"LicenseSupportUrl"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)licenseSupportUrl { return [[AppGroup userDefaults] objectForKey:@"LicenseSupportUrl"]; } - (void)setRevocationPasswordLastCheck:(NSDate *)revocationPasswordLastCheck { [[AppGroup userDefaults] setObject:revocationPasswordLastCheck forKey:@"RevocationPasswordLastCheck"]; [[AppGroup userDefaults] synchronize]; } - (BOOL)pendingCreateID { return [[AppGroup userDefaults] boolForKey:@"PendingCreateID"]; } - (void)setPendingCreateID:(BOOL)pendingCreateID { [[AppGroup userDefaults] setBool:pendingCreateID forKey:@"PendingCreateID"]; [[AppGroup userDefaults] synchronize]; } - (NSString*)createIDEmail { return [[AppGroup userDefaults] stringForKey:@"CreateIDEmail"]; } - (void)setCreateIDEmail:(NSString *)createIDEmail { [[AppGroup userDefaults] setObject:createIDEmail forKey:@"CreateIDEmail"]; [[AppGroup userDefaults] synchronize]; } - (NSString*)createIDPhone { return [[AppGroup userDefaults] stringForKey:@"CreateIDPhone"]; } - (void)setCreateIDPhone:(NSString *)createIDPhone { [[AppGroup userDefaults] setObject:createIDPhone forKey:@"CreateIDPhone"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)firstName { return [[AppGroup userDefaults] stringForKey:@"FirstName"]; } - (void)setFirstName:(NSString *)firstName { [[AppGroup userDefaults] setObject:firstName forKey:@"FirstName"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)lastName { return [[AppGroup userDefaults] stringForKey:@"LastName"]; } - (void)setLastName:(NSString *)lastName { [[AppGroup userDefaults] setObject:lastName forKey:@"LastName"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)csi { return [[AppGroup userDefaults] stringForKey:@"CSI"]; } - (void)setCsi:(NSString *)csi { [[AppGroup userDefaults] setObject:csi forKey:@"CSI"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)category { return [[AppGroup userDefaults] stringForKey:@"Category"]; } - (void)setCategory:(NSString *)category { [[AppGroup userDefaults] setObject:category forKey:@"Category"]; [[AppGroup userDefaults] synchronize]; } - (NSString *)companyName { return [[AppGroup userDefaults] stringForKey:@"CompanyName"]; } - (void)setCompanyName:(NSString *)companyName { [[AppGroup userDefaults] setObject:companyName forKey:@"CompanyName"]; [[AppGroup userDefaults] synchronize]; } - (NSMutableDictionary *)directoryCategories { return [[AppGroup userDefaults] objectForKey:@"DirectoryCategories"]; } - (void)setDirectoryCategories:(NSMutableDictionary *)directoryCategories { [[AppGroup userDefaults] setObject:directoryCategories forKey:@"DirectoryCategories"]; [[AppGroup userDefaults] synchronize]; } - (NSArray *)directoryCategoryIdsSortedByName { NSDictionary *allCategories = [self directoryCategories]; NSArray *keys = [allCategories allKeys]; NSArray *sortedKeys = [keys sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { NSString *first = [allCategories objectForKey:a]; NSString *second = [allCategories objectForKey:b]; return [first caseInsensitiveCompare:second]; }]; return sortedKeys; } - (NSDictionary *)lastWorkUpdateRequest { return [[AppGroup userDefaults] objectForKey:@"LastWorkUpdateRequest"]; } - (void)setLastWorkUpdateRequest:(NSDictionary *)lastWorkUpdateRequest { [[AppGroup userDefaults] setObject:lastWorkUpdateRequest forKey:@"LastWorkUpdateRequest"]; [[AppGroup userDefaults] synchronize]; } - (NSDate *)lastWorkUpdateDate { return [[AppGroup userDefaults] objectForKey:@"LastWorkUpdateDate"]; } - (void)setLastWorkUpdateDate:(NSDate *)lastWorkUpdateDate { [[AppGroup userDefaults] setObject:lastWorkUpdateDate forKey:@"LastWorkUpdateDate"]; [[AppGroup userDefaults] synchronize]; } - (NSString*)keyFingerprint { if (publicKey == nil) return nil; return [CryptoUtils fingerprintForPublicKey:publicKey]; } - (NSData*)keySecret { NSData *mySecretKey = [self _obtainSecretKey]; return mySecretKey; } - (NSData*)encryptData:(NSData*)data withNonce:(NSData*)nonce publicKey:(NSData*)_publicKey { NSData *mySecretKey = [self _obtainSecretKey]; if (mySecretKey == nil) { DDLogError(@"Cannot encrypt: no secret key"); return nil; } @try { return [[NaClCrypto sharedCrypto] encryptData:data withPublicKey:_publicKey signKey:mySecretKey nonce:nonce]; } @catch (NSException *exception) { DDLogError(@"Cannot encrypt: %@", [exception reason]); return nil; } } - (NSData*)decryptData:(NSData*)data withNonce:(NSData*)nonce publicKey:(NSData*)_publicKey { NSData *mySecretKey = [self _obtainSecretKey]; if (mySecretKey == nil) { DDLogError(@"Cannot decrypt: no secret key"); return nil; } @try { return [[NaClCrypto sharedCrypto] decryptData:data withSecretKey:mySecretKey signKey:_publicKey nonce:nonce]; } @catch (NSException *exception) { DDLogError(@"Cannot decrypt: %@", [exception reason]); return nil; } } - (NSData*)_obtainSecretKey { NSData *mySecretKey = secretKey; if (mySecretKey == nil && secretKeyInKeychain) { NSMutableDictionary *queryDict = [NSMutableDictionary dictionary]; [queryDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; [queryDict setObject:keychainLabel forKey:(__bridge id)kSecAttrLabel]; [queryDict setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData]; CFDataRef resultRef; if (SecItemCopyMatching((__bridge CFDictionaryRef)(queryDict), (CFTypeRef *)&resultRef) == noErr) { mySecretKey = (__bridge_transfer NSData*)resultRef; } } return mySecretKey; } - (NSString*)backupIdentityWithPassword:(NSString*)password { if ([self isInvalidIdentity]) { return nil; } /* Identity backup: derive an encryption key from the given password using PBKDF2 with 100000 iterations and a 64 bit salt. Then use it to encrypt a binary string of the following format using crypto_stream (zero nonce): hash = first two bytes of SHA256() Prepend the salt to the encrypted string and Base32-encode the result. Finally, split into groups of 4 and separate with dashes. The result will look like: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX- XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX */ NSData *salt = [[NaClCrypto sharedCrypto] randomBytes:8]; NSData *keyData = [self deriveBackupKeyFromPassword:password salt:salt]; if (keyData == nil) return nil; NSData *mySecretKey = [self _obtainSecretKey]; NSMutableData *idData = [NSMutableData dataWithCapacity:(mySecretKey.length + kIdentityLen + 2)]; [idData appendData:[identity dataUsingEncoding:NSASCIIStringEncoding]]; [idData appendData:mySecretKey]; unsigned char digest[CC_SHA256_DIGEST_LENGTH]; CC_SHA256(idData.bytes, (CC_LONG)idData.length, digest); [idData appendBytes:digest length:2]; NSData *nonceData = [[NaClCrypto sharedCrypto] zeroBytes:kNaClCryptoStreamNonceSize]; NSData *idDataEncrypted = [[NaClCrypto sharedCrypto] streamXorData:idData secretKey:keyData nonce:nonceData]; /* Concatenate salt + encrypted data. The result should be 50 bytes. */ NSMutableData *idDataWithSalt = [NSMutableData dataWithData:salt]; [idDataWithSalt appendData:idDataEncrypted]; NSString *base32 = [idDataWithSalt base32String]; NSString *grouped = [self addBackupGroupDashes:base32]; return grouped; } - (NSString*)addBackupGroupDashes:(NSString*)backup { NSString *myBackup = [backup stringByReplacingOccurrencesOfString:@"-" withString:@""]; NSMutableString *grouped = [NSMutableString string]; for (int i = 0; i < myBackup.length; i += 4) { NSUInteger len = 4; if ((i + len) > myBackup.length) len = myBackup.length - i; if (grouped.length > 0) [grouped appendString:@"-"]; [grouped appendString:[myBackup substringWithRange:NSMakeRange(i, len)]]; } return grouped; } - (void)restoreFromBackup:(NSString*)backup withPassword:(NSString*)password onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { /* decode Base32 data */ NSData *backupDecoded = [NSData dataWithBase32String:backup]; if (backupDecoded.length != 50) { DDLogError(@"Invalid decoded backup length: %lu", (unsigned long)backupDecoded.length); onError([ThreemaError threemaError:NSLocalizedString(@"bad_identity_backup", nil)]); return; } /* Extract salt and derive key */ NSData *salt = [NSData dataWithBytes:backupDecoded.bytes length:8]; NSData *keyData = [self deriveBackupKeyFromPassword:password salt:salt]; if (keyData == nil) { DDLogError(@"Invalid password"); onError([ThreemaError threemaError:NSLocalizedString(@"bad_identity_backup", nil)]); return; } /* Decrypt backup data */ NSData *backupData = [NSData dataWithBytes:(backupDecoded.bytes + 8) length:42]; NSData *nonceData = [[NaClCrypto sharedCrypto] zeroBytes:kNaClCryptoStreamNonceSize]; NSData *backupDataDecrypted = [[NaClCrypto sharedCrypto] streamXorData:backupData secretKey:keyData nonce:nonceData]; if (backupDataDecrypted == nil) { /* should never happen, even if the data is invalid */ onError([ThreemaError threemaError:@"Backup decryption failed"]); return; } /* Calculate digest and verify */ unsigned char digest[CC_SHA256_DIGEST_LENGTH]; CC_SHA256(backupDataDecrypted.bytes, 40, digest); if (memcmp(digest, backupDataDecrypted.bytes + 40, 2) != 0) { DDLogWarn(@"Digest mismatch in decrypted identity backup"); onError([ThreemaError threemaError:NSLocalizedString(@"bad_identity_backup", nil)]); return; } identity = [[NSString alloc] initWithData:[NSData dataWithBytes:backupDataDecrypted.bytes length:kIdentityLen] encoding:NSASCIIStringEncoding]; secretKey = [NSData dataWithBytes:(backupDataDecrypted.bytes + 8) length:kNaClCryptoSecKeySize]; [self restoreFromBackup:identity withSecretKey:secretKey onCompletion:onCompletion onError:onError]; } - (void)restoreFromBackup:(NSString*)myIdentity withSecretKey:(NSData*)mySecretKey onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError { identity = [[NSString alloc] initWithString:myIdentity]; secretKey = [NSData dataWithBytes:(mySecretKey.bytes) length:kNaClCryptoSecKeySize]; /* derive public key and store everything */ publicKey = [[NaClCrypto sharedCrypto] derivePublicKeyFromSecretKey:secretKey]; if (publicKey == nil) { /* should never happen, even if the data is invalid */ onError([ThreemaError threemaError:@"Public key derivation failed"]); return; } secretKeyInKeychain = NO; serverGroup = nil; DDLogInfo(@"Restored identity %@ from backup", identity); onCompletion(); } - (BOOL)isValidBackupFormat:(NSString *)backup { if (backup == nil) return NO; NSData *backupDecoded = [NSData dataWithBase32String:backup]; return (backupDecoded.length == 50); } - (NSData*)deriveBackupKeyFromPassword:(NSString*)password salt:(NSData*)salt { NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; uint8_t key[kNaClCryptoStreamKeySize]; if (CCKeyDerivationPBKDF(kCCPBKDF2, passwordData.bytes, passwordData.length, salt.bytes, salt.length, kCCPRFHmacAlgSHA256, 100000, key, kNaClCryptoStreamKeySize) != 0) { DDLogError(@"PBKDF key derivation failed"); return nil; } return [NSData dataWithBytes:key length:kNaClCryptoStreamKeySize]; } - (void)migrateProfilePicture { NSMutableDictionary *profile = [[AppGroup userDefaults] objectForKey:@"ProfilePicture"]; if (profile != nil) { [profile writeToFile:[self profilePicturePath] atomically:YES]; [[AppGroup userDefaults] removeObjectForKey:@"ProfilePicture"]; } } - (NSString *)profilePicturePath { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); return [[paths objectAtIndex:0] stringByAppendingPathComponent:@"ProfilePicture.out"]; } - (BOOL)sendUpdateWorkInfoStatus { NSDate *dateLastCheck = [self lastWorkUpdateDate]; if (dateLastCheck == nil) { return true; } return ![[NSCalendar currentCalendar] isDate:dateLastCheck inSameDayAsDate:[NSDate date]]; } @end