2
0

Fixed critical issue with storing passwords & inconsistency recovery + dictation support.

[FIXED]     Fixed an issue that caused stored passwords to be saved without encryption.
[ADDED]     Logic to check for any data inconsistencies and fix them.
[ADDED]     Support for using dictation in site search box.
This commit is contained in:
Maarten Billemont 2014-04-26 14:03:44 -04:00
parent fc82790b8c
commit 9fee4a2bbe
30 changed files with 624 additions and 312 deletions

View File

@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>Crashlytics</string>
<key>CFBundleIdentifier</key>
<string>com.crashlytics.sdk.mac</string>
<string>com.crashlytics.ios</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
@ -15,16 +15,16 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.1.2</string>
<string>2.1.7</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>macosx</string>
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>9</string>
<string>26</string>
<key>DTPlatformName</key>
<string>macosx</string>
<string>iphoneos</string>
<key>MinimumOSVersion</key>
<string>10.6</string>
<string>4.0</string>
</dict>
</plist>

Binary file not shown.

View File

@ -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;

View File

@ -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 <lhunath@lyndir.com>
* @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 <lhunath@lyndir.com>
* @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;
}
@ -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)],
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]
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<MPAlgorithm> 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;
}

View File

@ -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;
@ -99,7 +99,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
// 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 );
@ -168,7 +174,8 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
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) {

View File

@ -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;

View File

@ -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)
@ -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<MPFixable> *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];
@ -166,14 +203,14 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
// 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 {
@ -183,22 +220,22 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
// 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)
} );
@ -377,23 +414,29 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
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<MPAlgorithm> 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];

View File

@ -14,5 +14,6 @@
@property(nonatomic, retain) NSNumber *rememberLogin;
@property(nonatomic, retain) NSNumber *iCloudDecided;
@property(nonatomic, retain) NSNumber *checkInconsistency;
@end

View File

@ -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];

View File

@ -8,10 +8,11 @@
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "MPFixable.h"
@class MPUserEntity;
@interface MPElementEntity : NSManagedObject
@interface MPElementEntity : NSManagedObject <MPFixable>
@property(nonatomic, retain) NSDate *lastUsed;
@property(nonatomic, retain) NSString *loginName;

View File

@ -19,4 +19,9 @@
@dynamic version_;
@dynamic user;
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context {
return MPFixableResultNoProblems;
}
@end

View File

@ -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

View File

@ -12,6 +12,6 @@
@interface MPElementStoredEntity : MPElementEntity
@property(nonatomic, retain) id contentObject;
@property(nonatomic, retain) NSData *contentObject;
@end

View File

@ -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

View File

@ -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 );
}

View File

@ -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 <lhunath@lyndir.com>
* @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 <Foundation/Foundation.h>
typedef NS_ENUM( NSUInteger, MPFixableResult ) {
MPFixableResultNoProblems,
MPFixableResultProblemsFixed,
MPFixableResultProblemsNotFixed,
};
MPFixableResult MPApplyFix(MPFixableResult previousResult, MPFixableResult(^fixBlock)(void));
@protocol MPFixable<NSObject>
- (MPFixableResult)findAndFixInconsistenciesInContext:(NSManagedObjectContext *)context;
@end

View File

@ -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 <lhunath@lyndir.com>
* @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 );
}

View File

@ -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) {

View File

@ -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

View File

@ -44,10 +44,8 @@
element = [super saveContentTypeWithElement:element saveInContext:context];
MPElementStoredEntity *storedElement = [self storedElement:element];
if (storedElement) {
storedElement.contentObject = self.contentField.text;
[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( ^{

View File

@ -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;

View File

@ -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;

View File

@ -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];
}
@ -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 );
}
}
}

View File

@ -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];
[self updateFromConfig];
}];
[[NSNotificationCenter defaultCenter]
addObserverForName:kIASKAppSettingChanged object:nil queue:nil usingBlock:^(NSNotification *note) {
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification
object:note userInfo:nil];
[[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;
@ -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];
@ -468,7 +493,7 @@
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;
}
}];
@ -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;
}
}];
@ -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 {
@ -733,7 +755,6 @@
} 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;
}

View File

@ -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

View File

@ -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;

View File

