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

Binary file not shown.

View File

@ -37,6 +37,8 @@
- (NSString *)shortNameOfType:(MPElementType)type; - (NSString *)shortNameOfType:(MPElementType)type;
- (NSString *)classNameOfType:(MPElementType)type; - (NSString *)classNameOfType:(MPElementType)type;
- (Class)classOfType:(MPElementType)type; - (Class)classOfType:(MPElementType)type;
- (NSArray *)allTypes;
- (NSArray *)allTypesStartingWith:(MPElementType)startingType;
- (MPElementType)nextType:(MPElementType)type; - (MPElementType)nextType:(MPElementType)type;
- (MPElementType)previousType:(MPElementType)type; - (MPElementType)previousType:(MPElementType)type;

View File

@ -1,12 +1,12 @@
/** /**
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
* *
* See the enclosed file LICENSE for license information (LGPLv3). If you did * 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 * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
* *
* @author Maarten Billemont <lhunath@lyndir.com> * @author Maarten Billemont <lhunath@lyndir.com>
* @license http://www.gnu.org/licenses/lgpl-3.0.txt * @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/ */
// //
// MPAlgorithmV0 // MPAlgorithmV0
@ -53,7 +53,7 @@
migrationRequest.predicate = [NSPredicate predicateWithFormat:@"version_ < %d AND user == %@", self.version, user]; migrationRequest.predicate = [NSPredicate predicateWithFormat:@"version_ < %d AND user == %@", self.version, user];
NSArray *migrationElements = [moc executeFetchRequest:migrationRequest error:&error]; NSArray *migrationElements = [moc executeFetchRequest:migrationRequest error:&error];
if (!migrationElements) { if (!migrationElements) {
err(@"While looking for elements to migrate: %@", error); err( @"While looking for elements to migrate: %@", error );
return NO; return NO;
} }
@ -68,7 +68,7 @@
- (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit { - (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit {
if (element.version != [self version] - 1) if (element.version != [self version] - 1)
// Only migrate from previous version. // Only migrate from previous version.
return NO; return NO;
if (!explicit) { if (!explicit) {
@ -85,18 +85,19 @@
- (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName { - (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName {
uint32_t nuserNameLength = htonl(userName.length); uint32_t nuserNameLength = htonl( userName.length );
NSDate *start = [NSDate date]; NSDate *start = [NSDate date];
NSData *keyData = [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding] NSData *keyData = [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding]
usingSalt:[NSData dataByConcatenatingDatas: usingSalt:[NSData dataByConcatenatingDatas:
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding], [@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
[NSData dataWithBytes:&nuserNameLength [NSData dataWithBytes:&nuserNameLength
length:sizeof(nuserNameLength)], length:sizeof( nuserNameLength )],
[userName dataUsingEncoding:NSUTF8StringEncoding], [userName dataUsingEncoding:NSUTF8StringEncoding],
nil] N:MP_N r:MP_r p:MP_p]; nil] N:MP_N r:MP_r p:MP_p];
MPKey *key = [self keyFromKeyData:keyData]; 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; return key;
} }
@ -142,7 +143,7 @@
return @"Device Private Password"; return @"Device Private Password";
} }
Throw(@"Type not supported: %lu", (long)type); Throw( @"Type not supported: %lu", (long)type );
} }
- (NSString *)shortNameOfType:(MPElementType)type { - (NSString *)shortNameOfType:(MPElementType)type {
@ -176,7 +177,7 @@
return @"Device"; return @"Device";
} }
Throw(@"Type not supported: %lu", (long)type); Throw( @"Type not supported: %lu", (long)type );
} }
- (NSString *)classNameOfType:(MPElementType)type { - (NSString *)classNameOfType:(MPElementType)type {
@ -187,7 +188,7 @@
- (Class)classOfType:(MPElementType)type { - (Class)classOfType:(MPElementType)type {
if (!type) if (!type)
Throw(@"No type given."); Throw( @"No type given." );
switch (type) { switch (type) {
case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedMaximum:
@ -215,14 +216,27 @@
return [MPElementStoredEntity class]; 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 { - (MPElementType)nextType:(MPElementType)type {
if (!type)
Throw(@"No type given.");
switch (type) { switch (type) {
case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedMaximum:
return MPElementTypeStoredDevicePrivate; return MPElementTypeStoredDevicePrivate;
@ -240,9 +254,9 @@
return MPElementTypeGeneratedPIN; return MPElementTypeGeneratedPIN;
case MPElementTypeStoredDevicePrivate: case MPElementTypeStoredDevicePrivate:
return MPElementTypeStoredPersonal; return MPElementTypeStoredPersonal;
default:
return MPElementTypeGeneratedLong;
} }
Throw(@"Type not supported: %lu", (long)type);
} }
- (MPElementType)previousType:(MPElementType)type { - (MPElementType)previousType:(MPElementType)type {
@ -262,38 +276,38 @@
[[NSBundle mainBundle] URLForResource:@"ciphers" withExtension:@"plist"]]; [[NSBundle mainBundle] URLForResource:@"ciphers" withExtension:@"plist"]];
// Determine the seed whose bytes will be used for calculating a password // Determine the seed whose bytes will be used for calculating a password
uint32_t ncounter = htonl(counter), nnameLength = htonl(name.length); uint32_t ncounter = htonl( counter ), nnameLength = htonl( name.length );
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof(ncounter)]; NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof( ncounter )];
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof(nnameLength)]; NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof( nnameLength )];
trc(@"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64], trc( @"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64],
[nameLengthBytes encodeHex], name, [counterBytes encodeHex]); [nameLengthBytes encodeHex], name, [counterBytes encodeHex] );
NSData *seed = [[NSData dataByConcatenatingDatas: NSData *seed = [[NSData dataByConcatenatingDatas:
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding], [@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
nameLengthBytes, [name dataUsingEncoding:NSUTF8StringEncoding], nameLengthBytes, [name dataUsingEncoding:NSUTF8StringEncoding],
counterBytes, nil] counterBytes, nil]
hmacWith:PearlHashSHA256 key:key.keyData]; hmacWith:PearlHashSHA256 key:key.keyData];
trc(@"seed is: %@", [seed encodeBase64]); trc( @"seed is: %@", [seed encodeBase64] );
const char *seedBytes = seed.bytes; const char *seedBytes = seed.bytes;
// Determine the cipher from the first seed byte. // Determine the cipher from the first seed byte.
NSAssert([seed length], @"Missing seed."); NSAssert( [seed length], @"Missing seed." );
NSString *typeClass = [self classNameOfType:type]; NSString *typeClass = [self classNameOfType:type];
NSString *typeName = [self nameOfType:type]; NSString *typeName = [self nameOfType:type];
id classCiphers = [MPTypes_ciphers valueForKey:typeClass]; id classCiphers = [MPTypes_ciphers valueForKey:typeClass];
NSArray *typeCiphers = [classCiphers valueForKey:typeName]; NSArray *typeCiphers = [classCiphers valueForKey:typeName];
NSString *cipher = typeCiphers[htons(seedBytes[0]) % [typeCiphers count]]; NSString *cipher = typeCiphers[htons( seedBytes[0] ) % [typeCiphers count]];
trc(@"type %@, ciphers: %@, selected: %@", typeName, typeCiphers, cipher); trc( @"type %@, ciphers: %@, selected: %@", typeName, typeCiphers, cipher );
// Encode the content, character by character, using subsequent seed bytes and the 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]]; NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
for (NSUInteger c = 0; c < [cipher length]; ++c) { 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 *cipherClass = [cipher substringWithRange:NSMakeRange( c, 1 )];
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass]; NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange( keyByte % [cipherClassCharacters length], 1 )]; 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]; [content appendString:character];
} }
@ -307,7 +321,7 @@
- (void)saveContent:(NSString *)clearContent toElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey { - (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) { switch (element.type) {
case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedMaximum:
case MPElementTypeGeneratedLong: case MPElementTypeGeneratedLong:
@ -315,13 +329,14 @@
case MPElementTypeGeneratedBasic: case MPElementTypeGeneratedBasic:
case MPElementTypeGeneratedShort: case MPElementTypeGeneratedShort:
case MPElementTypeGeneratedPIN: { 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; break;
} }
case MPElementTypeStoredPersonal: { case MPElementTypeStoredPersonal: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]], NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
[element class] );
NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding] NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding]
encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES]; encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
@ -329,8 +344,9 @@
break; break;
} }
case MPElementTypeStoredDevicePrivate: { case MPElementTypeStoredDevicePrivate: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]], NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
[element class] );
NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding] NSData *encryptedContent = [[clearContent dataUsingEncoding:NSUTF8StringEncoding]
encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES]; encryptWithSymmetricKey:[elementKey subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
@ -364,9 +380,9 @@
return result; 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) { switch (element.type) {
case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedMaximum:
case MPElementTypeGeneratedLong: case MPElementTypeGeneratedLong:
@ -374,17 +390,18 @@
case MPElementTypeGeneratedBasic: case MPElementTypeGeneratedBasic:
case MPElementTypeGeneratedShort: case MPElementTypeGeneratedShort:
case MPElementTypeGeneratedPIN: { case MPElementTypeGeneratedPIN: {
NSAssert([element isKindOfClass:[MPElementGeneratedEntity class]], NSAssert( [element isKindOfClass:[MPElementGeneratedEntity class]],
@"Element with generated type %lu is not an MPElementGeneratedEntity, but a %@.", (long)element.type, [element class]); @"Element with generated type %lu is not an MPElementGeneratedEntity, but a %@.", (long)element.type,
[element class] );
NSString *name = element.name; NSString *name = element.name;
MPElementType type = element.type; MPElementType type = element.type;
NSUInteger counter = ((MPElementGeneratedEntity *)element).counter; NSUInteger counter = ((MPElementGeneratedEntity *)element).counter;
id<MPAlgorithm> algorithm = nil; id<MPAlgorithm> algorithm = nil;
if (!element.name.length) if (!element.name.length)
err(@"Missing name."); err( @"Missing name." );
else if (!elementKey.keyData.length) else if (!elementKey.keyData.length)
err(@"Missing key."); err( @"Missing key." );
else else
algorithm = element.algorithm; algorithm = element.algorithm;
@ -396,8 +413,9 @@
} }
case MPElementTypeStoredPersonal: { case MPElementTypeStoredPersonal: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]], NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
[element class] );
NSData *encryptedContent = ((MPElementStoredEntity *)element).contentObject; NSData *encryptedContent = ((MPElementStoredEntity *)element).contentObject;
@ -408,8 +426,9 @@
break; break;
} }
case MPElementTypeStoredDevicePrivate: { case MPElementTypeStoredDevicePrivate: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]], NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
[element class] );
NSDictionary *elementQuery = [self queryForDevicePrivateElementNamed:element.name]; NSDictionary *elementQuery = [self queryForDevicePrivateElementNamed:element.name];
NSData *encryptedContent = [PearlKeyChain dataOfItemForQuery:elementQuery]; NSData *encryptedContent = [PearlKeyChain dataOfItemForQuery:elementQuery];
@ -426,7 +445,7 @@
- (void)importProtectedContent:(NSString *)protectedContent protectedByKey:(MPKey *)importKey - (void)importProtectedContent:(NSString *)protectedContent protectedByKey:(MPKey *)importKey
intoElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey { 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) { switch (element.type) {
case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedMaximum:
case MPElementTypeGeneratedLong: case MPElementTypeGeneratedLong:
@ -437,8 +456,9 @@
break; break;
case MPElementTypeStoredPersonal: { case MPElementTypeStoredPersonal: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]], NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
[element class] );
if ([importKey.keyID isEqualToData:elementKey.keyID]) if ([importKey.keyID isEqualToData:elementKey.keyID])
((MPElementStoredEntity *)element).contentObject = [protectedContent decodeBase64]; ((MPElementStoredEntity *)element).contentObject = [protectedContent decodeBase64];
@ -456,7 +476,7 @@
- (void)importClearTextContent:(NSString *)clearContent intoElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey { - (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) { switch (element.type) {
case MPElementTypeGeneratedMaximum: case MPElementTypeGeneratedMaximum:
case MPElementTypeGeneratedLong: case MPElementTypeGeneratedLong:
@ -478,7 +498,7 @@
- (NSString *)exportContentForElement:(MPElementEntity *)element usingKey:(MPKey *)elementKey { - (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)) if (!(element.type & MPElementFeatureExportContent))
return nil; return nil;
@ -495,8 +515,9 @@
} }
case MPElementTypeStoredPersonal: { case MPElementTypeStoredPersonal: {
NSAssert([element isKindOfClass:[MPElementStoredEntity class]], NSAssert( [element isKindOfClass:[MPElementStoredEntity class]],
@"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type, [element class]); @"Element with stored type %lu is not an MPElementStoredEntity, but a %@.", (long)element.type,
[element class] );
result = [((MPElementStoredEntity *)element).contentObject encodeBase64]; result = [((MPElementStoredEntity *)element).contentObject encodeBase64];
break; break;
} }

View File

@ -16,7 +16,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:@{ attributes:@{
(__bridge id)kSecAttrService : @"Saved Master Password", (__bridge id)kSecAttrService : @"Saved Master Password",
(__bridge id)kSecAttrAccount : IfNotNilElse(user.name, @"") (__bridge id)kSecAttrAccount : IfNotNilElse( user.name, @"" )
} }
matches:nil]; matches:nil];
} }
@ -25,11 +25,11 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )]; NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )];
if (!keyData) { if (!keyData) {
inf(@"No key found in keychain for: %@", user.userID); inf( @"No key found in keychain for: %@", user.userID );
return nil; return nil;
} }
inf(@"Found key in keychain for: %@", user.userID); inf( @"Found key in keychain for: %@", user.userID );
return [MPAlgorithmDefault keyFromKeyData:keyData]; return [MPAlgorithmDefault keyFromKeyData:keyData];
} }
@ -39,7 +39,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
NSData *existingKeyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )]; NSData *existingKeyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )];
if (![existingKeyData isEqualToData:self.key.keyData]) { 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 ) [PearlKeyChain addOrUpdateItemForQuery:keyQuery( user )
withAttributes:@{ withAttributes:@{
@ -56,7 +56,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery( user )]; OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery( user )];
if (result == noErr) { 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]; [[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 { - (BOOL)signInAsUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc usingMasterPassword:(NSString *)password {
if (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) if (!user)
return NO; 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. // Method 2: Depending on the user's saveKey, load or remove the key from the keychain.
if (!user.saveKey) if (!user.saveKey)
// Key should not be stored in keychain. Delete it. // Key should not be stored in keychain. Delete it.
[self forgetSavedKeyFor:user]; [self forgetSavedKeyFor:user];
else if (!tryKey) { else if (!tryKey) {
// Key should be saved in keychain. Load it. // Key should be saved in keychain. Load it.
if ((tryKey = [self loadSavedKeyFor:user]) && ![user.keyID isEqual:tryKey.keyID]) { if ((tryKey = [self loadSavedKeyFor:user]) && ![user.keyID isEqual:tryKey.keyID]) {
// Loaded password doesn't match user's keyID. Forget saved password: it is incorrect. // 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; tryKey = nil;
[self forgetSavedKeyFor:user]; [self forgetSavedKeyFor:user];
@ -110,7 +110,7 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
if (!tryKey) { if (!tryKey) {
if ([password length]) if ((tryKey = [MPAlgorithmDefault keyForPassword:password if ([password length]) if ((tryKey = [MPAlgorithmDefault keyForPassword:password
ofUserNamed:user.name])) if (![user.keyID isEqual:tryKey.keyID]) { 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; tryKey = nil;
} }
@ -119,13 +119,13 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
// No more methods left, fail if key still not known. // No more methods left, fail if key still not known.
if (!tryKey) { if (!tryKey) {
if (password) { if (password) {
inf(@"Login failed for: %@", user.userID); inf( @"Login failed for: %@", user.userID );
MPCheckpoint( MPCheckpointSignInFailed, nil ); MPCheckpoint( MPCheckpointSignInFailed, nil );
} }
return NO; return NO;
} }
inf(@"Logged in: %@", user.userID); inf( @"Logged in: %@", user.userID );
if (![self.key isEqualToKey:tryKey]) { if (![self.key isEqualToKey:tryKey]) {
self.key = tryKey; self.key = tryKey;
@ -147,13 +147,19 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
} }
} }
@catch (id exception) { @catch (id exception) {
err(@"While setting username: %@", exception); err( @"While setting username: %@", exception );
} }
user.lastUsed = [NSDate date]; user.lastUsed = [NSDate date];
[moc saveToStore]; [moc saveToStore];
self.activeUser = user; 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]; [[NSNotificationCenter defaultCenter] postNotificationName:MPSignedInNotification object:self];
MPCheckpoint( MPCheckpointSignedIn, nil ); MPCheckpoint( MPCheckpointSignedIn, nil );
@ -163,12 +169,13 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
- (void)migrateElementsForUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc toKey:(MPKey *)newKey { - (void)migrateElementsForUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc toKey:(MPKey *)newKey {
if (![user.elements count]) if (![user.elements count])
// Nothing to migrate. // Nothing to migrate.
return; return;
MPKey *recoverKey = newKey; MPKey *recoverKey = newKey;
#ifdef PEARL_UIKIT #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 #endif
for (MPElementEntity *element in user.elements) { for (MPElementEntity *element in user.elements) {
@ -183,12 +190,12 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
dispatch_group_enter( recoverPasswordGroup ); dispatch_group_enter( recoverPasswordGroup );
[PearlAlert showAlertWithTitle:@"Enter Old Master Password" [PearlAlert showAlertWithTitle:@"Enter Old Master Password"
message:PearlString( @"Your old master password is required to migrate the stored password for %@", message:PearlString( @"Your old master password is required to migrate the stored password for %@",
element.name ) element.name )
viewStyle:UIAlertViewStyleSecureTextInput viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) { initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
@try { @try {
if (buttonIndex_ == [alert_ cancelButtonIndex]) if (buttonIndex_ == [alert_ cancelButtonIndex])
// Don't Migrate // Don't Migrate
return; return;
masterPassword = [alert_ textFieldAtIndex:0].text; masterPassword = [alert_ textFieldAtIndex:0].text;
@ -200,14 +207,14 @@ static NSDictionary *keyQuery(MPUserEntity *user) {
dispatch_group_wait( recoverPasswordGroup, DISPATCH_TIME_FOREVER ); dispatch_group_wait( recoverPasswordGroup, DISPATCH_TIME_FOREVER );
#endif #endif
if (!masterPassword) if (!masterPassword)
// Don't Migrate // Don't Migrate
break; break;
recoverKey = [element.algorithm keyForPassword:masterPassword ofUserNamed:user.name]; recoverKey = [element.algorithm keyForPassword:masterPassword ofUserNamed:user.name];
} }
if (!content) if (!content)
// Don't Migrate // Don't Migrate
break; break;
if (![recoverKey isEqualToKey:newKey]) if (![recoverKey isEqualToKey:newKey])

View File

@ -9,6 +9,7 @@
#import "MPAppDelegate_Shared.h" #import "MPAppDelegate_Shared.h"
#import "UbiquityStoreManager.h" #import "UbiquityStoreManager.h"
#import "MPFixable.h"
typedef enum { typedef enum {
MPImportResultSuccess, MPImportResultSuccess,
@ -27,6 +28,7 @@ typedef enum {
+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock; + (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock;
- (UbiquityStoreManager *)storeManager; - (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. */ /** @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; - (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion;

View File

@ -18,13 +18,13 @@
#define MPMigrationLevelLocalStoreKey @"MPMigrationLevelLocalStoreKey" #define MPMigrationLevelLocalStoreKey @"MPMigrationLevelLocalStoreKey"
#define MPMigrationLevelCloudStoreKey @"MPMigrationLevelCloudStoreKey" #define MPMigrationLevelCloudStoreKey @"MPMigrationLevelCloudStoreKey"
typedef NS_ENUM(NSInteger, MPMigrationLevelLocalStore) { typedef NS_ENUM( NSInteger, MPMigrationLevelLocalStore ) {
MPMigrationLevelLocalStoreV1, MPMigrationLevelLocalStoreV1,
MPMigrationLevelLocalStoreV2, MPMigrationLevelLocalStoreV2,
MPMigrationLevelLocalStoreCurrent = MPMigrationLevelLocalStoreV2, MPMigrationLevelLocalStoreCurrent = MPMigrationLevelLocalStoreV2,
}; };
typedef NS_ENUM(NSInteger, MPMigrationLevelCloudStore) { typedef NS_ENUM( NSInteger, MPMigrationLevelCloudStore ) {
MPMigrationLevelCloudStoreV1, MPMigrationLevelCloudStoreV1,
MPMigrationLevelCloudStoreV2, MPMigrationLevelCloudStoreV2,
MPMigrationLevelCloudStoreV3, MPMigrationLevelCloudStoreV3,
@ -32,16 +32,18 @@ typedef NS_ENUM(NSInteger, MPMigrationLevelCloudStore) {
}; };
@implementation MPAppDelegate_Shared(Store) @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 #pragma mark - Core Data setup
+ (NSManagedObjectContext *)managedObjectContextForMainThreadIfReady { + (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]; NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
if (!mainManagedObjectContext || ![[NSThread currentThread] isMainThread]) if (!mainManagedObjectContext || ![[NSThread currentThread] isMainThread])
return nil; return nil;
@ -49,7 +51,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
return mainManagedObjectContext; return mainManagedObjectContext;
} }
+ (BOOL)managedObjectContextForMainThreadPerformBlock:(void (^)(NSManagedObjectContext *mainContext))mocBlock { + (BOOL)managedObjectContextForMainThreadPerformBlock:(void ( ^ )(NSManagedObjectContext *mainContext))mocBlock {
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady]; NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
if (!mainManagedObjectContext) if (!mainManagedObjectContext)
@ -62,7 +64,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
return YES; return YES;
} }
+ (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void (^)(NSManagedObjectContext *mainContext))mocBlock { + (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void ( ^ )(NSManagedObjectContext *mainContext))mocBlock {
NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady]; NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady];
if (!mainManagedObjectContext) if (!mainManagedObjectContext)
@ -75,7 +77,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
return YES; return YES;
} }
+ (BOOL)managedObjectContextPerformBlock:(void (^)(NSManagedObjectContext *context))mocBlock { + (BOOL)managedObjectContextPerformBlock:(void ( ^ )(NSManagedObjectContext *context))mocBlock {
NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady]; NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady];
if (!privateManagedObjectContextIfReady) if (!privateManagedObjectContextIfReady)
@ -90,7 +92,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
return YES; return YES;
} }
+ (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock { + (BOOL)managedObjectContextPerformBlockAndWait:(void ( ^ )(NSManagedObjectContext *context))mocBlock {
NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady]; NSManagedObjectContext *privateManagedObjectContextIfReady = [[self get] privateManagedObjectContextIfReady];
if (!privateManagedObjectContextIfReady) if (!privateManagedObjectContextIfReady)
@ -132,14 +134,14 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification object:UIApp [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification object:UIApp
queue:[NSOperationQueue mainQueue] usingBlock: queue:[NSOperationQueue mainQueue] usingBlock:
^(NSNotification *note) { ^(NSNotification *note) {
[[self mainManagedObjectContext] saveToStore]; [[self mainManagedObjectContext] saveToStore];
}]; }];
[[NSNotificationCenter defaultCenter] [[NSNotificationCenter defaultCenter]
addObserverForName:UIApplicationWillResignActiveNotification object:UIApp addObserverForName:UIApplicationWillResignActiveNotification object:UIApp
queue:[NSOperationQueue mainQueue] usingBlock: queue:[NSOperationQueue mainQueue] usingBlock:
^(NSNotification *note) { ^(NSNotification *note) {
[[self mainManagedObjectContext] saveToStore]; [[self mainManagedObjectContext] saveToStore];
}]; }];
#else #else
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification object:NSApp [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification object:NSApp
queue:[NSOperationQueue mainQueue] usingBlock: queue:[NSOperationQueue mainQueue] usingBlock:
@ -151,6 +153,41 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
return storeManager; 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 { - (void)migrateStoreForManager:(UbiquityStoreManager *)manager isCloud:(BOOL)isCloudStore {
[self migrateLocalStore]; [self migrateLocalStore];
@ -163,42 +200,42 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
MPMigrationLevelLocalStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelLocalStoreKey]; MPMigrationLevelLocalStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelLocalStoreKey];
if (migrationLevel >= MPMigrationLevelLocalStoreCurrent) if (migrationLevel >= MPMigrationLevelLocalStoreCurrent)
// Local store up-to-date. // Local store up-to-date.
return; 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]) { 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; return;
} }
[[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelLocalStoreCurrent forKey:MPMigrationLevelLocalStoreKey]; [[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelLocalStoreCurrent forKey:MPMigrationLevelLocalStoreKey];
inf(@"Successfully migrated old to new local store."); inf( @"Successfully migrated old to new local store." );
} }
- (void)migrateCloudStore { - (void)migrateCloudStore {
MPMigrationLevelCloudStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelCloudStoreKey]; MPMigrationLevelCloudStore migrationLevel = (signed)[[NSUserDefaults standardUserDefaults] integerForKey:MPMigrationLevelCloudStoreKey];
if (migrationLevel >= MPMigrationLevelCloudStoreCurrent) if (migrationLevel >= MPMigrationLevelCloudStoreCurrent)
// Cloud store up-to-date. // Cloud store up-to-date.
return; 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 (migrationLevel <= MPMigrationLevelCloudStoreV1) {
if (![self migrateV1CloudStore]) { if (![self migrateV1CloudStore]) {
inf(@"Failed to migrate old V1 to new cloud store."); inf( @"Failed to migrate old V1 to new cloud store." );
return; return;
} }
} }
else if (migrationLevel <= MPMigrationLevelCloudStoreV2) { else if (migrationLevel <= MPMigrationLevelCloudStoreV2) {
if (![self migrateV2CloudStore]) { if (![self migrateV2CloudStore]) {
inf(@"Failed to migrate old V2 to new cloud store."); inf( @"Failed to migrate old V2 to new cloud store." );
return; return;
} }
} }
[[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelCloudStoreCurrent forKey:MPMigrationLevelCloudStoreKey]; [[NSUserDefaults standardUserDefaults] setInteger:MPMigrationLevelCloudStoreCurrent forKey:MPMigrationLevelCloudStoreKey];
inf(@"Successfully migrated old to new cloud store."); inf( @"Successfully migrated old to new cloud store." );
} }
- (BOOL)migrateV1CloudStore { - (BOOL)migrateV1CloudStore {
@ -211,11 +248,11 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
// Migrate cloud store. // Migrate cloud store.
NSString *uuid = [[NSUserDefaults standardUserDefaults] stringForKey:@"LocalUUIDKey"]; NSString *uuid = [[NSUserDefaults standardUserDefaults] stringForKey:@"LocalUUIDKey"];
if (!uuid) { if (!uuid) {
inf(@"No V1 cloud store to migrate."); inf( @"No V1 cloud store to migrate." );
return YES; 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 *cloudContainerURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:MPCloudContainerIdentifier];
NSURL *oldCloudContentURL = [[cloudContainerURL NSURL *oldCloudContentURL = [[cloudContainerURL
URLByAppendingPathComponent:@"Data" isDirectory:YES] URLByAppendingPathComponent:@"Data" isDirectory:YES]
@ -232,11 +269,11 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
// Migrate cloud store. // Migrate cloud store.
NSString *uuid = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:@"USMStoreUUIDKey"]; NSString *uuid = [[NSUbiquitousKeyValueStore defaultStore] stringForKey:@"USMStoreUUIDKey"];
if (!uuid) { if (!uuid) {
inf(@"No V2 cloud store to migrate."); inf( @"No V2 cloud store to migrate." );
return YES; 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 *cloudContainerURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:MPCloudContainerIdentifier];
NSURL *oldCloudContentURL = [[cloudContainerURL NSURL *oldCloudContentURL = [[cloudContainerURL
URLByAppendingPathComponent:@"CloudLogs" isDirectory:YES] URLByAppendingPathComponent:@"CloudLogs" isDirectory:YES]
@ -255,11 +292,11 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
NSURL *oldLocalStoreURL = [[applicationFilesDirectory NSURL *oldLocalStoreURL = [[applicationFilesDirectory
URLByAppendingPathComponent:@"MasterPassword" isDirectory:NO] URLByAppendingPathExtension:@"sqlite"]; URLByAppendingPathComponent:@"MasterPassword" isDirectory:NO] URLByAppendingPathExtension:@"sqlite"];
if (![[NSFileManager defaultManager] fileExistsAtPath:oldLocalStoreURL.path isDirectory:NO]) { if (![[NSFileManager defaultManager] fileExistsAtPath:oldLocalStoreURL.path isDirectory:NO]) {
inf(@"No V1 local store to migrate."); inf( @"No V1 local store to migrate." );
return YES; return YES;
} }
inf(@"Migrating V1 local store"); inf( @"Migrating V1 local store" );
return [self migrateFromLocalStore:oldLocalStoreURL]; return [self migrateFromLocalStore:oldLocalStoreURL];
} }
@ -267,7 +304,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
NSURL *newLocalStoreURL = [self.storeManager URLForLocalStore]; NSURL *newLocalStoreURL = [self.storeManager URLForLocalStore];
if ([[NSFileManager defaultManager] fileExistsAtPath:newLocalStoreURL.path isDirectory:NO]) { 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; return YES;
} }
@ -278,14 +315,14 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
return NO; return NO;
} }
inf(@"Successfully migrated to new local store."); inf( @"Successfully migrated to new local store." );
return YES; return YES;
} }
- (BOOL)migrateFromCloudStore:(NSURL *)oldCloudStoreURL cloudContent:(NSURL *)oldCloudContentURL { - (BOOL)migrateFromCloudStore:(NSURL *)oldCloudStoreURL cloudContent:(NSURL *)oldCloudContentURL {
if (![self.storeManager cloudSafeForSeeding]) { 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; return YES;
} }
@ -295,7 +332,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
strategy:0 error:nil cause:nil context:nil]) strategy:0 error:nil cause:nil context:nil])
return NO; return NO;
inf(@"Successfully migrated to new cloud store."); inf( @"Successfully migrated to new cloud store." );
return YES; return YES;
} }
@ -309,7 +346,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message { - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message {
inf(@"[StoreManager] %@", message); inf( @"[StoreManager] %@", message );
} }
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore { - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore {
@ -334,7 +371,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator
isCloud:(BOOL)isCloudStore { isCloud:(BOOL)isCloudStore {
inf(@"Using iCloud? %@", @(isCloudStore)); inf( @"Using iCloud? %@", @(isCloudStore) );
MPCheckpoint( MPCheckpointCloud, @{ MPCheckpoint( MPCheckpointCloud, @{
@"enabled" : @(isCloudStore) @"enabled" : @(isCloudStore)
} ); } );
@ -369,31 +406,37 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
self.saveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification self.saveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification
object:privateManagedObjectContext queue:nil usingBlock: object:privateManagedObjectContext queue:nil usingBlock:
^(NSNotification *note) { ^(NSNotification *note) {
// When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext. // When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext.
[mainManagedObjectContext performBlock:^{ [mainManagedObjectContext performBlock:^{
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note]; [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note];
}]; }];
}]; }];
self.privateManagedObjectContext = privateManagedObjectContext; self.privateManagedObjectContext = privateManagedObjectContext;
self.mainManagedObjectContext = mainManagedObjectContext; 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 - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreErrorCause)cause
context:(id)context { 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, @{ MPCheckpoint( MPCheckpointMPErrorUbiquity, @{
@"cause" : @(cause), @"cause" : @(cause),
@"error.code" : @(error.code), @"error.code" : @(error.code),
@"error.domain" : NilToNSNull(error.domain), @"error.domain" : NilToNSNull( error.domain ),
@"error.reason" : NilToNSNull(IfNotNilElse( [error localizedFailureReason], [error localizedDescription] )), @"error.reason" : NilToNSNull( IfNotNilElse( [error localizedFailureReason], [error localizedDescription] ) ),
} ); } );
} }
#pragma mark - Utilities #pragma mark - Utilities
- (void)addElementNamed:(NSString *)siteName completion:(void (^)(MPElementEntity *element))completion { - (void)addElementNamed:(NSString *)siteName completion:(void ( ^ )(MPElementEntity *element))completion {
if (![siteName length]) { if (![siteName length]) {
completion( nil ); completion( nil );
@ -402,7 +445,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
[MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { [MPAppDelegate_Shared managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPUserEntity *activeUser = [self activeUserInContext:context]; MPUserEntity *activeUser = [self activeUserInContext:context];
NSAssert(activeUser, @"Missing user."); NSAssert( activeUser, @"Missing user." );
if (!activeUser) { if (!activeUser) {
completion( nil ); completion( nil );
return; return;
@ -420,7 +463,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
NSError *error = nil; NSError *error = nil;
if (element.objectID.isTemporaryID && ![context obtainPermanentIDsForObjects:@[ element ] error:&error]) 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]; [context saveToStore];
@ -452,7 +495,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
NSError *error = nil; NSError *error = nil;
if (![context obtainPermanentIDsForObjects:@[ newElement ] error:&error]) 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 deleteObject:element];
[context saveToStore]; [context saveToStore];
@ -466,10 +509,10 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
} }
- (MPImportResult)importSites:(NSString *)importedSitesString - (MPImportResult)importSites:(NSString *)importedSitesString
askImportPassword:(NSString *(^)(NSString *userName))importPassword askImportPassword:(NSString *( ^ )(NSString *userName))importPassword
askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))userPassword { 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; __block MPImportResult result = MPImportResultCancelled;
do { do {
@ -485,8 +528,8 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
} }
- (MPImportResult)importSites:(NSString *)importedSitesString - (MPImportResult)importSites:(NSString *)importedSitesString
askImportPassword:(NSString *(^)(NSString *userName))askImportPassword askImportPassword:(NSString *( ^ )(NSString *userName))askImportPassword
askUserPassword:(NSString *(^)(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))askUserPassword askUserPassword:(NSString *( ^ )(NSString *userName, NSUInteger importCount, NSUInteger deleteCount))askUserPassword
saveInContext:(NSManagedObjectContext *)context { saveInContext:(NSManagedObjectContext *)context {
// Compile patterns. // Compile patterns.
@ -497,7 +540,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
initWithPattern:@"^#[[:space:]]*([^:]+): (.*)" initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
options:(NSRegularExpressionOptions)0 error:&error]; options:(NSRegularExpressionOptions)0 error:&error];
if (error) { if (error) {
err(@"Error loading the header pattern: %@", error); err( @"Error loading the header pattern: %@", error );
return MPImportResultInternalError; return MPImportResultInternalError;
} }
} }
@ -506,13 +549,13 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)(:[[:digit:]]+)?[[:space:]]+([^\t]+)\t(.*)" initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)(:[[:digit:]]+)?[[:space:]]+([^\t]+)\t(.*)"
options:(NSRegularExpressionOptions)0 error:&error]; options:(NSRegularExpressionOptions)0 error:&error];
if (error) { if (error) {
err(@"Error loading the site pattern: %@", error); err( @"Error loading the site pattern: %@", error );
return MPImportResultInternalError; return MPImportResultInternalError;
} }
} }
// Parse import data. // Parse import data.
inf(@"Importing sites."); inf( @"Importing sites." );
__block MPUserEntity *user = nil; __block MPUserEntity *user = nil;
id<MPAlgorithm> importAlgorithm = nil; id<MPAlgorithm> importAlgorithm = nil;
NSString *importBundleVersion = nil, *importUserName = nil; NSString *importBundleVersion = nil, *importUserName = nil;
@ -540,7 +583,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
// Header // Header
if ([headerPattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0 if ([headerPattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0
range:NSMakeRange( 0, [importedSiteLine length] )] != 1) { range:NSMakeRange( 0, [importedSiteLine length] )] != 1) {
err(@"Invalid header format in line: %@", importedSiteLine); err( @"Invalid header format in line: %@", importedSiteLine );
return MPImportResultMalformedInput; return MPImportResultMalformedInput;
} }
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:(NSMatchingOptions)0 NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:(NSMatchingOptions)0
@ -554,16 +597,16 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", importUserName]; userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", importUserName];
NSArray *users = [context executeFetchRequest:userFetchRequest error:&error]; NSArray *users = [context executeFetchRequest:userFetchRequest error:&error];
if (!users) { if (!users) {
err(@"While looking for user: %@, error: %@", importUserName, error); err( @"While looking for user: %@, error: %@", importUserName, error );
return MPImportResultInternalError; return MPImportResultInternalError;
} }
if ([users count] > 1) { 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; return MPImportResultInternalError;
} }
user = [users count]? [users lastObject]: nil; user = [users count]? [users lastObject]: nil;
dbg(@"Found user: %@", [user debugDescription]); dbg( @"Found user: %@", [user debugDescription] );
} }
if ([headerName isEqualToString:@"Key ID"]) if ([headerName isEqualToString:@"Key ID"])
importKeyID = [headerValue decodeHex]; importKeyID = [headerValue decodeHex];
@ -588,7 +631,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
// Site // Site
if ([sitePattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0 if ([sitePattern numberOfMatchesInString:importedSiteLine options:(NSMatchingOptions)0
range:NSMakeRange( 0, [importedSiteLine length] )] != 1) { range:NSMakeRange( 0, [importedSiteLine length] )] != 1) {
err(@"Invalid site format in line: %@", importedSiteLine); err( @"Invalid site format in line: %@", importedSiteLine );
return MPImportResultMalformedInput; return MPImportResultMalformedInput;
} }
NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:(NSMatchingOptions)0 NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:(NSMatchingOptions)0
@ -607,25 +650,26 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
elementFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user]; elementFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user];
NSArray *existingSites = [context executeFetchRequest:elementFetchRequest error:&error]; NSArray *existingSites = [context executeFetchRequest:elementFetchRequest error:&error];
if (!existingSites) { 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; return MPImportResultInternalError;
} }
if ([existingSites count]) { if ([existingSites count]) {
dbg(@"Existing sites: %@", existingSites); dbg( @"Existing sites: %@", existingSites );
[elementsToDelete addObjectsFromArray:existingSites]; [elementsToDelete addObjectsFromArray:existingSites];
} }
} }
[importedSiteElements addObject:@[ lastUsed, uses, type, version, name, exportContent ]]; [importedSiteElements addObject:@[ lastUsed, uses, type, version, name, exportContent ]];
dbg(@"Will import site: lastUsed=%@, uses=%@, type=%@, version=%@, name=%@, exportContent=%@", dbg( @"Will import site: lastUsed=%@, uses=%@, type=%@, version=%@, name=%@, exportContent=%@",
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. // 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], NSString *userMasterPassword = askUserPassword( user? user.name: importUserName, [importedSiteElements count],
[elementsToDelete count] ); [elementsToDelete count] );
if (!userMasterPassword) { if (!userMasterPassword) {
inf(@"Import cancelled."); inf( @"Import cancelled." );
return MPImportResultCancelled; return MPImportResultCancelled;
} }
MPKey *userKey = [MPAlgorithmDefault keyForPassword:userMasterPassword ofUserNamed:user? user.name: importUserName]; MPKey *userKey = [MPAlgorithmDefault keyForPassword:userMasterPassword ofUserNamed:user? user.name: importUserName];
@ -641,7 +685,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
// Delete existing sites. // Delete existing sites.
if (elementsToDelete.count) if (elementsToDelete.count)
[elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) { [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]; [context deleteObject:obj];
}]; }];
@ -650,7 +694,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
user = [MPUserEntity insertNewObjectInContext:context]; user = [MPUserEntity insertNewObjectInContext:context];
user.name = importUserName; user.name = importUserName;
user.keyID = importKeyID; user.keyID = importKeyID;
dbg(@"Created User: %@", [user debugDescription]); dbg( @"Created User: %@", [user debugDescription] );
} }
// Import new sites. // Import new sites.
@ -678,13 +722,13 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
[element.algorithm importProtectedContent:exportContent protectedByKey:importKey intoElement:element usingKey:userKey]; [element.algorithm importProtectedContent:exportContent protectedByKey:importKey intoElement:element usingKey:userKey];
} }
dbg(@"Created Element: %@", [element debugDescription]); dbg( @"Created Element: %@", [element debugDescription] );
} }
if (![context saveToStore]) if (![context saveToStore])
return MPImportResultInternalError; return MPImportResultInternalError;
inf(@"Import completed successfully."); inf( @"Import completed successfully." );
MPCheckpoint( MPCheckpointSitesImported, nil ); MPCheckpoint( MPCheckpointSitesImported, nil );
[[NSNotificationCenter defaultCenter] postNotificationName:MPSitesImportedNotification object:nil userInfo:@{ [[NSNotificationCenter defaultCenter] postNotificationName:MPSitesImportedNotification object:nil userInfo:@{
@ -697,7 +741,7 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext,
- (NSString *)exportSitesRevealPasswords:(BOOL)revealPasswords { - (NSString *)exportSitesRevealPasswords:(BOOL)revealPasswords {
MPUserEntity *activeUser = [self activeUserForMainThread]; 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. // Header.
NSMutableString *export = [NSMutableString new]; NSMutableString *export = [NSMutableString new];

View File

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

View File

@ -6,12 +6,11 @@
// Copyright (c) 2012 Lyndir. All rights reserved. // Copyright (c) 2012 Lyndir. All rights reserved.
// //
#import "MPConfig.h"
#import "MPAppDelegate_Shared.h" #import "MPAppDelegate_Shared.h"
@implementation MPConfig @implementation MPConfig
@dynamic sendInfo, rememberLogin, iCloudDecided; @dynamic sendInfo, rememberLogin, iCloudDecided, checkInconsistency;
- (id)init { - (id)init {
@ -19,11 +18,12 @@
return nil; return nil;
[self.defaults registerDefaults:@{ [self.defaults registerDefaults:@{
NSStringFromSelector( @selector(askForReviews) ) : @YES, NSStringFromSelector( @selector( askForReviews ) ) : @YES,
NSStringFromSelector( @selector(sendInfo) ) : @NO, NSStringFromSelector( @selector( sendInfo ) ) : @NO,
NSStringFromSelector( @selector(rememberLogin) ) : @NO, NSStringFromSelector( @selector( rememberLogin ) ) : @NO,
NSStringFromSelector( @selector(iCloudDecided) ) : @NO NSStringFromSelector( @selector( iCloudDecided ) ) : @NO,
NSStringFromSelector( @selector( checkInconsistency ) ) : @NO
}]; }];
self.delegate = [MPAppDelegate_Shared get]; self.delegate = [MPAppDelegate_Shared get];

View File

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

View File

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

View File

@ -7,9 +7,49 @@
// //
#import "MPElementGeneratedEntity.h" #import "MPElementGeneratedEntity.h"
#import "MPAppDelegate_Shared.h"
@implementation MPElementGeneratedEntity @implementation MPElementGeneratedEntity
@dynamic counter_; @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 @end

View File

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

View File

@ -7,9 +7,31 @@
// //
#import "MPElementStoredEntity.h" #import "MPElementStoredEntity.h"
#import "MPEntities.h"
#import "MPAppDelegate_Shared.h"
@implementation MPElementStoredEntity @implementation MPElementStoredEntity
@dynamic contentObject; @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 @end

View File

@ -38,34 +38,11 @@
- (MPElementType)type { - (MPElementType)type {
// Some people got elements with type == 0. return (MPElementType)[self.type_ unsignedIntegerValue];
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;
} }
- (void)setType:(MPElementType)aType { - (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); self.type_ = @(aType);
} }
@ -132,12 +109,12 @@
- (NSString *)description { - (NSString *)description {
return PearlString( @"%@:%@", [self class], [self name] ); return strf( @"%@:%@", [self class], [self name] );
} }
- (NSString *)debugDescription { - (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, NSStringFromClass( [self class] ), self.name, self.user.name, (long)self.type, (long)self.uses, self.lastUsed, (long)self.version,
self.loginName, self.requiresExplicitMigration ); 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 MPElementUpdatedNotification @"MPElementUpdatedNotification"
#define MPCheckConfigNotification @"MPCheckConfigNotification" #define MPCheckConfigNotification @"MPCheckConfigNotification"
#define MPSitesImportedNotification @"MPSitesImportedNotification" #define MPSitesImportedNotification @"MPSitesImportedNotification"
#define MPFoundInconsistenciesNotification @"MPFoundInconsistenciesNotification"
#define MPSitesImportedNotificationUserKey @"MPSitesImportedNotificationUserKey" #define MPSitesImportedNotificationUserKey @"MPSitesImportedNotificationUserKey"
#define MPInconsistenciesFixResultUserKey @"MPInconsistenciesFixResultUserKey"
static void MPCheckpoint(NSString *checkpoint, NSDictionary *attributes) { 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 { - (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue {
[[NSNotificationCenter defaultCenter] [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey )];
postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey ) userInfo:nil];
} }
#pragma mark - NSApplicationDelegate #pragma mark - NSApplicationDelegate

View File

@ -44,10 +44,8 @@
element = [super saveContentTypeWithElement:element saveInContext:context]; element = [super saveContentTypeWithElement:element saveInContext:context];
MPElementStoredEntity *storedElement = [self storedElement:element]; MPElementStoredEntity *storedElement = [self storedElement:element];
if (storedElement) { [storedElement.algorithm saveContent:self.contentField.text toElement:storedElement usingKey:[MPiOSAppDelegate get].key];
storedElement.contentObject = self.contentField.text; [context saveToStore];
[context saveToStore];
}
return element; return element;
} }
@ -78,7 +76,7 @@
switch (self.contentFieldMode) { switch (self.contentFieldMode) {
case MPContentFieldModePassword: { case MPContentFieldModePassword: {
storedElement.contentObject = newContent; [storedElement.algorithm saveContent:newContent toElement:storedElement usingKey:[MPiOSAppDelegate get].key];
[context saveToStore]; [context saveToStore];
PearlMainQueue( ^{ PearlMainQueue( ^{

View File

@ -68,6 +68,7 @@
[self registerObservers]; [self registerObservers];
[self observeStore]; [self observeStore];
[self updateFromConfig];
[self updatePasswords]; [self updatePasswords];
} }
@ -464,6 +465,11 @@
self.passwordSelectionContainer.alpha = 1; 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]; [[NSNotificationCenter defaultCenter] removeObserver:_storeObserver];
} }
- (void)updateFromConfig {
self.passwordsSearchBar.keyboardType = [[MPiOSConfig get].dictationSearch boolValue]? UIKeyboardTypeDefault: UIKeyboardTypeURL;
}
- (void)updatePasswords { - (void)updatePasswords {
NSString *query = self.query; NSString *query = self.query;

View File

@ -15,6 +15,7 @@
@property(weak, nonatomic) IBOutlet UITableViewCell *feedbackCell; @property(weak, nonatomic) IBOutlet UITableViewCell *feedbackCell;
@property(weak, nonatomic) IBOutlet UITableViewCell *coachmarksCell; @property(weak, nonatomic) IBOutlet UITableViewCell *coachmarksCell;
@property(weak, nonatomic) IBOutlet UITableViewCell *exportCell; @property(weak, nonatomic) IBOutlet UITableViewCell *exportCell;
@property(weak, nonatomic) IBOutlet UITableViewCell *checkInconsistencies;
@property(weak, nonatomic) IBOutlet UIImageView *avatarImage; @property(weak, nonatomic) IBOutlet UIImageView *avatarImage;
@property(weak, nonatomic) IBOutlet UISegmentedControl *generatedTypeControl; @property(weak, nonatomic) IBOutlet UISegmentedControl *generatedTypeControl;
@property(weak, nonatomic) IBOutlet UISegmentedControl *storedTypeControl; @property(weak, nonatomic) IBOutlet UISegmentedControl *storedTypeControl;

View File

@ -29,7 +29,7 @@
- (void)viewWillAppear:(BOOL)animated { - (void)viewWillAppear:(BOOL)animated {
inf(@"Preferences will appear"); inf( @"Preferences will appear" );
[super viewWillAppear:animated]; [super viewWillAppear:animated];
MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForMainThread]; MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForMainThread];
@ -70,6 +70,14 @@
[vc performSegueWithIdentifier:@"coachmarks" sender:self]; [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]; [tableView deselectRowAtIndexPath:indexPath animated:YES];
} }
@ -162,7 +170,7 @@
case 1: case 1:
return MPElementTypeStoredDevicePrivate; return MPElementTypeStoredDevicePrivate;
default: 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 #endif
} }
@catch (id exception) { @catch (id exception) {
err(@"During Analytics Setup: %@", exception); err( @"During Analytics Setup: %@", exception );
} }
@try { @try {
[[NSNotificationCenter defaultCenter] addObserverForName:MPCheckConfigNotification object:nil queue:nil usingBlock: [[NSNotificationCenter defaultCenter] addObserverForName:MPCheckConfigNotification object:nil queue:nil usingBlock:
^(NSNotification *note) { ^(NSNotification *note) {
[self checkConfig]; [self updateFromConfig];
}]; }];
[[NSNotificationCenter defaultCenter] [[NSNotificationCenter defaultCenter] addObserverForName:kIASKAppSettingChanged object:nil queue:nil usingBlock:
addObserverForName:kIASKAppSettingChanged object:nil queue:nil usingBlock:^(NSNotification *note) { ^(NSNotification *note) {
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:note userInfo:nil];
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 #ifdef ADHOC
@ -137,18 +140,41 @@
#endif #endif
} }
@catch (id exception) { @catch (id exception) {
err(@"During Config Test: %@", exception); err( @"During Config Test: %@", exception );
} }
@try { @try {
[super application:application didFinishLaunchingWithOptions:launchOptions]; [super application:application didFinishLaunchingWithOptions:launchOptions];
} }
@catch (id exception) { @catch (id exception) {
err(@"During Pearl Application Launch: %@", exception); err( @"During Pearl Application Launch: %@", exception );
} }
@try { @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]) if ([[MPiOSConfig get].showSetup boolValue])
[self.navigationController performSegueWithIdentifier:@"setup" sender:self]; [self.navigationController performSegueWithIdentifier:@"setup" sender:self];
} ); } );
@ -166,7 +192,7 @@
} ); } );
} }
@catch (id exception) { @catch (id exception) {
err(@"During Post-Startup: %@", exception); err( @"During Post-Startup: %@", exception );
} }
return YES; return YES;
@ -186,7 +212,7 @@
NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url]
returningResponse:&response error:&error]; returningResponse:&response error:&error];
if (error) if (error)
err(@"While reading imported sites from %@: %@", url, error); err( @"While reading imported sites from %@: %@", url, error );
if (!importedSitesData) if (!importedSitesData)
return; return;
@ -227,7 +253,7 @@
dispatch_async( dispatch_get_main_queue(), ^{ dispatch_async( dispatch_get_main_queue(), ^{
[PearlAlert showAlertWithTitle:PearlString( @"Master Password for\n%@", userName ) [PearlAlert showAlertWithTitle:PearlString( @"Master Password for\n%@", userName )
message:PearlString( @"Imports %lu sites, overwriting %lu.", message:PearlString( @"Imports %lu sites, overwriting %lu.",
(unsigned long)importCount, (unsigned long)deleteCount ) (unsigned long)importCount, (unsigned long)deleteCount )
viewStyle:UIAlertViewStyleSecureTextInput viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) { initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
@try { @try {
@ -270,7 +296,7 @@
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
inf(@"Received memory warning."); inf( @"Received memory warning." );
[super applicationDidReceiveMemoryWarning:application]; [super applicationDidReceiveMemoryWarning:application];
} }
@ -307,7 +333,7 @@
- (void)applicationWillResignActive:(UIApplication *)application { - (void)applicationWillResignActive:(UIApplication *)application {
inf(@"Will deactivate"); inf( @"Will deactivate" );
if (![[MPiOSConfig get].rememberLogin boolValue]) if (![[MPiOSConfig get].rememberLogin boolValue])
[self signOutAnimated:NO]; [self signOutAnimated:NO];
@ -321,9 +347,8 @@
- (void)applicationDidBecomeActive:(UIApplication *)application { - (void)applicationDidBecomeActive:(UIApplication *)application {
inf(@"Re-activated"); inf( @"Re-activated" );
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:application];
object:application userInfo:nil];
#ifdef LOCALYTICS #ifdef LOCALYTICS
[[LocalyticsSession sharedLocalyticsSession] resume]; [[LocalyticsSession sharedLocalyticsSession] resume];
@ -377,14 +402,14 @@
[[[PearlEMail alloc] initForEMailTo:@"Master Password Development <masterpassword@lyndir.com>" [[[PearlEMail alloc] initForEMailTo:@"Master Password Development <masterpassword@lyndir.com>"
subject:PearlString( @"Feedback for Master Password [%@]", subject:PearlString( @"Feedback for Master Password [%@]",
[[PearlKeyChain deviceIdentifier] stringByDeletingMatchesOf:@"-.*"] ) [[PearlKeyChain deviceIdentifier] stringByDeletingMatchesOf:@"-.*"] )
body:PearlString( @"\n\n\n" body:PearlString( @"\n\n\n"
@"--\n" @"--\n"
@"%@" @"%@"
@"Master Password %@, build %@", @"Master Password %@, build %@",
userName? ([userName stringByAppendingString:@"\n"]): @"", userName? ([userName stringByAppendingString:@"\n"]): @"",
[PearlInfoPlist get].CFBundleShortVersionString, [PearlInfoPlist get].CFBundleShortVersionString,
[PearlInfoPlist get].CFBundleVersion ) [PearlInfoPlist get].CFBundleVersion )
attachments:(logs attachments:(logs
? [[PearlEMailAttachment alloc] ? [[PearlEMailAttachment alloc]
@ -392,8 +417,8 @@
dataUsingEncoding:NSUTF8StringEncoding] dataUsingEncoding:NSUTF8StringEncoding]
mimeType:@"text/plain" mimeType:@"text/plain"
fileName:PearlString( @"%@-%@.log", fileName:PearlString( @"%@-%@.log",
[[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]], [[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]],
[PearlKeyChain deviceIdentifier] )] [PearlKeyChain deviceIdentifier] )]
: nil), nil] : nil), nil]
showComposerForVC:viewController]; 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" @"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." @"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) { tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
[PearlAlert showAlertWithTitle:@"Reveal Passwords?" message: [PearlAlert showAlertWithTitle:@"Reveal Passwords?" message:
@"Would you like to make all your passwords visible in the export?\n\n" @"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, " @"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" @"making the result safe from falling in the wrong hands.\n\n"
@"If all your passwords are shown and somebody else finds the export, " @"If all your passwords are shown and somebody else finds the export, "
@"they could gain access to all your sites!" @"they could gain access to all your sites!"
viewStyle:UIAlertViewStyleDefault initAlert:nil viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) { tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 0) if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 0)
// Safe Export // Safe Export
[self showExportRevealPasswords:NO forVC:viewController]; [self showExportRevealPasswords:NO forVC:viewController];
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1) if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1)
// Show Passwords // Show Passwords
[self showExportRevealPasswords:YES forVC:viewController]; [self showExportRevealPasswords:YES forVC:viewController];
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil]; } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil];
} otherTitles:nil]; } otherTitles:nil];
} }
- (void)showExportRevealPasswords:(BOOL)revealPasswords forVC:(UIViewController *)viewController { - (void)showExportRevealPasswords:(BOOL)revealPasswords forVC:(UIViewController *)viewController {
@ -445,17 +470,17 @@
@"--\n" @"--\n"
@"%@\n" @"%@\n"
@"Master Password %@, build %@", @"Master Password %@, build %@",
[self activeUserForMainThread].name, [self activeUserForMainThread].name,
[PearlInfoPlist get].CFBundleShortVersionString, [PearlInfoPlist get].CFBundleShortVersionString,
[PearlInfoPlist get].CFBundleVersion ); [PearlInfoPlist get].CFBundleVersion );
else else
message = PearlString( @"Backup of Master Password sites.\n\n\n" message = PearlString( @"Backup of Master Password sites.\n\n\n"
@"--\n" @"--\n"
@"%@\n" @"%@\n"
@"Master Password %@, build %@", @"Master Password %@, build %@",
[self activeUserForMainThread].name, [self activeUserForMainThread].name,
[PearlInfoPlist get].CFBundleShortVersionString, [PearlInfoPlist get].CFBundleShortVersionString,
[PearlInfoPlist get].CFBundleVersion ); [PearlInfoPlist get].CFBundleVersion );
NSDateFormatter *exportDateFormatter = [NSDateFormatter new]; NSDateFormatter *exportDateFormatter = [NSDateFormatter new];
[exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"]; [exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
@ -464,11 +489,11 @@
attachments:[[PearlEMailAttachment alloc] initWithContent:[exportedSites dataUsingEncoding:NSUTF8StringEncoding] attachments:[[PearlEMailAttachment alloc] initWithContent:[exportedSites dataUsingEncoding:NSUTF8StringEncoding]
mimeType:@"text/plain" fileName: mimeType:@"text/plain" fileName:
PearlString( @"%@ (%@).mpsites", [self activeUserForMainThread].name, PearlString( @"%@ (%@).mpsites", [self activeUserForMainThread].name,
[exportDateFormatter stringFromDate:[NSDate date]] )], [exportDateFormatter stringFromDate:[NSDate date]] )],
nil]; 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" [PearlAlert showAlertWithTitle:@"Changing Master Password"
message: message:
@ -481,7 +506,7 @@
return; return;
[moc performBlockAndWait:^{ [moc performBlockAndWait:^{
inf(@"Unsetting master password for: %@.", user.userID); inf( @"Unsetting master password for: %@.", user.userID );
user.keyID = nil; user.keyID = nil;
[self forgetSavedKeyFor:user]; [self forgetSavedKeyFor:user];
[moc saveToStore]; [moc saveToStore];
@ -497,16 +522,14 @@
otherTitles:[PearlStrings get].commonButtonContinue, nil]; otherTitles:[PearlStrings get].commonButtonContinue, nil];
} }
#pragma mark - PearlConfigDelegate #pragma mark - PearlConfigDelegate
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)value { - (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)value {
[[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification [[NSNotificationCenter defaultCenter] postNotificationName:MPCheckConfigNotification object:NSStringFromSelector( configKey )];
object:NSStringFromSelector( configKey ) userInfo:nil];
} }
- (void)checkConfig { - (void)updateFromConfig {
// iCloud enabled / disabled // iCloud enabled / disabled
BOOL iCloudEnabled = [[MPiOSConfig get].iCloudEnabled boolValue]; BOOL iCloudEnabled = [[MPiOSConfig get].iCloudEnabled boolValue];
@ -519,7 +542,7 @@
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )];
NSError *error = nil; NSError *error = nil;
if ((siteCount = [context countForFetchRequest:fetchRequest error:&error]) == NSNotFound) { if ((siteCount = [context countForFetchRequest:fetchRequest error:&error]) == NSNotFound) {
wrn(@"Couldn't count current sites: %@", error); wrn( @"Couldn't count current sites: %@", error );
return; return;
} }
}]; }];
@ -536,11 +559,11 @@
@"or overwrite them with your current sites." @"or overwrite them with your current sites."
viewStyle:UIAlertViewStyleDefault initAlert:nil viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex]) if (buttonIndex == [alert cancelButtonIndex])
setConfirmationAnswer( NO ); setConfirmationAnswer( NO );
if (buttonIndex == [alert firstOtherButtonIndex]) if (buttonIndex == [alert firstOtherButtonIndex])
setConfirmationAnswer( YES ); setConfirmationAnswer( YES );
} }
cancelTitle:@"Use Old" otherTitles:@"Overwrite", nil]; cancelTitle:@"Use Old" otherTitles:@"Overwrite", nil];
}]; }];
else else
@ -550,7 +573,7 @@
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )];
NSError *error = nil; NSError *error = nil;
if ((siteCount = [context countForFetchRequest:fetchRequest error:&error]) == NSNotFound) { if ((siteCount = [context countForFetchRequest:fetchRequest error:&error]) == NSNotFound) {
wrn(@"Couldn't count current sites: %@", error); wrn( @"Couldn't count current sites: %@", error );
return; return;
} }
}]; }];
@ -566,11 +589,11 @@
@"or overwrite them with your current iCloud sites." @"or overwrite them with your current iCloud sites."
viewStyle:UIAlertViewStyleDefault initAlert:nil viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert cancelButtonIndex]) if (buttonIndex == [alert cancelButtonIndex])
setConfirmationAnswer( NO ); setConfirmationAnswer( NO );
if (buttonIndex == [alert firstOtherButtonIndex]) if (buttonIndex == [alert firstOtherButtonIndex])
setConfirmationAnswer( YES ); setConfirmationAnswer( YES );
} }
cancelTitle:@"Use Old" otherTitles:@"Overwrite", nil]; cancelTitle:@"Use Old" otherTitles:@"Overwrite", nil];
}]; }];
} }
@ -630,15 +653,14 @@
@"helpHidden" : @([[MPiOSConfig get].helpHidden boolValue]), @"helpHidden" : @([[MPiOSConfig get].helpHidden boolValue]),
@"showQuickStart" : @([[MPiOSConfig get].showSetup boolValue]), @"showQuickStart" : @([[MPiOSConfig get].showSetup boolValue]),
@"firstRun" : @([[PearlConfig get].firstRun boolValue]), @"firstRun" : @([[PearlConfig get].firstRun boolValue]),
@"launchCount" : NilToNSNull([PearlConfig get].launchCount), @"launchCount" : NilToNSNull( [PearlConfig get].launchCount ),
@"askForReviews" : @([[PearlConfig get].askForReviews boolValue]), @"askForReviews" : @([[PearlConfig get].askForReviews boolValue]),
@"reviewAfterLaunches" : NilToNSNull([PearlConfig get].reviewAfterLaunches), @"reviewAfterLaunches" : NilToNSNull( [PearlConfig get].reviewAfterLaunches ),
@"reviewedVersion" : NilToNSNull([PearlConfig get].reviewedVersion) @"reviewedVersion" : NilToNSNull( [PearlConfig get].reviewedVersion )
} ); } );
} }
} }
#pragma mark - UbiquityStoreManager #pragma mark - UbiquityStoreManager
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore { - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore {
@ -690,13 +712,13 @@
@"To add one, go into Apple's Settings -> iCloud." @"To add one, go into Apple's Settings -> iCloud."
viewStyle:UIAlertViewStyleDefault initAlert:nil viewStyle:UIAlertViewStyleDefault initAlert:nil
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == alert.firstOtherButtonIndex) { if (buttonIndex == alert.firstOtherButtonIndex) {
[MPiOSConfig get].iCloudEnabled = @NO; [MPiOSConfig get].iCloudEnabled = @NO;
return; return;
} }
[self.storeManager reloadStore]; [self.storeManager reloadStore];
} cancelTitle:@"Try Again" otherTitles:@"Disable iCloud", nil]; } cancelTitle:@"Try Again" otherTitles:@"Disable iCloud", nil];
return YES; return YES;
} }
@ -710,30 +732,29 @@
message:@"Waiting for your other device to autocorrect the problem..." message:@"Waiting for your other device to autocorrect the problem..."
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock: viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:
^(UIAlertView *alert, NSInteger buttonIndex) { ^(UIAlertView *alert, NSInteger buttonIndex) {
if (buttonIndex == [alert firstOtherButtonIndex]) if (buttonIndex == [alert firstOtherButtonIndex])
wSelf.fixCloudContentAlert = [PearlAlert showAlertWithTitle:@"Fix iCloud Now" message: wSelf.fixCloudContentAlert = [PearlAlert showAlertWithTitle:@"Fix iCloud Now" message:
@"This problem can be autocorrected by opening the app on another device where you recently made changes.\n" @"This problem can be autocorrected 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 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." @"You can also turn iCloud off for now."
viewStyle:UIAlertViewStyleDefault viewStyle:UIAlertViewStyleDefault
initAlert:nil tappedButtonBlock: initAlert:nil tappedButtonBlock:
^(UIAlertView *alert_, NSInteger buttonIndex_) { ^(UIAlertView *alert_, NSInteger buttonIndex_) {
if (buttonIndex_ == alert_.cancelButtonIndex) if (buttonIndex_ == alert_.cancelButtonIndex)
[wSelf showCloudContentAlert]; [wSelf showCloudContentAlert];
if (buttonIndex_ == [alert_ firstOtherButtonIndex]) if (buttonIndex_ == [alert_ firstOtherButtonIndex])
[wSelf.storeManager rebuildCloudContentFromCloudStoreOrLocalStore:YES]; [wSelf.storeManager rebuildCloudContentFromCloudStoreOrLocalStore:YES];
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1) if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1)
[MPiOSConfig get].iCloudEnabled = @NO; [MPiOSConfig get].iCloudEnabled = @NO;
} }
cancelTitle:[PearlStrings get].commonButtonBack cancelTitle:[PearlStrings get].commonButtonBack
otherTitles:@"Fix Anyway", otherTitles:@"Fix Anyway",
@"Turn Off", nil]; @"Turn Off", nil];
if (buttonIndex == [alert firstOtherButtonIndex] + 1) if (buttonIndex == [alert firstOtherButtonIndex] + 1)
[MPiOSConfig get].iCloudEnabled = @NO; [MPiOSConfig get].iCloudEnabled = @NO;
} cancelTitle:nil otherTitles:@"Fix Now", @"Turn Off", nil]; } cancelTitle:nil otherTitles:@"Fix Now", @"Turn Off", nil];
} }
#pragma mark - TestFlight #pragma mark - TestFlight
- (NSDictionary *)testFlightInfo { - (NSDictionary *)testFlightInfo {
@ -748,14 +769,13 @@
- (NSString *)testFlightToken { - (NSString *)testFlightToken {
NSString *testFlightToken = NSNullToNil([[self testFlightInfo] valueForKeyPath:@"Application Token"]); NSString *testFlightToken = NSNullToNil( [[self testFlightInfo] valueForKeyPath:@"Application Token"] );
if (![testFlightToken length]) 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; return testFlightToken;
} }
#pragma mark - Crashlytics #pragma mark - Crashlytics
- (NSDictionary *)crashlyticsInfo { - (NSDictionary *)crashlyticsInfo {
@ -770,14 +790,13 @@
- (NSString *)crashlyticsAPIKey { - (NSString *)crashlyticsAPIKey {
NSString *crashlyticsAPIKey = NSNullToNil([[self crashlyticsInfo] valueForKeyPath:@"API Key"]); NSString *crashlyticsAPIKey = NSNullToNil( [[self crashlyticsInfo] valueForKeyPath:@"API Key"] );
if (![crashlyticsAPIKey length]) 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; return crashlyticsAPIKey;
} }
#pragma mark - Localytics #pragma mark - Localytics
- (NSDictionary *)localyticsInfo { - (NSDictionary *)localyticsInfo {
@ -793,12 +812,12 @@
- (NSString *)localyticsKey { - (NSString *)localyticsKey {
#ifdef DEBUG #ifdef DEBUG
NSString *localyticsKey = NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.development"]); NSString *localyticsKey = NSNullToNil( [[self localyticsInfo] valueForKeyPath:@"Key.development"] );
#else #else
NSString *localyticsKey = NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution"]); NSString *localyticsKey = NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution"]);
#endif #endif
if (![localyticsKey length]) 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; return localyticsKey;
} }

View File

@ -18,5 +18,6 @@
@property(nonatomic, retain) NSNumber *loginNameTipShown; @property(nonatomic, retain) NSNumber *loginNameTipShown;
@property(nonatomic, retain) NSNumber *traceMode; @property(nonatomic, retain) NSNumber *traceMode;
@property(nonatomic, retain) NSNumber *iCloudEnabled; @property(nonatomic, retain) NSNumber *iCloudEnabled;
@property(nonatomic, retain) NSNumber *dictationSearch;
@end @end

View File

@ -8,7 +8,7 @@
@implementation MPiOSConfig @implementation MPiOSConfig
@dynamic helpHidden, siteInfoHidden, showSetup, actionsTipShown, typeTipShown, loginNameTipShown, traceMode, iCloudEnabled; @dynamic helpHidden, siteInfoHidden, showSetup, actionsTipShown, typeTipShown, loginNameTipShown, traceMode, iCloudEnabled, dictationSearch;
- (id)init { - (id)init {
@ -24,7 +24,8 @@
NSStringFromSelector( @selector(typeTipShown) ) : @(!self.firstRun), NSStringFromSelector( @selector(typeTipShown) ) : @(!self.firstRun),
NSStringFromSelector( @selector(loginNameTipShown) ) : @NO, NSStringFromSelector( @selector(loginNameTipShown) ) : @NO,
NSStringFromSelector( @selector(traceMode) ) : @NO, NSStringFromSelector( @selector(traceMode) ) : @NO,
NSStringFromSelector( @selector(iCloudEnabled) ) : @NO NSStringFromSelector( @selector(iCloudEnabled) ) : @NO,
NSStringFromSelector( @selector( dictationSearch) ) : @NO
}]; }];
return self; return self;

View File

@ -47,6 +47,7 @@
93D39CB5E2EC1078E898F46A /* MPPasswordLargeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3937863061C3916AF7AD2 /* MPPasswordLargeCell.m */; }; 93D39CB5E2EC1078E898F46A /* MPPasswordLargeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3937863061C3916AF7AD2 /* MPPasswordLargeCell.m */; };
93D39D596A2E376D6F6F5DA1 /* MPCombinedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393310223DDB35218467A /* MPCombinedViewController.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 */; }; 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 */; }; 93D39EDD960C381D64E4DCDD /* MPPasswordSmallCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3952CC60991B97D69F26A /* MPPasswordSmallCell.m */; };
93D39F8A9254177891F38705 /* MPSetupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39A28369954D147E239BA /* MPSetupViewController.m */; }; 93D39F8A9254177891F38705 /* MPSetupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39A28369954D147E239BA /* MPSetupViewController.m */; };
93D39FA97F4C3F69A75D5A03 /* MPPasswordLargeGeneratedCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3993422E207BF0B21D089 /* MPPasswordLargeGeneratedCell.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 93D39B050DD5F55E9794EFD4 /* MPPopdownSegue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPopdownSegue.m; sourceTree = "<group>"; };
@ -2496,6 +2499,8 @@
DABD3BB91711E2DC00CF925C /* MPUserEntity.h */, DABD3BB91711E2DC00CF925C /* MPUserEntity.h */,
DABD3BBA1711E2DC00CF925C /* MPUserEntity.m */, DABD3BBA1711E2DC00CF925C /* MPUserEntity.m */,
DABD3BD01711E2DC00CF925C /* MasterPassword.xcdatamodeld */, DABD3BD01711E2DC00CF925C /* MasterPassword.xcdatamodeld */,
93D399F244BB522A317811BB /* MPFixable.h */,
93D39A813CA9D7E192261ED2 /* MPFixable.m */,
); );
name = ObjC; name = ObjC;
path = ..; path = ..;
@ -3840,6 +3845,7 @@
93D39BA1EA3CAAC8A220B4A6 /* MPAppSettingsViewController.m in Sources */, 93D39BA1EA3CAAC8A220B4A6 /* MPAppSettingsViewController.m in Sources */,
93D396D8B67DA6522CDBA142 /* MPCoachmarkViewController.m in Sources */, 93D396D8B67DA6522CDBA142 /* MPCoachmarkViewController.m in Sources */,
93D391C07818F4C2DC1B6956 /* MPPasswordsCoachmarkViewController.m in Sources */, 93D391C07818F4C2DC1B6956 /* MPPasswordsCoachmarkViewController.m in Sources */,
93D39EAA4D064193074D3021 /* MPFixable.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -70,6 +70,24 @@
<key>Type</key> <key>Type</key>
<string>PSToggleSwitchSpecifier</string> <string>PSToggleSwitchSpecifier</string>
</dict> </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> <dict>
<key>Type</key> <key>Type</key>
<string>PSGroupSpecifier</string> <string>PSGroupSpecifier</string>
@ -88,6 +106,24 @@
<key>DefaultValue</key> <key>DefaultValue</key>
<false/> <false/>
</dict> </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> </array>
<key>StringsTable</key> <key>StringsTable</key>
<string>Root</string> <string>Root</string>

View File

@ -714,8 +714,42 @@
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</tableViewCell> </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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="IVT-Rs-nTu" id="Q5J-2f-mmz"> <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"/> <rect key="frame" x="0.0" y="0.0" width="287" height="124"/>
@ -762,6 +796,7 @@
<size key="freeformSize" width="320" height="568"/> <size key="freeformSize" width="320" height="568"/>
<connections> <connections>
<outlet property="avatarImage" destination="tWi-sc-DGp" id="ifT-Ct-WL6"/> <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="coachmarksCell" destination="eth-Dc-JYn" id="0Tq-I3-SwK"/>
<outlet property="exportCell" destination="IVT-Rs-nTu" id="RU0-qr-Bdi"/> <outlet property="exportCell" destination="IVT-Rs-nTu" id="RU0-qr-Bdi"/>
<outlet property="feedbackCell" destination="9QG-lM-ymM" id="18X-Ph-0ac"/> <outlet property="feedbackCell" destination="9QG-lM-ymM" id="18X-Ph-0ac"/>