diff --git a/External/Crashlytics.framework/Versions/A/Crashlytics b/External/Crashlytics.framework/Versions/A/Crashlytics index 41884f03..1230b7e0 100644 Binary files a/External/Crashlytics.framework/Versions/A/Crashlytics and b/External/Crashlytics.framework/Versions/A/Crashlytics differ diff --git a/External/Crashlytics.framework/Versions/A/Resources/Info.plist b/External/Crashlytics.framework/Versions/A/Resources/Info.plist index bf21197c..60ba0760 100644 --- a/External/Crashlytics.framework/Versions/A/Resources/Info.plist +++ b/External/Crashlytics.framework/Versions/A/Resources/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable Crashlytics CFBundleIdentifier - com.crashlytics.sdk.mac + com.crashlytics.ios CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -15,16 +15,16 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.1.2 + 2.1.7 CFBundleSupportedPlatforms - macosx + iPhoneOS CFBundleVersion - 9 + 26 DTPlatformName - macosx + iphoneos MinimumOSVersion - 10.6 + 4.0 diff --git a/External/Crashlytics.framework/run b/External/Crashlytics.framework/run index a43a84fe..d2c28068 100755 Binary files a/External/Crashlytics.framework/run and b/External/Crashlytics.framework/run differ diff --git a/MasterPassword/ObjC/MPAlgorithm.h b/MasterPassword/ObjC/MPAlgorithm.h index 8bbf6b4f..b7dd2e3b 100644 --- a/MasterPassword/ObjC/MPAlgorithm.h +++ b/MasterPassword/ObjC/MPAlgorithm.h @@ -37,6 +37,8 @@ - (NSString *)shortNameOfType:(MPElementType)type; - (NSString *)classNameOfType:(MPElementType)type; - (Class)classOfType:(MPElementType)type; +- (NSArray *)allTypes; +- (NSArray *)allTypesStartingWith:(MPElementType)startingType; - (MPElementType)nextType:(MPElementType)type; - (MPElementType)previousType:(MPElementType)type; diff --git a/MasterPassword/ObjC/MPAlgorithmV0.m b/MasterPassword/ObjC/MPAlgorithmV0.m index 6638ca2d..efa47966 100644 --- a/MasterPassword/ObjC/MPAlgorithmV0.m +++ b/MasterPassword/ObjC/MPAlgorithmV0.m @@ -1,12 +1,12 @@ /** - * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) - * - * See the enclosed file LICENSE for license information (LGPLv3). If you did - * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt - * - * @author Maarten Billemont - * @license http://www.gnu.org/licenses/lgpl-3.0.txt - */ +* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) +* +* See the enclosed file LICENSE for license information (LGPLv3). If you did +* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt +* +* @author Maarten Billemont +* @license http://www.gnu.org/licenses/lgpl-3.0.txt +*/ // // MPAlgorithmV0 @@ -53,7 +53,7 @@ migrationRequest.predicate = [NSPredicate predicateWithFormat:@"version_ < %d AND user == %@", self.version, user]; NSArray *migrationElements = [moc executeFetchRequest:migrationRequest error:&error]; if (!migrationElements) { - err(@"While looking for elements to migrate: %@", error); + err( @"While looking for elements to migrate: %@", error ); return NO; } @@ -68,7 +68,7 @@ - (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit { if (element.version != [self version] - 1) - // Only migrate from previous version. + // Only migrate from previous version. return NO; if (!explicit) { @@ -85,18 +85,19 @@ - (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName { - uint32_t nuserNameLength = htonl(userName.length); + uint32_t nuserNameLength = htonl( userName.length ); NSDate *start = [NSDate date]; NSData *keyData = [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding] - usingSalt:[NSData dataByConcatenatingDatas: - [@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding], - [NSData dataWithBytes:&nuserNameLength - length:sizeof(nuserNameLength)], - [userName dataUsingEncoding:NSUTF8StringEncoding], - nil] N:MP_N r:MP_r p:MP_p]; + usingSalt:[NSData dataByConcatenatingDatas: + [@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding], + [NSData dataWithBytes:&nuserNameLength + length:sizeof( nuserNameLength )], + [userName dataUsingEncoding:NSUTF8StringEncoding], + nil] N:MP_N r:MP_r p:MP_p]; MPKey *key = [self keyFromKeyData:keyData]; - trc(@"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", userName, password, [key.keyID encodeHex], -[start timeIntervalSinceNow]); + trc( @"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", userName, password, [key.keyID encodeHex], + -[start timeIntervalSinceNow] ); return key; } @@ -142,7 +143,7 @@ return @"Device Private Password"; } - Throw(@"Type not supported: %lu", (long)type); + Throw( @"Type not supported: %lu", (long)type ); } - (NSString *)shortNameOfType:(MPElementType)type { @@ -176,7 +177,7 @@ return @"Device"; } - Throw(@"Type not supported: %lu", (long)type); + Throw( @"Type not supported: %lu", (long)type ); } - (NSString *)classNameOfType:(MPElementType)type { @@ -187,7 +188,7 @@ - (Class)classOfType:(MPElementType)type { if (!type) - Throw(@"No type given."); + Throw( @"No type given." ); switch (type) { case MPElementTypeGeneratedMaximum: @@ -215,14 +216,27 @@ return [MPElementStoredEntity class]; } - Throw(@"Type not supported: %lu", (long)type); + Throw( @"Type not supported: %lu", (long)type ); +} + +- (NSArray *)allTypes { + + return [self allTypesStartingWith:MPElementTypeGeneratedMaximum]; +} + +- (NSArray *)allTypesStartingWith:(MPElementType)startingType { + + NSMutableArray *allTypes = [[NSMutableArray alloc] initWithCapacity:8]; + MPElementType currentType = startingType; + do { + [allTypes addObject:@(currentType)]; + } while ((currentType = [self nextType:currentType]) != startingType); + + return allTypes; } - (MPElementType)nextType:(MPElementType)type { - if (!type) - Throw(@"No type given."); - switch (type) { case MPElementTypeGeneratedMaximum: return MPElementTypeStoredDevicePrivate; @@ -240,9 +254,9 @@ return MPElementTypeGeneratedPIN; case MPElementTypeStoredDevicePrivate: return MPElementTypeStoredPersonal; + default: + return MPElementTypeGeneratedLong; } - - Throw(@"Type not supported: %lu", (long)type); } - (MPElementType)previousType:(MPElementType)type { @@ -262,38 +276,38 @@ [[NSBundle mainBundle] URLForResource:@"ciphers" withExtension:@"plist"]]; // Determine the seed whose bytes will be used for calculating a password - uint32_t ncounter = htonl(counter), nnameLength = htonl(name.length); - NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof(ncounter)]; - NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof(nnameLength)]; - trc(@"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64], - [nameLengthBytes encodeHex], name, [counterBytes encodeHex]); + uint32_t ncounter = htonl( counter ), nnameLength = htonl( name.length ); + NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof( ncounter )]; + NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof( nnameLength )]; + trc( @"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64], + [nameLengthBytes encodeHex], name, [counterBytes encodeHex] ); NSData *seed = [[NSData dataByConcatenatingDatas: [@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding], - nameLengthBytes, [name dataUsingEncoding:NSUTF8StringEncoding], - counterBytes, nil] + nameLengthBytes, [name dataUsingEncoding:NSUTF8StringEncoding], + counterBytes, nil] hmacWith:PearlHashSHA256 key:key.keyData]; - trc(@"seed is: %@", [seed encodeBase64]); + trc( @"seed is: %@", [seed encodeBase64] ); const char *seedBytes = seed.bytes; // Determine the cipher from the first seed byte. - NSAssert([seed length], @"Missing seed."); + NSAssert( [seed length], @"Missing seed." ); NSString *typeClass = [self classNameOfType:type]; NSString *typeName = [self nameOfType:type]; id classCiphers = [MPTypes_ciphers valueForKey:typeClass]; NSArray *typeCiphers = [classCiphers valueForKey:typeName]; - NSString *cipher = typeCiphers[htons(seedBytes[0]) % [typeCiphers count]]; - trc(@"type %@, ciphers: %@, selected: %@", typeName, typeCiphers, cipher); + NSString *cipher = typeCiphers[htons( seedBytes[0] ) % [typeCiphers count]]; + trc( @"type %@, ciphers: %@, selected: %@", typeName, typeCiphers, cipher ); // Encode the content, character by character, using subsequent seed bytes and the cipher. - NSAssert([seed length] >= [cipher length] + 1, @"Insufficient seed bytes to encode cipher."); + NSAssert( [seed length] >= [cipher length] + 1, @"Insufficient seed bytes to encode cipher." ); NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]]; for (NSUInteger c = 0; c < [cipher length]; ++c) { - uint16_t keyByte = htons(seedBytes[c + 1]); + uint16_t keyByte = htons( seedBytes[c + 1] ); NSString *cipherClass = [cipher substringWithRange:NSMakeRange( c, 1 )]; NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass]; NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange( keyByte % [cipherClassCharacters length], 1 )]; - trc(@"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character); + trc( @"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character ); [content appendString:character]; } @@ -307,7 +321,7 @@ - (void)saveContent:(NSString *)clearContent toElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey { - NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user."); + NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." ); switch (element.type) { case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedLong: @@ -315,13 +329,14 @@ case MPElementTypeGeneratedBasic: case MPElementTypeGeneratedShort: case MPElementTypeGeneratedPIN: { - NSAssert(NO, @"Cannot save content to element with generated type %lu.", (long)element.type); + NSAssert( NO, @"Cannot save content to element with generated type %lu.", (long)element.type ); break; } case MPElementTypeStoredPersonal: { - NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); + NSAssert( [element isKindOfClass:[MPElementStoredEntity class]], + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, + [element class] ); NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding] encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES]; @@ -329,8 +344,9 @@ break; } case MPElementTypeStoredDevicePrivate: { - NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); + NSAssert( [element isKindOfClass:[MPElementStoredEntity class]], + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, + [element class] ); NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding] encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES]; @@ -364,9 +380,9 @@ return result; } -- (void)resolveContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey result:(void (^)(NSString *result))resultBlock { +- (void)resolveContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey result:(void ( ^ )(NSString *result))resultBlock { - NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user."); + NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." ); switch (element.type) { case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedLong: @@ -374,17 +390,18 @@ case MPElementTypeGeneratedBasic: case MPElementTypeGeneratedShort: case MPElementTypeGeneratedPIN: { - NSAssert([element isKindOfClass:[MPElementGeneratedEntity class]], - @"Element with generated type %lu is not an MPElementGeneratedEntity, but a %@.", (long)element.type, [element class]); + NSAssert( [element isKindOfClass:[MPElementGeneratedEntity class]], + @"Element with generated type %lu is not an MPElementGeneratedEntity, but a %@.", (long)element.type, + [element class] ); NSString *name = element.name; MPElementType type = element.type; NSUInteger counter = ((MPElementGeneratedEntity *)element).counter; id algorithm = nil; if (!element.name.length) - err(@"Missing name."); + err( @"Missing name." ); else if (!elementKey.keyData.length) - err(@"Missing key."); + err( @"Missing key." ); else algorithm = element.algorithm; @@ -396,8 +413,9 @@ } case MPElementTypeStoredPersonal: { - NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); + NSAssert( [element isKindOfClass:[MPElementStoredEntity class]], + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, + [element class] ); NSData *encryptedContent = ((MPElementStoredEntity *)element).contentObject; @@ -408,8 +426,9 @@ break; } case MPElementTypeStoredDevicePrivate: { - NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); + NSAssert( [element isKindOfClass:[MPElementStoredEntity class]], + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, + [element class] ); NSDictionary *elementQuery = [self queryForDevicePrivateElementNamed:element.name]; NSData *encryptedContent = [PearlKeyChain dataOfItemForQuery:elementQuery]; @@ -426,7 +445,7 @@ - (void)importProtectedContent:(NSString *)protectedContent protectedByKey:(MPKey *)importKey intoElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey { - NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user."); + NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." ); switch (element.type) { case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedLong: @@ -437,8 +456,9 @@ break; case MPElementTypeStoredPersonal: { - NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); + NSAssert( [element isKindOfClass:[MPElementStoredEntity class]], + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, + [element class] ); if ([importKey.keyID isEqualToData:elementKey.keyID]) ((MPElementStoredEntity *)element).contentObject = [protectedContent decodeBase64]; @@ -456,7 +476,7 @@ - (void)importClearTextContent:(NSString *)clearContent intoElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey { - NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user."); + NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." ); switch (element.type) { case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedLong: @@ -478,7 +498,7 @@ - (NSString *)exportContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey { - NSAssert([elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user."); + NSAssert( [elementKey.keyID isEqualToData:element.user.keyID], @"Element does not belong to current user." ); if (!(element.type & MPElementFeatureExportContent)) return nil; @@ -495,8 +515,9 @@ } case MPElementTypeStoredPersonal: { - NSAssert([element isKindOfClass:[MPElementStoredEntity class]], - @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); + NSAssert( [element isKindOfClass:[MPElementStoredEntity class]], + @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, + [element class] ); result = [((MPElementStoredEntity *)element).contentObject encodeBase64]; break; } diff --git a/MasterPassword/ObjC/MPAppDelegate_Key.m b/MasterPassword/ObjC/MPAppDelegate_Key.m index 37fcdf58..69286ae8 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Key.m +++ b/MasterPassword/ObjC/MPAppDelegate_Key.m @@ -16,7 +16,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) { return [PearlKeyChain createQueryForClass:kSecClassGenericPassword attributes:@{ (__bridge id)kSecAttrService : @"Saved Master Password", - (__bridge id)kSecAttrAccount : IfNotNilElse(user.name, @"") + (__bridge id)kSecAttrAccount : IfNotNilElse( user.name, @"" ) } matches:nil]; } @@ -25,11 +25,11 @@ static NSDictionary *keyQuery(MPUserEntity *user) { NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )]; if (!keyData) { - inf(@"No key found in keychain for: %@", user.userID); + inf( @"No key found in keychain for: %@", user.userID ); return nil; } - inf(@"Found key in keychain for: %@", user.userID); + inf( @"Found key in keychain for: %@", user.userID ); return [MPAlgorithmDefault keyFromKeyData:keyData]; } @@ -39,7 +39,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) { NSData *existingKeyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )]; if (![existingKeyData isEqualToData:self.key.keyData]) { - inf(@"Saving key in keychain for: %@", user.userID); + inf( @"Saving key in keychain for: %@", user.userID ); [PearlKeyChain addOrUpdateItemForQuery:keyQuery( user ) withAttributes:@{ @@ -56,7 +56,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) { OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery( user )]; if (result == noErr) { - inf(@"Removed key from keychain for: %@", user.userID); + inf( @"Removed key from keychain for: %@", user.userID ); [[NSNotificationCenter defaultCenter] postNotificationName:MPKeyForgottenNotification object:self]; } @@ -74,7 +74,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) { - (BOOL)signInAsUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc usingMasterPassword:(NSString *)password { if (password) - NSAssert(![NSThread isMainThread], @"Computing key must not happen from the main thread."); + NSAssert( ![NSThread isMainThread], @"Computing key must not happen from the main thread." ); if (!user) return NO; @@ -92,14 +92,14 @@ static NSDictionary *keyQuery(MPUserEntity *user) { // Method 2: Depending on the user's saveKey, load or remove the key from the keychain. if (!user.saveKey) - // Key should not be stored in keychain. Delete it. + // Key should not be stored in keychain. Delete it. [self forgetSavedKeyFor:user]; else if (!tryKey) { // Key should be saved in keychain. Load it. if ((tryKey = [self loadSavedKeyFor:user]) && ![user.keyID isEqual:tryKey.keyID]) { // Loaded password doesn't match user's keyID. Forget saved password: it is incorrect. - inf(@"Saved password doesn't match keyID for: %@", user.userID); + inf( @"Saved password doesn't match keyID for: %@", user.userID ); tryKey = nil; [self forgetSavedKeyFor:user]; @@ -110,7 +110,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) { if (!tryKey) { if ([password length]) if ((tryKey = [MPAlgorithmDefault keyForPassword:password ofUserNamed:user.name])) if (![user.keyID isEqual:tryKey.keyID]) { - inf(@"Key derived from password doesn't match keyID for: %@", user.userID); + inf( @"Key derived from password doesn't match keyID for: %@", user.userID ); tryKey = nil; } @@ -119,13 +119,13 @@ static NSDictionary *keyQuery(MPUserEntity *user) { // No more methods left, fail if key still not known. if (!tryKey) { if (password) { - inf(@"Login failed for: %@", user.userID); + inf( @"Login failed for: %@", user.userID ); MPCheckpoint( MPCheckpointSignInFailed, nil ); } return NO; } - inf(@"Logged in: %@", user.userID); + inf( @"Logged in: %@", user.userID ); if (![self.key isEqualToKey:tryKey]) { self.key = tryKey; @@ -147,13 +147,19 @@ static NSDictionary *keyQuery(MPUserEntity *user) { } } @catch (id exception) { - err(@"While setting username: %@", exception); + err( @"While setting username: %@", exception ); } user.lastUsed = [NSDate date]; [moc saveToStore]; self.activeUser = user; + // Perform a data sanity check now that we're logged in as the user to allow fixes that require the user's key. + if ([[MPConfig get].checkInconsistency boolValue]) + [MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) { + [self findAndFixInconsistenciesSaveInContext:context]; + }]; + [[NSNotificationCenter defaultCenter] postNotificationName:MPSignedInNotification object:self]; MPCheckpoint( MPCheckpointSignedIn, nil ); @@ -163,12 +169,13 @@ static NSDictionary *keyQuery(MPUserEntity *user) { - (void)migrateElementsForUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc toKey:(MPKey *)newKey { if (![user.elements count]) - // Nothing to migrate. + // Nothing to migrate. return; MPKey *recoverKey = newKey; #ifdef PEARL_UIKIT - PearlOverlay *activityOverlay = [PearlOverlay showProgressOverlayWithTitle:PearlString( @"Migrating %ld sites...", (long)[user.elements count] )]; + PearlOverlay *activityOverlay = [PearlOverlay showProgressOverlayWithTitle:PearlString( @"Migrating %ld sites...", + (long)[user.elements count] )]; #endif for (MPElementEntity *element in user.elements) { @@ -183,12 +190,12 @@ static NSDictionary *keyQuery(MPUserEntity *user) { dispatch_group_enter( recoverPasswordGroup ); [PearlAlert showAlertWithTitle:@"Enter Old Master Password" message:PearlString( @"Your old master password is required to migrate the stored password for %@", - element.name ) + element.name ) viewStyle:UIAlertViewStyleSecureTextInput initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) { @try { if (buttonIndex_ == [alert_ cancelButtonIndex]) - // Don't Migrate + // Don't Migrate return; masterPassword = [alert_ textFieldAtIndex:0].text; @@ -200,14 +207,14 @@ static NSDictionary *keyQuery(MPUserEntity *user) { dispatch_group_wait( recoverPasswordGroup, DISPATCH_TIME_FOREVER ); #endif if (!masterPassword) - // Don't Migrate + // Don't Migrate break; recoverKey = [element.algorithm keyForPassword:masterPassword ofUserNamed:user.name]; } if (!content) - // Don't Migrate + // Don't Migrate break; if (![recoverKey isEqualToKey:newKey]) diff --git a/MasterPassword/ObjC/MPAppDelegate_Store.h b/MasterPassword/ObjC/MPAppDelegate_Store.h index ef8cc1c5..cd5b5c62 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Store.h +++ b/MasterPassword/ObjC/MPAppDelegate_Store.h @@ -9,6 +9,7 @@ #import "MPAppDelegate_Shared.h" #import "UbiquityStoreManager.h" +#import "MPFixable.h" typedef enum { MPImportResultSuccess, @@ -27,6 +28,7 @@ typedef enum { + (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock; - (UbiquityStoreManager *)storeManager; +- (MPFixableResult)findAndFixInconsistenciesSaveInContext:(NSManagedObjectContext *)context; /** @param completion The block to execute after adding the element, executed from the main thread with the new element in the main MOC. */ - (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion; diff --git a/MasterPassword/ObjC/MPAppDelegate_Store.m b/MasterPassword/ObjC/MPAppDelegate_Store.m index 0b9f399c..ee238736 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Store.m +++ b/MasterPassword/ObjC/MPAppDelegate_Store.m @@ -18,13 +18,13 @@ #define MPMigrationLevelLocalStoreKey @"MPMigrationLevelLocalStoreKey" #define MPMigrationLevelCloudStoreKey @"MPMigrationLevelCloudStoreKey" -typedef NS_ENUM(NSInteger, MPMigrationLevelLocalStore) { +typedef NS_ENUM( NSInteger, MPMigrationLevelLocalStore ) { MPMigrationLevelLocalStoreV1, MPMigrationLevelLocalStoreV2, MPMigrationLevelLocalStoreCurrent = MPMigrationLevelLocalStoreV2, }; -typedef NS_ENUM(NSInteger, MPMigrationLevelCloudStore) { +typedef NS_ENUM( NSInteger, MPMigrationLevelCloudStore ) { MPMigrationLevelCloudStoreV1, MPMigrationLevelCloudStoreV2, MPMigrationLevelCloudStoreV3, @@ -32,16 +32,18 @@ typedef NS_ENUM(NSInteger, MPMigrationLevelCloudStore) { }; @implementation MPAppDelegate_Shared(Store) - PearlAssociatedObjectProperty(id, SaveObserver, saveObserver); -PearlAssociatedObjectProperty(NSManagedObjectContext*, PrivateManagedObjectContext, privateManagedObjectContext); -PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, mainManagedObjectContext); +PearlAssociatedObjectProperty( id, SaveObserver, saveObserver ); + +PearlAssociatedObjectProperty( NSManagedObjectContext*, PrivateManagedObjectContext, privateManagedObjectContext ); + +PearlAssociatedObjectProperty( NSManagedObjectContext*, MainManagedObjectContext, mainManagedObjectContext ); #pragma mark - Core Data setup + (NSManagedObjectContext *)managedObjectContextForMainThreadIfReady { - NSAssert([[NSThread currentThread] isMainThread], @"Can only access main MOC from the main thread."); + NSAssert( [[NSThread currentThread] isMainThread], @"Can only access main MOC from the main thread." ); NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady]; if (!mainManagedObjectContext || ![[NSThread currentThread] isMainThread]) return nil; @@ -49,7 +51,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, return mainManagedObjectContext; } -+ (BOOL)managedObjectContextForMainThreadPerformBlock:(void (^)(NSManagedObjectContext *mainContext))mocBlock { ++ (BOOL)managedObjectContextForMainThreadPerformBlock:(void ( ^ )(NSManagedObjectContext *mainContext))mocBlock { NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady]; if (!mainManagedObjectContext) @@ -62,7 +64,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, return YES; } -+ (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void (^)(NSManagedObjectContext *mainContext))mocBlock { ++ (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void ( ^ )(NSManagedObjectContext *mainContext))mocBlock { NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady]; if (!mainManagedObjectContext) @@ -75,7 +77,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, return YES; } -+ (BOOL)managedObjectContextPerformBlock:(void (^)(NSManagedObjectContext *context))mocBlock { ++ (BOOL)managedObjectContextPerformBlock:(void ( ^ )(NSManagedObjectContext *context))mocBlock { NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady]; if (!privateManagedObjectContextIfReady) @@ -90,7 +92,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, return YES; } -+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock { ++ (BOOL)managedObjectContextPerformBlockAndWait:(void ( ^ )(NSManagedObjectContext *context))mocBlock { NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady]; if (!privateManagedObjectContextIfReady) @@ -132,14 +134,14 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification object:UIApp queue:[NSOperationQueue mainQueue] usingBlock: ^(NSNotification *note) { - [[self mainManagedObjectContext] saveToStore]; - }]; + [[self mainManagedObjectContext] saveToStore]; + }]; [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:UIApp queue:[NSOperationQueue mainQueue] usingBlock: ^(NSNotification *note) { - [[self mainManagedObjectContext] saveToStore]; - }]; + [[self mainManagedObjectContext] saveToStore]; + }]; #else [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification object:NSApp queue:[NSOperationQueue mainQueue] usingBlock: @@ -151,6 +153,41 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, return storeManager; } +- (MPFixableResult)findAndFixInconsistenciesSaveInContext:(NSManagedObjectContext *)context { + + NSError *error = nil; + NSFetchRequest *fetchRequest = [NSFetchRequest new]; + fetchRequest.fetchBatchSize = 50; + + MPFixableResult result = MPFixableResultNoProblems; + for (NSEntityDescription *entity in [context.persistentStoreCoordinator.managedObjectModel entities]) + if (class_conformsToProtocol( NSClassFromString( entity.managedObjectClassName ), @protocol(MPFixable) )) { + fetchRequest.entity = entity; + NSArray *objects = [context executeFetchRequest:fetchRequest error:&error]; + if (!objects) { + err( @"Failed to fetch %@ objects: %@", entity, error ); + continue; + } + + for (NSManagedObject *object in objects) + result = MPApplyFix( result, ^MPFixableResult { + return [object findAndFixInconsistenciesInContext:context]; + } ); + } + + if (result == MPFixableResultNoProblems) + inf( @"Sanity check found no problems in store." ); + + else { + [context saveToStore]; + [[NSNotificationCenter defaultCenter] postNotificationName:MPFoundInconsistenciesNotification object:nil userInfo:@{ + MPInconsistenciesFixResultUserKey : @(result) + }]; + } + + return result; +} + - (void)migrateStoreForManager:(UbiquityStoreManager *)manager isCloud:(BOOL)isCloudStore { [self migrateLocalStore]; @@ -163,42 +200,42 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, MPMigrationLevelLocalStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelLocalStoreKey]; if (migrationLevel >= MPMigrationLevelLocalStoreCurrent) - // Local store up-to-date. + // Local store up-to-date. return; - inf(@"Local store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelLocalStoreCurrent); + inf( @"Local store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelLocalStoreCurrent ); if (migrationLevel <= MPMigrationLevelLocalStoreV1) if (![self migrateV1LocalStore]) { - inf(@"Failed to migrate old V1 to new local store."); + inf( @"Failed to migrate old V1 to new local store." ); return; } [[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelLocalStoreCurrent forKey:MPMigrationLevelLocalStoreKey]; - inf(@"Successfully migrated old to new local store."); + inf( @"Successfully migrated old to new local store." ); } - (void)migrateCloudStore { MPMigrationLevelCloudStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelCloudStoreKey]; if (migrationLevel >= MPMigrationLevelCloudStoreCurrent) - // Cloud store up-to-date. + // Cloud store up-to-date. return; - inf(@"Cloud store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelCloudStoreCurrent); + inf( @"Cloud store migration level: %d (current %d)", (signed)migrationLevel, (signed)MPMigrationLevelCloudStoreCurrent ); if (migrationLevel <= MPMigrationLevelCloudStoreV1) { if (![self migrateV1CloudStore]) { - inf(@"Failed to migrate old V1 to new cloud store."); + inf( @"Failed to migrate old V1 to new cloud store." ); return; } } else if (migrationLevel <= MPMigrationLevelCloudStoreV2) { if (![self migrateV2CloudStore]) { - inf(@"Failed to migrate old V2 to new cloud store."); + inf( @"Failed to migrate old V2 to new cloud store." ); return; } } [[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelCloudStoreCurrent forKey:MPMigrationLevelCloudStoreKey]; - inf(@"Successfully migrated old to new cloud store."); + inf( @"Successfully migrated old to new cloud store." ); } - (BOOL)migrateV1CloudStore { @@ -211,11 +248,11 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, // Migrate cloud store. NSString *uuid = [[NSUserDefaults standardUserDefaults] stringForKey:@"LocalUUIDKey"]; if (!uuid) { - inf(@"No V1 cloud store to migrate."); + inf( @"No V1 cloud store to migrate." ); return YES; } - inf(@"Migrating V1 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"]); + inf( @"Migrating V1 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"] ); NSURL *cloudContainerURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:MPCloudContainerIdentifier]; NSURL *oldCloudContentURL = [[cloudContainerURL URLByAppendingPathComponent:@"Data" isDirectory:YES] @@ -232,11 +269,11 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, // Migrate cloud store. NSString *uuid = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:@"USMStoreUUIDKey"]; if (!uuid) { - inf(@"No V2 cloud store to migrate."); + inf( @"No V2 cloud store to migrate." ); return YES; } - inf(@"Migrating V2 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"]); + inf( @"Migrating V2 cloud store: %@ -> %@", uuid, [self.storeManager valueForKey:@"storeUUID"] ); NSURL *cloudContainerURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:MPCloudContainerIdentifier]; NSURL *oldCloudContentURL = [[cloudContainerURL URLByAppendingPathComponent:@"CloudLogs" isDirectory:YES] @@ -255,11 +292,11 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, NSURL *oldLocalStoreURL = [[applicationFilesDirectory URLByAppendingPathComponent:@"MasterPassword" isDirectory:NO] URLByAppendingPathExtension:@"sqlite"]; if (![[NSFileManager defaultManager] fileExistsAtPath:oldLocalStoreURL.path isDirectory:NO]) { - inf(@"No V1 local store to migrate."); + inf( @"No V1 local store to migrate." ); return YES; } - inf(@"Migrating V1 local store"); + inf( @"Migrating V1 local store" ); return [self migrateFromLocalStore:oldLocalStoreURL]; } @@ -267,7 +304,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, NSURL *newLocalStoreURL = [self.storeManager URLForLocalStore]; if ([[NSFileManager defaultManager] fileExistsAtPath:newLocalStoreURL.path isDirectory:NO]) { - wrn(@"Can't migrate local store: A new local store already exists."); + wrn( @"Can't migrate local store: A new local store already exists." ); return YES; } @@ -278,14 +315,14 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, return NO; } - inf(@"Successfully migrated to new local store."); + inf( @"Successfully migrated to new local store." ); return YES; } - (BOOL)migrateFromCloudStore:(NSURL *)oldCloudStoreURL cloudContent:(NSURL *)oldCloudContentURL { if (![self.storeManager cloudSafeForSeeding]) { - inf(@"Can't migrate cloud store: A new cloud store already exists."); + inf( @"Can't migrate cloud store: A new cloud store already exists." ); return YES; } @@ -295,7 +332,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, strategy:0 error:nil cause:nil context:nil]) return NO; - inf(@"Successfully migrated to new cloud store."); + inf( @"Successfully migrated to new cloud store." ); return YES; } @@ -309,7 +346,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message { - inf(@"[StoreManager] %@", message); + inf( @"[StoreManager] %@", message ); } - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore { @@ -334,7 +371,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore { - inf(@"Using iCloud? %@", @(isCloudStore)); + inf( @"Using iCloud? %@", @(isCloudStore) ); MPCheckpoint( MPCheckpointCloud, @{ @"enabled" : @(isCloudStore) } ); @@ -369,31 +406,37 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, self.saveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:privateManagedObjectContext queue:nil usingBlock: ^(NSNotification *note) { - // When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext. - [mainManagedObjectContext performBlock:^{ - [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note]; - }]; - }]; + // When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext. + [mainManagedObjectContext performBlock:^{ + [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note]; + }]; + }]; self.privateManagedObjectContext = privateManagedObjectContext; self.mainManagedObjectContext = mainManagedObjectContext; + + // Perform a data sanity check on the newly loaded store to find and fix any issues. + if ([[MPConfig get].checkInconsistency boolValue]) + [MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) { + [self findAndFixInconsistenciesSaveInContext:context]; + }]; } - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreErrorCause)cause context:(id)context { - err(@"[StoreManager] ERROR: cause=%@, context=%@, error=%@", NSStringFromUSMCause( cause ), context, error); + err( @"[StoreManager] ERROR: cause=%@, context=%@, error=%@", NSStringFromUSMCause( cause ), context, error ); MPCheckpoint( MPCheckpointMPErrorUbiquity, @{ @"cause" : @(cause), @"error.code" : @(error.code), - @"error.domain" : NilToNSNull(error.domain), - @"error.reason" : NilToNSNull(IfNotNilElse( [error localizedFailureReason], [error localizedDescription] )), + @"error.domain" : NilToNSNull( error.domain ), + @"error.reason" : NilToNSNull( IfNotNilElse( [error localizedFailureReason], [error localizedDescription] ) ), } ); } #pragma mark - Utilities -- (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion { +- (void)addElementNamed:(NSString *)siteName completion:(void ( ^ )(MPElementEntity *element))completion { if (![siteName length]) { completion( nil ); @@ -402,7 +445,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, [MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { MPUserEntity *activeUser = [self activeUserInContext:context]; - NSAssert(activeUser, @"Missing user."); + NSAssert( activeUser, @"Missing user." ); if (!activeUser) { completion( nil ); return; @@ -420,7 +463,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, NSError *error = nil; if (element.objectID.isTemporaryID && ![context obtainPermanentIDsForObjects:@[ element ] error:&error]) - err(@"Failed to obtain a permanent object ID after creating new element: %@", error); + err( @"Failed to obtain a permanent object ID after creating new element: %@", error ); [context saveToStore]; @@ -452,7 +495,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, NSError *error = nil; if (![context obtainPermanentIDsForObjects:@[ newElement ] error:&error]) - err(@"Failed to obtain a permanent object ID after changing object type: %@", error); + err( @"Failed to obtain a permanent object ID after changing object type: %@", error ); [context deleteObject:element]; [context saveToStore]; @@ -466,10 +509,10 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, } - (MPImportResult)importSites:(NSString *)importedSitesString - askImportPassword:(NSString *(^)(NSString *userName))importPassword - askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword { + askImportPassword:(NSString *( ^ )(NSString *userName))importPassword + askUserPassword:(NSString *( ^ )(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword { - NSAssert(![[NSThread currentThread] isMainThread], @"This method should not be invoked from the main thread."); + NSAssert( ![[NSThread currentThread] isMainThread], @"This method should not be invoked from the main thread." ); __block MPImportResult result = MPImportResultCancelled; do { @@ -485,8 +528,8 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, } - (MPImportResult)importSites:(NSString *)importedSitesString - askImportPassword:(NSString *(^)(NSString *userName))askImportPassword - askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))askUserPassword + askImportPassword:(NSString *( ^ )(NSString *userName))askImportPassword + askUserPassword:(NSString *( ^ )(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))askUserPassword saveInContext:(NSManagedObjectContext *)context { // Compile patterns. @@ -497,7 +540,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, initWithPattern:@"^#[[:space:]]*([^:]+): (.*)" options:(NSRegularExpressionOptions)0 error:&error]; if (error) { - err(@"Error loading the header pattern: %@", error); + err( @"Error loading the header pattern: %@", error ); return MPImportResultInternalError; } } @@ -506,13 +549,13 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)(:[[:digit:]]+)?[[:space:]]+([^\t]+)\t(.*)" options:(NSRegularExpressionOptions)0 error:&error]; if (error) { - err(@"Error loading the site pattern: %@", error); + err( @"Error loading the site pattern: %@", error ); return MPImportResultInternalError; } } // Parse import data. - inf(@"Importing sites."); + inf( @"Importing sites." ); __block MPUserEntity *user = nil; id importAlgorithm = nil; NSString *importBundleVersion = nil, *importUserName = nil; @@ -540,7 +583,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, // Header if ([headerPattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0 range:NSMakeRange( 0, [importedSiteLine length] )] != 1) { - err(@"Invalid header format in line: %@", importedSiteLine); + err( @"Invalid header format in line: %@", importedSiteLine ); return MPImportResultMalformedInput; } NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:(NSMatchingOptions)0 @@ -554,16 +597,16 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", importUserName]; NSArray *users = [context executeFetchRequest:userFetchRequest error:&error]; if (!users) { - err(@"While looking for user: %@, error: %@", importUserName, error); + err( @"While looking for user: %@, error: %@", importUserName, error ); return MPImportResultInternalError; } if ([users count] > 1) { - err(@"While looking for user: %@, found more than one: %lu", importUserName, (unsigned long)[users count]); + err( @"While looking for user: %@, found more than one: %lu", importUserName, (unsigned long)[users count] ); return MPImportResultInternalError; } user = [users count]? [users lastObject]: nil; - dbg(@"Found user: %@", [user debugDescription]); + dbg( @"Found user: %@", [user debugDescription] ); } if ([headerName isEqualToString:@"Key ID"]) importKeyID = [headerValue decodeHex]; @@ -588,7 +631,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, // Site if ([sitePattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0 range:NSMakeRange( 0, [importedSiteLine length] )] != 1) { - err(@"Invalid site format in line: %@", importedSiteLine); + err( @"Invalid site format in line: %@", importedSiteLine ); return MPImportResultMalformedInput; } NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:(NSMatchingOptions)0 @@ -607,25 +650,26 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, elementFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user]; NSArray *existingSites = [context executeFetchRequest:elementFetchRequest error:&error]; if (!existingSites) { - err(@"Lookup of existing sites failed for site: %@, user: %@, error: %@", name, user.userID, error); + err( @"Lookup of existing sites failed for site: %@, user: %@, error: %@", name, user.userID, error ); return MPImportResultInternalError; } if ([existingSites count]) { - dbg(@"Existing sites: %@", existingSites); + dbg( @"Existing sites: %@", existingSites ); [elementsToDelete addObjectsFromArray:existingSites]; } } [importedSiteElements addObject:@[ lastUsed, uses, type, version, name, exportContent ]]; - dbg(@"Will import site: lastUsed=%@, uses=%@, type=%@, version=%@, name=%@, exportContent=%@", - lastUsed, uses, type, version, name, exportContent); + dbg( @"Will import site: lastUsed=%@, uses=%@, type=%@, version=%@, name=%@, exportContent=%@", + lastUsed, uses, type, version, name, exportContent ); } // Ask for confirmation to import these sites and the master password of the user. - inf(@"Importing %lu sites, deleting %lu sites, for user: %@", (unsigned long)[importedSiteElements count], (unsigned long)[elementsToDelete count], [MPUserEntity idFor:importUserName]); + inf( @"Importing %lu sites, deleting %lu sites, for user: %@", (unsigned long)[importedSiteElements count], + (unsigned long)[elementsToDelete count], [MPUserEntity idFor:importUserName] ); NSString *userMasterPassword = askUserPassword( user? user.name: importUserName, [importedSiteElements count], [elementsToDelete count] ); if (!userMasterPassword) { - inf(@"Import cancelled."); + inf( @"Import cancelled." ); return MPImportResultCancelled; } MPKey *userKey = [MPAlgorithmDefault keyForPassword:userMasterPassword ofUserNamed:user? user.name: importUserName]; @@ -641,7 +685,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, // Delete existing sites. if (elementsToDelete.count) [elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) { - inf(@"Deleting site: %@, it will be replaced by an imported site.", [obj name]); + inf( @"Deleting site: %@, it will be replaced by an imported site.", [obj name] ); [context deleteObject:obj]; }]; @@ -650,7 +694,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, user = [MPUserEntity insertNewObjectInContext:context]; user.name = importUserName; user.keyID = importKeyID; - dbg(@"Created User: %@", [user debugDescription]); + dbg( @"Created User: %@", [user debugDescription] ); } // Import new sites. @@ -678,13 +722,13 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, [element.algorithm importProtectedContent:exportContent protectedByKey:importKey intoElement:element usingKey:userKey]; } - dbg(@"Created Element: %@", [element debugDescription]); + dbg( @"Created Element: %@", [element debugDescription] ); } if (![context saveToStore]) return MPImportResultInternalError; - inf(@"Import completed successfully."); + inf( @"Import completed successfully." ); MPCheckpoint( MPCheckpointSitesImported, nil ); [[NSNotificationCenter defaultCenter] postNotificationName:MPSitesImportedNotification object:nil userInfo:@{ @@ -697,7 +741,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, - (NSString *)exportSitesRevealPasswords:(BOOL)revealPasswords { MPUserEntity *activeUser = [self activeUserForMainThread]; - inf(@"Exporting sites, %@, for: %@", revealPasswords? @"revealing passwords": @"omitting passwords", activeUser.userID); + inf( @"Exporting sites, %@, for: %@", revealPasswords? @"revealing passwords": @"omitting passwords", activeUser.userID ); // Header. NSMutableString *export = [NSMutableString new]; diff --git a/MasterPassword/ObjC/MPConfig.h b/MasterPassword/ObjC/MPConfig.h index e5784706..e8bffa16 100644 --- a/MasterPassword/ObjC/MPConfig.h +++ b/MasterPassword/ObjC/MPConfig.h @@ -14,5 +14,6 @@ @property(nonatomic, retain) NSNumber *rememberLogin; @property(nonatomic, retain) NSNumber *iCloudDecided; +@property(nonatomic, retain) NSNumber *checkInconsistency; @end diff --git a/MasterPassword/ObjC/MPConfig.m b/MasterPassword/ObjC/MPConfig.m index 1424d9d2..68783083 100644 --- a/MasterPassword/ObjC/MPConfig.m +++ b/MasterPassword/ObjC/MPConfig.m @@ -6,12 +6,11 @@ // Copyright (c) 2012 Lyndir. All rights reserved. // -#import "MPConfig.h" #import "MPAppDelegate_Shared.h" @implementation MPConfig -@dynamic sendInfo, rememberLogin, iCloudDecided; +@dynamic sendInfo, rememberLogin, iCloudDecided, checkInconsistency; - (id)init { @@ -19,11 +18,12 @@ return nil; [self.defaults registerDefaults:@{ - NSStringFromSelector( @selector(askForReviews) ) : @YES, + NSStringFromSelector( @selector( askForReviews ) ) : @YES, - NSStringFromSelector( @selector(sendInfo) ) : @NO, - NSStringFromSelector( @selector(rememberLogin) ) : @NO, - NSStringFromSelector( @selector(iCloudDecided) ) : @NO + NSStringFromSelector( @selector( sendInfo ) ) : @NO, + NSStringFromSelector( @selector( rememberLogin ) ) : @NO, + NSStringFromSelector( @selector( iCloudDecided ) ) : @NO, + NSStringFromSelector( @selector( checkInconsistency ) ) : @NO }]; self.delegate = [MPAppDelegate_Shared get]; diff --git a/MasterPassword/ObjC/MPElementEntity.h b/MasterPassword/ObjC/MPElementEntity.h index 587ea556..510d14f6 100644 --- a/MasterPassword/ObjC/MPElementEntity.h +++ b/MasterPassword/ObjC/MPElementEntity.h @@ -8,10 +8,11 @@ #import #import +#import "MPFixable.h" @class MPUserEntity; -@interface MPElementEntity : NSManagedObject +@interface MPElementEntity : NSManagedObject @property(nonatomic, retain) NSDate *lastUsed; @property(nonatomic, retain) NSString *loginName; diff --git a/MasterPassword/ObjC/MPElementEntity.m b/MasterPassword/ObjC/MPElementEntity.m index 52289517..d90110a4 100644 --- a/MasterPassword/ObjC/MPElementEntity.m +++ b/MasterPassword/ObjC/MPElementEntity.m @@ -19,4 +19,9 @@ @dynamic version_; @dynamic user; +- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context { + + return MPFixableResultNoProblems; +} + @end diff --git a/MasterPassword/ObjC/MPElementGeneratedEntity.m b/MasterPassword/ObjC/MPElementGeneratedEntity.m index 5f0441fe..6914a180 100644 --- a/MasterPassword/ObjC/MPElementGeneratedEntity.m +++ b/MasterPassword/ObjC/MPElementGeneratedEntity.m @@ -7,9 +7,49 @@ // #import "MPElementGeneratedEntity.h" +#import "MPAppDelegate_Shared.h" @implementation MPElementGeneratedEntity @dynamic counter_; +- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context { + + MPFixableResult result = [super findAndFixInconsistenciesInContext:context]; + + if (!self.type || self.type == (MPElementType)NSNotFound || ![[self.algorithm allTypes] containsObject:self.type_]) + // Invalid self.type + result = MPApplyFix( result, ^MPFixableResult { + wrn( @"Invalid type for: %@ of %@, type: %ld. Will use %ld instead.", + self.name, self.user.name, (long)self.type, (long)self.user.defaultType ); + self.type = self.user.defaultType; + return MPFixableResultProblemsFixed; + } ); + if (!self.type || self.type == (MPElementType)NSNotFound || ![[self.algorithm allTypes] containsObject:self.type_]) + // Invalid self.user.defaultType + result = MPApplyFix( result, ^MPFixableResult { + wrn( @"Invalid type for: %@ of %@, type: %ld. Will use %ld instead.", + self.name, self.user.name, (long)self.type, (long)MPElementTypeGeneratedLong ); + self.type = MPElementTypeGeneratedLong; + return MPFixableResultProblemsFixed; + } ); + if (![self isKindOfClass:[self.algorithm classOfType:self.type]]) + // Mismatch between self.type and self.class + result = MPApplyFix( result, ^MPFixableResult { + for (MPElementType newType = self.type; self.type != (newType = [self.algorithm nextType:newType]);) + if ([self isKindOfClass:[self.algorithm classOfType:newType]]) { + wrn( @"Mismatching type for: %@ of %@, type: %lu, class: %@. Will use %ld instead.", + self.name, self.user.name, (long)self.type, self.class, (long)newType ); + self.type = newType; + return MPFixableResultProblemsFixed; + } + + err( @"Mismatching type for: %@ of %@, type: %lu, class: %@. Couldn't find a type to fix problem with.", + self.name, self.user.name, (long)self.type, self.class ); + return MPFixableResultProblemsNotFixed; + } ); + + return result; +} + @end diff --git a/MasterPassword/ObjC/MPElementStoredEntity.h b/MasterPassword/ObjC/MPElementStoredEntity.h index c4cda5d7..0d0a4600 100644 --- a/MasterPassword/ObjC/MPElementStoredEntity.h +++ b/MasterPassword/ObjC/MPElementStoredEntity.h @@ -12,6 +12,6 @@ @interface MPElementStoredEntity : MPElementEntity -@property(nonatomic, retain) id contentObject; +@property(nonatomic, retain) NSData *contentObject; @end diff --git a/MasterPassword/ObjC/MPElementStoredEntity.m b/MasterPassword/ObjC/MPElementStoredEntity.m index 98d8360f..af6ecd6c 100644 --- a/MasterPassword/ObjC/MPElementStoredEntity.m +++ b/MasterPassword/ObjC/MPElementStoredEntity.m @@ -7,9 +7,31 @@ // #import "MPElementStoredEntity.h" +#import "MPEntities.h" +#import "MPAppDelegate_Shared.h" @implementation MPElementStoredEntity @dynamic contentObject; +- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context { + + MPFixableResult result = [super findAndFixInconsistenciesInContext:context]; + + if (self.contentObject && ![self.contentObject isKindOfClass:[NSData class]]) + result = MPApplyFix( result, ^MPFixableResult { + MPKey *key = [MPAppDelegate_Shared get].key; + if (key && [[MPAppDelegate_Shared get] activeUserInContext:context] == self.user) { + wrn( @"Content object not encrypted for: %@ of %@. Will re-encrypt.", self.name, self.user.name ); + [self.algorithm saveContent:[self.contentObject description] toElement:self usingKey:key]; + return MPFixableResultProblemsFixed; + } + + err( @"Content object not encrypted for: %@ of %@. Couldn't fix, please sign in.", self.name, self.user.name ); + return MPFixableResultProblemsNotFixed; + } ); + + return result; +} + @end diff --git a/MasterPassword/ObjC/MPEntities.m b/MasterPassword/ObjC/MPEntities.m index 1efc3208..8cd60ba3 100644 --- a/MasterPassword/ObjC/MPEntities.m +++ b/MasterPassword/ObjC/MPEntities.m @@ -38,34 +38,11 @@ - (MPElementType)type { - // Some people got elements with type == 0. - MPElementType type = (MPElementType)[self.type_ unsignedIntegerValue]; - if (!type || type == (MPElementType)NSNotFound) - type = [self.user defaultType]; - if (!type || type == (MPElementType)NSNotFound) - type = MPElementTypeGeneratedLong; - if (![self isKindOfClass:[self.algorithm classOfType:type]]) { -// NSAssert(NO, @"This object's class does not support the type: %lu", (long)type); - for (MPElementType aType = type; type != (aType = [self.algorithm nextType:aType]);) - if ([self isKindOfClass:[self.algorithm classOfType:aType]]) { - err(@"Invalid type for: %@, type: %lu. Will use %lu instead.", self.name, (long)type, (long)aType); - return aType; - } - } - - return type; + return (MPElementType)[self.type_ unsignedIntegerValue]; } - (void)setType:(MPElementType)aType { - // Make sure we don't poison our model data with invalid values. - if (!aType || aType == (MPElementType)NSNotFound) - aType = [self.user defaultType]; - if (!aType || aType == (MPElementType)NSNotFound) - aType = MPElementTypeGeneratedLong; - if (![self isKindOfClass:[self.algorithm classOfType:aType]]) - Throw(@"This object's class does not support the type: %lu", (long)aType); - self.type_ = @(aType); } @@ -132,12 +109,12 @@ - (NSString *)description { - return PearlString( @"%@:%@", [self class], [self name] ); + return strf( @"%@:%@", [self class], [self name] ); } - (NSString *)debugDescription { - return PearlString( @"{%@: name=%@, user=%@, type=%lu, uses=%ld, lastUsed=%@, version=%ld, loginName=%@, requiresExplicitMigration=%d}", + return strf( @"{%@: name=%@, user=%@, type=%lu, uses=%ld, lastUsed=%@, version=%ld, loginName=%@, requiresExplicitMigration=%d}", NSStringFromClass( [self class] ), self.name, self.user.name, (long)self.type, (long)self.uses, self.lastUsed, (long)self.version, self.loginName, self.requiresExplicitMigration ); } diff --git a/MasterPassword/ObjC/MPFixable.h b/MasterPassword/ObjC/MPFixable.h new file mode 100644 index 00000000..7ad01702 --- /dev/null +++ b/MasterPassword/ObjC/MPFixable.h @@ -0,0 +1,33 @@ +/** +* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) +* +* See the enclosed file LICENSE for license information (LGPLv3). If you did +* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt +* +* @author Maarten Billemont +* @license http://www.gnu.org/licenses/lgpl-3.0.txt +*/ + +// +// MPFixable.h +// MPFixable +// +// Created by lhunath on 2014-04-26. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import + +typedef NS_ENUM( NSUInteger, MPFixableResult ) { + MPFixableResultNoProblems, + MPFixableResultProblemsFixed, + MPFixableResultProblemsNotFixed, +}; + +MPFixableResult MPApplyFix(MPFixableResult previousResult, MPFixableResult(^fixBlock)(void)); + +@protocol MPFixable + +- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context; + +@end diff --git a/MasterPassword/ObjC/MPFixable.m b/MasterPassword/ObjC/MPFixable.m new file mode 100644 index 00000000..519981e8 --- /dev/null +++ b/MasterPassword/ObjC/MPFixable.m @@ -0,0 +1,40 @@ +/** +* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) +* +* See the enclosed file LICENSE for license information (LGPLv3). If you did +* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt +* +* @author Maarten Billemont +* @license http://www.gnu.org/licenses/lgpl-3.0.txt +*/ + +// +// MPFixable.m +// MPFixable +// +// Created by lhunath on 2014-04-26. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "MPFixable.h" + +MPFixableResult MPApplyFix(MPFixableResult previousResult, MPFixableResult(^fixBlock)(void)) { + + MPFixableResult additionalResult = fixBlock(); + switch (previousResult) { + case MPFixableResultNoProblems: + return additionalResult; + case MPFixableResultProblemsFixed: + switch (additionalResult) { + case MPFixableResultNoProblems: + case MPFixableResultProblemsFixed: + return previousResult; + case MPFixableResultProblemsNotFixed: + return additionalResult; + } + case MPFixableResultProblemsNotFixed: + return additionalResult; + } + + Throw( @"Unexpected previous=%ld or additional=%ld result.", (long)previousResult, (long)additionalResult ); +} diff --git a/MasterPassword/ObjC/MPTypes.h b/MasterPassword/ObjC/MPTypes.h index c9fd7949..338fbd1e 100644 --- a/MasterPassword/ObjC/MPTypes.h +++ b/MasterPassword/ObjC/MPTypes.h @@ -77,8 +77,10 @@ typedef NS_ENUM(NSUInteger, MPElementType) { #define MPElementUpdatedNotification @"MPElementUpdatedNotification" #define MPCheckConfigNotification @"MPCheckConfigNotification" #define MPSitesImportedNotification @"MPSitesImportedNotification" +#define MPFoundInconsistenciesNotification @"MPFoundInconsistenciesNotification" #define MPSitesImportedNotificationUserKey @"MPSitesImportedNotificationUserKey" +#define MPInconsistenciesFixResultUserKey @"MPInconsistenciesFixResultUserKey" static void MPCheckpoint(NSString *checkpoint, NSDictionary *attributes) { diff --git a/MasterPassword/ObjC/Mac/MPMacAppDelegate.m b/MasterPassword/ObjC/Mac/MPMacAppDelegate.m index c7fe0911..b5dad96e 100644 --- a/MasterPassword/ObjC/Mac/MPMacAppDelegate.m +++ b/MasterPassword/ObjC/Mac/MPMacAppDelegate.m @@ -243,8 +243,7 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven - (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue { - [[NSNotificationCenter defaultCenter] - postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey ) userInfo:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey )]; } #pragma mark - NSApplicationDelegate diff --git a/MasterPassword/ObjC/iOS/MPPasswordLargeStoredCell.m b/MasterPassword/ObjC/iOS/MPPasswordLargeStoredCell.m index 12e92d83..3fb430b9 100644 --- a/MasterPassword/ObjC/iOS/MPPasswordLargeStoredCell.m +++ b/MasterPassword/ObjC/iOS/MPPasswordLargeStoredCell.m @@ -44,10 +44,8 @@ element = [super saveContentTypeWithElement:element saveInContext:context]; MPElementStoredEntity *storedElement = [self storedElement:element]; - if (storedElement) { - storedElement.contentObject = self.contentField.text; - [context saveToStore]; - } + [storedElement.algorithm saveContent:self.contentField.text toElement:storedElement usingKey:[MPiOSAppDelegate get].key]; + [context saveToStore]; return element; } @@ -78,7 +76,7 @@ switch (self.contentFieldMode) { case MPContentFieldModePassword: { - storedElement.contentObject = newContent; + [storedElement.algorithm saveContent:newContent toElement:storedElement usingKey:[MPiOSAppDelegate get].key]; [context saveToStore]; PearlMainQueue( ^{ diff --git a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m index 852ddd6a..e4325f1a 100644 --- a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m +++ b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m @@ -68,6 +68,7 @@ [self registerObservers]; [self observeStore]; + [self updateFromConfig]; [self updatePasswords]; } @@ -464,6 +465,11 @@ self.passwordSelectionContainer.alpha = 1; }]; }], + [[NSNotificationCenter defaultCenter] + addObserverForName:MPCheckConfigNotification object:nil + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + [self updateFromConfig]; + }], ]; } @@ -504,6 +510,11 @@ [[NSNotificationCenter defaultCenter] removeObserver:_storeObserver]; } +- (void)updateFromConfig { + + self.passwordsSearchBar.keyboardType = [[MPiOSConfig get].dictationSearch boolValue]? UIKeyboardTypeDefault: UIKeyboardTypeURL; +} + - (void)updatePasswords { NSString *query = self.query; diff --git a/MasterPassword/ObjC/iOS/MPPreferencesViewController.h b/MasterPassword/ObjC/iOS/MPPreferencesViewController.h index 5efce9dc..a33e1f79 100644 --- a/MasterPassword/ObjC/iOS/MPPreferencesViewController.h +++ b/MasterPassword/ObjC/iOS/MPPreferencesViewController.h @@ -15,6 +15,7 @@ @property(weak, nonatomic) IBOutlet UITableViewCell *feedbackCell; @property(weak, nonatomic) IBOutlet UITableViewCell *coachmarksCell; @property(weak, nonatomic) IBOutlet UITableViewCell *exportCell; +@property(weak, nonatomic) IBOutlet UITableViewCell *checkInconsistencies; @property(weak, nonatomic) IBOutlet UIImageView *avatarImage; @property(weak, nonatomic) IBOutlet UISegmentedControl *generatedTypeControl; @property(weak, nonatomic) IBOutlet UISegmentedControl *storedTypeControl; diff --git a/MasterPassword/ObjC/iOS/MPPreferencesViewController.m b/MasterPassword/ObjC/iOS/MPPreferencesViewController.m index 130ff51f..30e6bd13 100644 --- a/MasterPassword/ObjC/iOS/MPPreferencesViewController.m +++ b/MasterPassword/ObjC/iOS/MPPreferencesViewController.m @@ -29,7 +29,7 @@ - (void)viewWillAppear:(BOOL)animated { - inf(@"Preferences will appear"); + inf( @"Preferences will appear" ); [super viewWillAppear:animated]; MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForMainThread]; @@ -70,6 +70,14 @@ [vc performSegueWithIdentifier:@"coachmarks" sender:self]; } } + if (cell == self.checkInconsistencies) + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + if ([[MPiOSAppDelegate get] findAndFixInconsistenciesSaveInContext:context] == MPFixableResultNoProblems) + [PearlAlert showAlertWithTitle:@"No Inconsistencies" message: + @"No inconsistencies were detected in your sites." + viewStyle:UIAlertViewStyleDefault initAlert:nil + tappedButtonBlock:nil cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil]; + }]; [tableView deselectRowAtIndexPath:indexPath animated:YES]; } @@ -93,7 +101,7 @@ self.storedTypeControl.selectedSegmentIndex = -1; else if (sender == self.storedTypeControl) self.generatedTypeControl.selectedSegmentIndex = -1; - + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { MPElementType defaultType = [[MPiOSAppDelegate get] activeUserInContext:context].defaultType = [self typeForSelectedSegment]; [context saveToStore]; @@ -162,7 +170,7 @@ case 1: return MPElementTypeStoredDevicePrivate; default: - Throw(@"unsupported selected type index: generated=%d, stored=%d", selectedGeneratedIndex, selectedStoredIndex); + Throw( @"unsupported selected type index: generated=%d, stored=%d", selectedGeneratedIndex, selectedStoredIndex ); } } } diff --git a/MasterPassword/ObjC/iOS/MPiOSAppDelegate.m b/MasterPassword/ObjC/iOS/MPiOSAppDelegate.m index ccd4ea6b..42359679 100644 --- a/MasterPassword/ObjC/iOS/MPiOSAppDelegate.m +++ b/MasterPassword/ObjC/iOS/MPiOSAppDelegate.m @@ -111,17 +111,20 @@ #endif } @catch (id exception) { - err(@"During Analytics Setup: %@", exception); + err( @"During Analytics Setup: %@", exception ); } @try { [[NSNotificationCenter defaultCenter] addObserverForName:MPCheckConfigNotification object:nil queue:nil usingBlock: ^(NSNotification *note) { - [self checkConfig]; - }]; - [[NSNotificationCenter defaultCenter] - addObserverForName:kIASKAppSettingChanged object:nil queue:nil usingBlock:^(NSNotification *note) { - [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification - object:note userInfo:nil]; + [self updateFromConfig]; + }]; + [[NSNotificationCenter defaultCenter] addObserverForName:kIASKAppSettingChanged object:nil queue:nil usingBlock: + ^(NSNotification *note) { + [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:note userInfo:nil]; + }]; + [[NSNotificationCenter defaultCenter] addObserverForName:NSUserDefaultsDidChangeNotification object:nil queue:nil usingBlock: + ^(NSNotification *note) { + [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:note userInfo:nil]; }]; #ifdef ADHOC @@ -137,18 +140,41 @@ #endif } @catch (id exception) { - err(@"During Config Test: %@", exception); + err( @"During Config Test: %@", exception ); } @try { [super application:application didFinishLaunchingWithOptions:launchOptions]; } @catch (id exception) { - err(@"During Pearl Application Launch: %@", exception); + err( @"During Pearl Application Launch: %@", exception ); } @try { - inf(@"Started up with device identifier: %@", [PearlKeyChain deviceIdentifier]); + inf( @"Started up with device identifier: %@", [PearlKeyChain deviceIdentifier] ); - dispatch_async( dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] addObserverForName:MPFoundInconsistenciesNotification object:nil queue:nil usingBlock: + ^(NSNotification *note) { + switch ((MPFixableResult)[note.userInfo[MPInconsistenciesFixResultUserKey] unsignedIntegerValue]) { + + case MPFixableResultNoProblems: + break; + case MPFixableResultProblemsFixed: + [PearlAlert showAlertWithTitle:@"Inconsistencies Fixed" message: + @"Some inconsistencies were detected in your sites.\n" + @"All issues were fixed." + viewStyle:UIAlertViewStyleDefault initAlert:nil + tappedButtonBlock:nil cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil]; + break; + case MPFixableResultProblemsNotFixed: + [PearlAlert showAlertWithTitle:@"Inconsistencies Found" message: + @"Some inconsistencies were detected in your sites.\n" + @"Not all issues could be fixed. Try signing in to each user or checking the logs." + viewStyle:UIAlertViewStyleDefault initAlert:nil + tappedButtonBlock:nil cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil]; + break; + } + }]; + + PearlMainQueue( ^{ if ([[MPiOSConfig get].showSetup boolValue]) [self.navigationController performSegueWithIdentifier:@"setup" sender:self]; } ); @@ -166,7 +192,7 @@ } ); } @catch (id exception) { - err(@"During Post-Startup: %@", exception); + err( @"During Post-Startup: %@", exception ); } return YES; @@ -186,7 +212,7 @@ NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:&response error:&error]; if (error) - err(@"While reading imported sites from %@: %@", url, error); + err( @"While reading imported sites from %@: %@", url, error ); if (!importedSitesData) return; @@ -227,7 +253,7 @@ dispatch_async( dispatch_get_main_queue(), ^{ [PearlAlert showAlertWithTitle:PearlString( @"Master Password for\n%@", userName ) message:PearlString( @"Imports %lu sites, overwriting %lu.", - (unsigned long)importCount, (unsigned long)deleteCount ) + (unsigned long)importCount, (unsigned long)deleteCount ) viewStyle:UIAlertViewStyleSecureTextInput initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) { @try { @@ -270,7 +296,7 @@ - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { - inf(@"Received memory warning."); + inf( @"Received memory warning." ); [super applicationDidReceiveMemoryWarning:application]; } @@ -307,7 +333,7 @@ - (void)applicationWillResignActive:(UIApplication *)application { - inf(@"Will deactivate"); + inf( @"Will deactivate" ); if (![[MPiOSConfig get].rememberLogin boolValue]) [self signOutAnimated:NO]; @@ -321,9 +347,8 @@ - (void)applicationDidBecomeActive:(UIApplication *)application { - inf(@"Re-activated"); - [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification - object:application userInfo:nil]; + inf( @"Re-activated" ); + [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:application]; #ifdef LOCALYTICS [[LocalyticsSession sharedLocalyticsSession] resume]; @@ -377,14 +402,14 @@ [[[PearlEMail alloc] initForEMailTo:@"Master Password Development " subject:PearlString( @"Feedback for Master Password [%@]", - [[PearlKeyChain deviceIdentifier] stringByDeletingMatchesOf:@"-.*"] ) + [[PearlKeyChain deviceIdentifier] stringByDeletingMatchesOf:@"-.*"] ) body:PearlString( @"\n\n\n" @"--\n" @"%@" @"Master Password %@, build %@", - userName? ([userName stringByAppendingString:@"\n"]): @"", - [PearlInfoPlist get].CFBundleShortVersionString, - [PearlInfoPlist get].CFBundleVersion ) + userName? ([userName stringByAppendingString:@"\n"]): @"", + [PearlInfoPlist get].CFBundleShortVersionString, + [PearlInfoPlist get].CFBundleVersion ) attachments:(logs ? [[PearlEMailAttachment alloc] @@ -392,8 +417,8 @@ dataUsingEncoding:NSUTF8StringEncoding] mimeType:@"text/plain" fileName:PearlString( @"%@-%@.log", - [[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]], - [PearlKeyChain deviceIdentifier] )] + [[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]], + [PearlKeyChain deviceIdentifier] )] : nil), nil] showComposerForVC:viewController]; } @@ -405,22 +430,22 @@ @"You can open the export with a text editor to get an overview of all your sites.\n\n" @"The file also acts as a personal backup of your site list in case you don't sync with iCloud/iTunes." tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { - [PearlAlert showAlertWithTitle:@"Reveal Passwords?" message: - @"Would you like to make all your passwords visible in the export?\n\n" - @"A safe export will only include your stored passwords, in an encrypted manner, " - @"making the result safe from falling in the wrong hands.\n\n" - @"If all your passwords are shown and somebody else finds the export, " - @"they could gain access to all your sites!" - viewStyle:UIAlertViewStyleDefault initAlert:nil - tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) { - if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 0) - // Safe Export - [self showExportRevealPasswords:NO forVC:viewController]; - if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1) - // Show Passwords - [self showExportRevealPasswords:YES forVC:viewController]; - } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil]; - } otherTitles:nil]; + [PearlAlert showAlertWithTitle:@"Reveal Passwords?" message: + @"Would you like to make all your passwords visible in the export?\n\n" + @"A safe export will only include your stored passwords, in an encrypted manner, " + @"making the result safe from falling in the wrong hands.\n\n" + @"If all your passwords are shown and somebody else finds the export, " + @"they could gain access to all your sites!" + viewStyle:UIAlertViewStyleDefault initAlert:nil + tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) { + if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 0) + // Safe Export + [self showExportRevealPasswords:NO forVC:viewController]; + if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1) + // Show Passwords + [self showExportRevealPasswords:YES forVC:viewController]; + } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil]; + } otherTitles:nil]; } - (void)showExportRevealPasswords:(BOOL)revealPasswords forVC:(UIViewController *)viewController { @@ -445,17 +470,17 @@ @"--\n" @"%@\n" @"Master Password %@, build %@", - [self activeUserForMainThread].name, - [PearlInfoPlist get].CFBundleShortVersionString, - [PearlInfoPlist get].CFBundleVersion ); + [self activeUserForMainThread].name, + [PearlInfoPlist get].CFBundleShortVersionString, + [PearlInfoPlist get].CFBundleVersion ); else message = PearlString( @"Backup of Master Password sites.\n\n\n" @"--\n" @"%@\n" @"Master Password %@, build %@", - [self activeUserForMainThread].name, - [PearlInfoPlist get].CFBundleShortVersionString, - [PearlInfoPlist get].CFBundleVersion ); + [self activeUserForMainThread].name, + [PearlInfoPlist get].CFBundleShortVersionString, + [PearlInfoPlist get].CFBundleVersion ); NSDateFormatter *exportDateFormatter = [NSDateFormatter new]; [exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"]; @@ -464,11 +489,11 @@ attachments:[[PearlEMailAttachment alloc] initWithContent:[exportedSites dataUsingEncoding:NSUTF8StringEncoding] mimeType:@"text/plain" fileName: PearlString( @"%@ (%@).mpsites", [self activeUserForMainThread].name, - [exportDateFormatter stringFromDate:[NSDate date]] )], + [exportDateFormatter stringFromDate:[NSDate date]] )], nil]; } -- (void)changeMasterPasswordFor:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc didResetBlock:(void (^)(void))didReset { +- (void)changeMasterPasswordFor:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc didResetBlock:(void ( ^ )(void))didReset { [PearlAlert showAlertWithTitle:@"Changing Master Password" message: @@ -481,7 +506,7 @@ return; [moc performBlockAndWait:^{ - inf(@"Unsetting master password for: %@.", user.userID); + inf( @"Unsetting master password for: %@.", user.userID ); user.keyID = nil; [self forgetSavedKeyFor:user]; [moc saveToStore]; @@ -497,16 +522,14 @@ otherTitles:[PearlStrings get].commonButtonContinue, nil]; } - #pragma mark - PearlConfigDelegate - (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)value { - [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification - object:NSStringFromSelector( configKey ) userInfo:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey )]; } -- (void)checkConfig { +- (void)updateFromConfig { // iCloud enabled / disabled BOOL iCloudEnabled = [[MPiOSConfig get].iCloudEnabled boolValue]; @@ -519,7 +542,7 @@ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )]; NSError *error = nil; if ((siteCount = [context countForFetchRequest:fetchRequest error:&error]) == NSNotFound) { - wrn(@"Couldn't count current sites: %@", error); + wrn( @"Couldn't count current sites: %@", error ); return; } }]; @@ -536,11 +559,11 @@ @"or overwrite them with your current sites." viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { - if (buttonIndex == [alert cancelButtonIndex]) - setConfirmationAnswer( NO ); - if (buttonIndex == [alert firstOtherButtonIndex]) - setConfirmationAnswer( YES ); - } + if (buttonIndex == [alert cancelButtonIndex]) + setConfirmationAnswer( NO ); + if (buttonIndex == [alert firstOtherButtonIndex]) + setConfirmationAnswer( YES ); + } cancelTitle:@"Use Old" otherTitles:@"Overwrite", nil]; }]; else @@ -550,7 +573,7 @@ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )]; NSError *error = nil; if ((siteCount = [context countForFetchRequest:fetchRequest error:&error]) == NSNotFound) { - wrn(@"Couldn't count current sites: %@", error); + wrn( @"Couldn't count current sites: %@", error ); return; } }]; @@ -566,11 +589,11 @@ @"or overwrite them with your current iCloud sites." viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { - if (buttonIndex == [alert cancelButtonIndex]) - setConfirmationAnswer( NO ); - if (buttonIndex == [alert firstOtherButtonIndex]) - setConfirmationAnswer( YES ); - } + if (buttonIndex == [alert cancelButtonIndex]) + setConfirmationAnswer( NO ); + if (buttonIndex == [alert firstOtherButtonIndex]) + setConfirmationAnswer( YES ); + } cancelTitle:@"Use Old" otherTitles:@"Overwrite", nil]; }]; } @@ -630,15 +653,14 @@ @"helpHidden" : @([[MPiOSConfig get].helpHidden boolValue]), @"showQuickStart" : @([[MPiOSConfig get].showSetup boolValue]), @"firstRun" : @([[PearlConfig get].firstRun boolValue]), - @"launchCount" : NilToNSNull([PearlConfig get].launchCount), + @"launchCount" : NilToNSNull( [PearlConfig get].launchCount ), @"askForReviews" : @([[PearlConfig get].askForReviews boolValue]), - @"reviewAfterLaunches" : NilToNSNull([PearlConfig get].reviewAfterLaunches), - @"reviewedVersion" : NilToNSNull([PearlConfig get].reviewedVersion) + @"reviewAfterLaunches" : NilToNSNull( [PearlConfig get].reviewAfterLaunches ), + @"reviewedVersion" : NilToNSNull( [PearlConfig get].reviewedVersion ) } ); } } - #pragma mark - UbiquityStoreManager - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore { @@ -690,13 +712,13 @@ @"To add one, go into Apple's Settings -> iCloud." viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { - if (buttonIndex == alert.firstOtherButtonIndex) { - [MPiOSConfig get].iCloudEnabled = @NO; - return; - } + if (buttonIndex == alert.firstOtherButtonIndex) { + [MPiOSConfig get].iCloudEnabled = @NO; + return; + } - [self.storeManager reloadStore]; - } cancelTitle:@"Try Again" otherTitles:@"Disable iCloud", nil]; + [self.storeManager reloadStore]; + } cancelTitle:@"Try Again" otherTitles:@"Disable iCloud", nil]; return YES; } @@ -710,30 +732,29 @@ message:@"Waiting for your other device to auto‑correct the problem..." viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock: ^(UIAlertView *alert, NSInteger buttonIndex) { - if (buttonIndex == [alert firstOtherButtonIndex]) - wSelf.fixCloudContentAlert = [PearlAlert showAlertWithTitle:@"Fix iCloud Now" message: - @"This problem can be auto‑corrected by opening the app on another device where you recently made changes.\n" - @"You can fix the problem from this device anyway, but recent changes from another device might get lost.\n\n" - @"You can also turn iCloud off for now." - viewStyle:UIAlertViewStyleDefault - initAlert:nil tappedButtonBlock: - ^(UIAlertView *alert_, NSInteger buttonIndex_) { - if (buttonIndex_ == alert_.cancelButtonIndex) - [wSelf showCloudContentAlert]; - if (buttonIndex_ == [alert_ firstOtherButtonIndex]) - [wSelf.storeManager rebuildCloudContentFromCloudStoreOrLocalStore:YES]; - if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1) - [MPiOSConfig get].iCloudEnabled = @NO; - } - cancelTitle:[PearlStrings get].commonButtonBack - otherTitles:@"Fix Anyway", - @"Turn Off", nil]; - if (buttonIndex == [alert firstOtherButtonIndex] + 1) - [MPiOSConfig get].iCloudEnabled = @NO; - } cancelTitle:nil otherTitles:@"Fix Now", @"Turn Off", nil]; + if (buttonIndex == [alert firstOtherButtonIndex]) + wSelf.fixCloudContentAlert = [PearlAlert showAlertWithTitle:@"Fix iCloud Now" message: + @"This problem can be auto‑corrected by opening the app on another device where you recently made changes.\n" + @"You can fix the problem from this device anyway, but recent changes from another device might get lost.\n\n" + @"You can also turn iCloud off for now." + viewStyle:UIAlertViewStyleDefault + initAlert:nil tappedButtonBlock: + ^(UIAlertView *alert_, NSInteger buttonIndex_) { + if (buttonIndex_ == alert_.cancelButtonIndex) + [wSelf showCloudContentAlert]; + if (buttonIndex_ == [alert_ firstOtherButtonIndex]) + [wSelf.storeManager rebuildCloudContentFromCloudStoreOrLocalStore:YES]; + if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1) + [MPiOSConfig get].iCloudEnabled = @NO; + } + cancelTitle:[PearlStrings get].commonButtonBack + otherTitles:@"Fix Anyway", + @"Turn Off", nil]; + if (buttonIndex == [alert firstOtherButtonIndex] + 1) + [MPiOSConfig get].iCloudEnabled = @NO; + } cancelTitle:nil otherTitles:@"Fix Now", @"Turn Off", nil]; } - #pragma mark - TestFlight - (NSDictionary *)testFlightInfo { @@ -748,14 +769,13 @@ - (NSString *)testFlightToken { - NSString *testFlightToken = NSNullToNil([[self testFlightInfo] valueForKeyPath:@"Application Token"]); + NSString *testFlightToken = NSNullToNil( [[self testFlightInfo] valueForKeyPath:@"Application Token"] ); if (![testFlightToken length]) - wrn(@"TestFlight token not set. Test Flight won't be aware of this test."); + wrn( @"TestFlight token not set. Test Flight won't be aware of this test." ); return testFlightToken; } - #pragma mark - Crashlytics - (NSDictionary *)crashlyticsInfo { @@ -770,14 +790,13 @@ - (NSString *)crashlyticsAPIKey { - NSString *crashlyticsAPIKey = NSNullToNil([[self crashlyticsInfo] valueForKeyPath:@"API Key"]); + NSString *crashlyticsAPIKey = NSNullToNil( [[self crashlyticsInfo] valueForKeyPath:@"API Key"] ); if (![crashlyticsAPIKey length]) - wrn(@"Crashlytics API key not set. Crash logs won't be recorded."); + wrn( @"Crashlytics API key not set. Crash logs won't be recorded." ); return crashlyticsAPIKey; } - #pragma mark - Localytics - (NSDictionary *)localyticsInfo { @@ -793,12 +812,12 @@ - (NSString *)localyticsKey { #ifdef DEBUG - NSString *localyticsKey = NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.development"]); + NSString *localyticsKey = NSNullToNil( [[self localyticsInfo] valueForKeyPath:@"Key.development"] ); #else NSString *localyticsKey = NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution"]); #endif if (![localyticsKey length]) - wrn(@"Localytics key not set. Demographics won't be collected."); + wrn( @"Localytics key not set. Demographics won't be collected." ); return localyticsKey; } diff --git a/MasterPassword/ObjC/iOS/MPiOSConfig.h b/MasterPassword/ObjC/iOS/MPiOSConfig.h index cbb0b2b4..434d30b7 100644 --- a/MasterPassword/ObjC/iOS/MPiOSConfig.h +++ b/MasterPassword/ObjC/iOS/MPiOSConfig.h @@ -18,5 +18,6 @@ @property(nonatomic, retain) NSNumber *loginNameTipShown; @property(nonatomic, retain) NSNumber *traceMode; @property(nonatomic, retain) NSNumber *iCloudEnabled; +@property(nonatomic, retain) NSNumber *dictationSearch; @end diff --git a/MasterPassword/ObjC/iOS/MPiOSConfig.m b/MasterPassword/ObjC/iOS/MPiOSConfig.m index 7234b221..82ca660b 100644 --- a/MasterPassword/ObjC/iOS/MPiOSConfig.m +++ b/MasterPassword/ObjC/iOS/MPiOSConfig.m @@ -8,7 +8,7 @@ @implementation MPiOSConfig -@dynamic helpHidden, siteInfoHidden, showSetup, actionsTipShown, typeTipShown, loginNameTipShown, traceMode, iCloudEnabled; +@dynamic helpHidden, siteInfoHidden, showSetup, actionsTipShown, typeTipShown, loginNameTipShown, traceMode, iCloudEnabled, dictationSearch; - (id)init { @@ -24,7 +24,8 @@ NSStringFromSelector( @selector(typeTipShown) ) : @(!self.firstRun), NSStringFromSelector( @selector(loginNameTipShown) ) : @NO, NSStringFromSelector( @selector(traceMode) ) : @NO, - NSStringFromSelector( @selector(iCloudEnabled) ) : @NO + NSStringFromSelector( @selector(iCloudEnabled) ) : @NO, + NSStringFromSelector( @selector( dictationSearch) ) : @NO }]; return self; diff --git a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj index b9717f3a..d2748e10 100644 --- a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj +++ b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ 93D39CB5E2EC1078E898F46A /* MPPasswordLargeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3937863061C3916AF7AD2 /* MPPasswordLargeCell.m */; }; 93D39D596A2E376D6F6F5DA1 /* MPCombinedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393310223DDB35218467A /* MPCombinedViewController.m */; }; 93D39E281E3658B30550CB55 /* NSDictionary+Indexing.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */; }; + 93D39EAA4D064193074D3021 /* MPFixable.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39A813CA9D7E192261ED2 /* MPFixable.m */; }; 93D39EDD960C381D64E4DCDD /* MPPasswordSmallCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3952CC60991B97D69F26A /* MPPasswordSmallCell.m */; }; 93D39F8A9254177891F38705 /* MPSetupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39A28369954D147E239BA /* MPSetupViewController.m */; }; 93D39FA97F4C3F69A75D5A03 /* MPPasswordLargeGeneratedCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3993422E207BF0B21D089 /* MPPasswordLargeGeneratedCell.m */; }; @@ -556,10 +557,12 @@ 93D39975CE5AEC99E3F086C7 /* MPPasswordCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordCell.h; sourceTree = ""; }; 93D3999693660C89A7465F4E /* MPCoachmarkViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPCoachmarkViewController.h; sourceTree = ""; }; 93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPUsersViewController.m; sourceTree = ""; }; + 93D399F244BB522A317811BB /* MPFixable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPFixable.h; sourceTree = ""; }; 93D39A1DDFA09AE2E14D26DC /* UIResponder+PearlFirstResponder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIResponder+PearlFirstResponder.m"; sourceTree = ""; }; 93D39A28369954D147E239BA /* MPSetupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPSetupViewController.m; sourceTree = ""; }; 93D39A3CC4D8330831FC8CB4 /* LLToggleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLToggleViewController.h; sourceTree = ""; }; 93D39A41340CF778E00D0E6D /* MPEmergencySegue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPEmergencySegue.h; sourceTree = ""; }; + 93D39A813CA9D7E192261ED2 /* MPFixable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPFixable.m; sourceTree = ""; }; 93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Indexing.m"; sourceTree = ""; }; 93D39ACBA9F4878B6A1CC33B /* MPEmergencyViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPEmergencyViewController.m; sourceTree = ""; }; 93D39B050DD5F55E9794EFD4 /* MPPopdownSegue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPopdownSegue.m; sourceTree = ""; }; @@ -2496,6 +2499,8 @@ DABD3BB91711E2DC00CF925C /* MPUserEntity.h */, DABD3BBA1711E2DC00CF925C /* MPUserEntity.m */, DABD3BD01711E2DC00CF925C /* MasterPassword.xcdatamodeld */, + 93D399F244BB522A317811BB /* MPFixable.h */, + 93D39A813CA9D7E192261ED2 /* MPFixable.m */, ); name = ObjC; path = ..; @@ -3840,6 +3845,7 @@ 93D39BA1EA3CAAC8A220B4A6 /* MPAppSettingsViewController.m in Sources */, 93D396D8B67DA6522CDBA142 /* MPCoachmarkViewController.m in Sources */, 93D391C07818F4C2DC1B6956 /* MPPasswordsCoachmarkViewController.m in Sources */, + 93D39EAA4D064193074D3021 /* MPFixable.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MasterPassword/ObjC/iOS/Settings.bundle/Root.plist b/MasterPassword/ObjC/iOS/Settings.bundle/Root.plist index e3ffcd59..30099a7c 100644 --- a/MasterPassword/ObjC/iOS/Settings.bundle/Root.plist +++ b/MasterPassword/ObjC/iOS/Settings.bundle/Root.plist @@ -70,6 +70,24 @@ Type PSToggleSwitchSpecifier + + FooterText + Enabling support for dictation in the site search box will enable the dictation button next to the space bar at the bottom of the keyboard. Press this button and speak the name of your site to look it up. Enabling dictation will change your keyboard which might make it slightly more difficult to enter a site name manually. + Title + + Type + PSGroupSpecifier + + + DefaultValue + + Key + dictationSearch + Title + Dictation Search + Type + PSToggleSwitchSpecifier + Type PSGroupSpecifier @@ -88,6 +106,24 @@ DefaultValue + + Type + PSGroupSpecifier + Title + + FooterText + If the app tends to crash on login, enable this to check if there are any inconsistencies in your site data. It may slow down login a bit, so keep it off when no issues are reported on login. + + + Type + PSToggleSwitchSpecifier + Title + Check For Inconsistencies + Key + checkInconsistency + DefaultValue + + StringsTable Root diff --git a/MasterPassword/ObjC/iOS/Storyboard.storyboard b/MasterPassword/ObjC/iOS/Storyboard.storyboard index 69e4ab62..ecf9baaf 100644 --- a/MasterPassword/ObjC/iOS/Storyboard.storyboard +++ b/MasterPassword/ObjC/iOS/Storyboard.storyboard @@ -714,8 +714,42 @@ + + + + + + + + + + + + + + + + + + + + + + - + @@ -762,6 +796,7 @@ +