@ -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 = "<group>"; };
93D3999693660C89A7465F4E /* MPCoachmarkViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPCoachmarkViewController.h; sourceTree = "<group>"; };
93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPUsersViewController.m; sourceTree = "<group>"; };
93D399F244BB522A317811BB /* MPFixable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPFixable.h; sourceTree = "<group>"; };
93D39A1DDFA09AE2E14D26DC /* UIResponder+PearlFirstResponder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIResponder+PearlFirstResponder.m"; sourceTree = "<group>"; };
93D39A28369954D147E239BA /* MPSetupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPSetupViewController.m; sourceTree = "<group>"; };
93D39A3CC4D8330831FC8CB4 /* LLToggleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLToggleViewController.h; sourceTree = "<group>"; };
93D39A41340CF778E00D0E6D /* MPEmergencySegue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPEmergencySegue.h; sourceTree = "<group>"; };
93D39A813CA9D7E192261ED2 /* MPFixable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPFixable.m; sourceTree = "<group>"; };
93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Indexing.m"; sourceTree = "<group>"; };
93D39ACBA9F4878B6A1CC33B /* MPEmergencyViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPEmergencyViewController.m; sourceTree = "<group>"; };
93D39B050DD5F55E9794EFD4 /* MPPopdownSegue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPopdownSegue.m; sourceTree = "<group>"; };
@ -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;
};

View File

@ -70,6 +70,24 @@
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>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.</string>
<key>Title</key>
<string></string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>DefaultValue</key>
<false/>
<key>Key</key>
<string>dictationSearch</string>
<key>Title</key>
<string>Dictation Search</string>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
@ -88,6 +106,24 @@
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string></string>
<key>FooterText</key>
<string>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.</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Check For Inconsistencies</string>
<key>Key</key>
<string>checkInconsistency</string>
<key>DefaultValue</key>
<false/>
</dict>
</array>
<key>StringsTable</key>
<string>Root</string>

View File

@ -714,8 +714,42 @@
</tableViewCellContentView>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="110" id="UdB-BV-AHA" userLabel="Check Inconsistencies">
<rect key="frame" x="0.0" y="860" width="320" height="110"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="UdB-BV-AHA" id="V2Y-nu-jhZ">
<rect key="frame" x="0.0" y="0.0" width="287" height="109"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Check For Inconsistencies" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WXh-sg-l2h">
<rect key="frame" x="20" y="20" width="247" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="Exo2.0-Bold" family="Exo 2.0" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Perform a check to see if there are any inconsistencies in your site data that might cause issues." lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="247" translatesAutoresizingMaskIntoConstraints="NO" id="gTs-JA-zmL">
<rect key="frame" x="20" y="49" width="247" height="40"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="Exo2.0-Thin" family="Exo 2.0" pointSize="11"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="gTs-JA-zmL" secondAttribute="trailing" constant="20" symbolic="YES" id="E6T-PX-DeO"/>
<constraint firstItem="gTs-JA-zmL" firstAttribute="leading" secondItem="V2Y-nu-jhZ" secondAttribute="leading" constant="20" symbolic="YES" id="KNx-Ww-XeE"/>
<constraint firstAttribute="trailing" secondItem="WXh-sg-l2h" secondAttribute="trailing" constant="20" symbolic="YES" id="MTI-sn-fnK"/>
<constraint firstItem="WXh-sg-l2h" firstAttribute="leading" secondItem="V2Y-nu-jhZ" secondAttribute="leading" constant="20" symbolic="YES" id="cXs-lo-6cy"/>
<constraint firstItem="gTs-JA-zmL" firstAttribute="top" secondItem="WXh-sg-l2h" secondAttribute="bottom" constant="8" symbolic="YES" id="jA7-sy-qAs"/>
<constraint firstItem="WXh-sg-l2h" firstAttribute="top" secondItem="V2Y-nu-jhZ" secondAttribute="top" constant="20" symbolic="YES" id="tOD-LM-bbp"/>
<constraint firstAttribute="bottom" secondItem="gTs-JA-zmL" secondAttribute="bottom" constant="20" symbolic="YES" id="trB-mg-HbI"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="125" id="IVT-Rs-nTu" userLabel="Export">
<rect key="frame" x="0.0" y="860" width="320" height="125"/>
<rect key="frame" x="0.0" y="970" width="320" height="125"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="IVT-Rs-nTu" id="Q5J-2f-mmz">
<rect key="frame" x="0.0" y="0.0" width="287" height="124"/>
@ -762,6 +796,7 @@
<size key="freeformSize" width="320" height="568"/>
<connections>
<outlet property="avatarImage" destination="tWi-sc-DGp" id="ifT-Ct-WL6"/>
<outlet property="checkInconsistencies" destination="UdB-BV-AHA" id="Cm2-Om-UzP"/>
<outlet property="coachmarksCell" destination="eth-Dc-JYn" id="0Tq-I3-SwK"/>
<outlet property="exportCell" destination="IVT-Rs-nTu" id="RU0-qr-Bdi"/>
<outlet property="feedbackCell" destination="9QG-lM-ymM" id="18X-Ph-0ac"/>