2
0

MPAlgorithm V2: handle multi-byte UTF-8 correctly by counting bytes, not characters.

This commit is contained in:
Maarten Billemont 2014-09-24 07:58:23 -04:00
parent 466863f8fd
commit 9bb613a3b6
15 changed files with 172 additions and 28 deletions

2
External/Pearl vendored

@ -1 +1 @@
Subproject commit 82753ec57f65343bb86a3db86d84d933a7aa9caa Subproject commit 5af61226a78398de136dd1663f9e6ac8624e7df5

View File

@ -19,7 +19,7 @@
#import "MPStoredSiteEntity.h" #import "MPStoredSiteEntity.h"
#import "MPGeneratedSiteEntity.h" #import "MPGeneratedSiteEntity.h"
#define MPAlgorithmDefaultVersion 1 #define MPAlgorithmDefaultVersion 2
#define MPAlgorithmDefault MPAlgorithmForVersion(MPAlgorithmDefaultVersion) #define MPAlgorithmDefault MPAlgorithmForVersion(MPAlgorithmDefaultVersion)
id<MPAlgorithm> MPAlgorithmForVersion(NSUInteger version); id<MPAlgorithm> MPAlgorithmForVersion(NSUInteger version);
@ -43,8 +43,8 @@ NSString *NSStringFromTimeToCrack(TimeToCrack timeToCrack);
@required @required
- (NSUInteger)version; - (NSUInteger)version;
- (BOOL)migrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc; - (BOOL)tryMigrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc;
- (BOOL)migrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit; - (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit;
- (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName; - (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName;
- (MPKey *)keyFromKeyData:(NSData *)keyData; - (MPKey *)keyFromKeyData:(NSData *)keyData;

View File

@ -35,6 +35,9 @@ id<MPAlgorithm> MPAlgorithmDefaultForBundleVersion(NSString *bundleVersion) {
if (PearlCFBundleVersionCompare( bundleVersion, @"1.3" ) == NSOrderedAscending) if (PearlCFBundleVersionCompare( bundleVersion, @"1.3" ) == NSOrderedAscending)
// Pre-1.3 // Pre-1.3
return MPAlgorithmForVersion( 0 ); return MPAlgorithmForVersion( 0 );
if (PearlCFBundleVersionCompare( bundleVersion, @"2.1" ) == NSOrderedAscending)
// Pre-2.1
return MPAlgorithmForVersion( 1 );
return MPAlgorithmDefault; return MPAlgorithmDefault;
} }

View File

@ -73,7 +73,7 @@
return [(id<MPAlgorithm>)other version] == [self version]; return [(id<MPAlgorithm>)other version] == [self version];
} }
- (BOOL)migrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc { - (BOOL)tryMigrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc {
NSError *error = nil; NSError *error = nil;
NSFetchRequest *migrationRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )]; NSFetchRequest *migrationRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )];
@ -84,15 +84,15 @@
return NO; return NO;
} }
BOOL requiresExplicitMigration = NO; BOOL success = YES;
for (MPSiteEntity *migrationSite in migrationSites) for (MPSiteEntity *migrationSite in migrationSites)
if (![migrationSite migrateExplicitly:NO]) if (![migrationSite tryMigrateExplicitly:NO])
requiresExplicitMigration = YES; success = NO;
return requiresExplicitMigration; return success;
} }
- (BOOL)migrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit { - (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit {
if (site.version != [self version] - 1) if (site.version != [self version] - 1)
// Only migrate from previous version. // Only migrate from previous version.
@ -364,11 +364,11 @@
- (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key { - (NSString *)generateLoginForSiteNamed:(NSString *)name usingKey:(MPKey *)key {
return [self generateContentForSiteNamed:name ofType:MPSiteTypeGeneratedName withCounter:1 return [self generateContentForSiteNamed:name ofType:MPSiteTypeGeneratedName withCounter:1
variant:MPSiteVariantLogin context:nil usingKey:key]; variant:MPSiteVariantLogin context:nil usingKey:key];
} }
- (NSString *)generatePasswordForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter - (NSString *)generatePasswordForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
usingKey:(MPKey *)key { usingKey:(MPKey *)key {
return [self generateContentForSiteNamed:name ofType:type withCounter:counter return [self generateContentForSiteNamed:name ofType:type withCounter:counter
variant:MPSiteVariantPassword context:nil usingKey:key]; variant:MPSiteVariantPassword context:nil usingKey:key];
@ -381,7 +381,7 @@
} }
- (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter - (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key { variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key {
// 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 ), ncontextLength = htonl( context.length ); uint32_t ncounter = htonl( counter ), nnameLength = htonl( name.length ), ncontextLength = htonl( context.length );
@ -396,9 +396,9 @@
nameLengthBytes, nameLengthBytes,
[name dataUsingEncoding:NSUTF8StringEncoding], [name dataUsingEncoding:NSUTF8StringEncoding],
counterBytes, counterBytes,
context? contextLengthBytes: nil, context? contextLengthBytes: nil,
[context dataUsingEncoding:NSUTF8StringEncoding], [context dataUsingEncoding:NSUTF8StringEncoding],
nil] nil]
hmacWith:PearlHashSHA256 key:key.keyData]; hmacWith:PearlHashSHA256 key:key.keyData];
trc( @"seed is: %@", [seed encodeHex] ); trc( @"seed is: %@", [seed encodeHex] );
const char *seedBytes = seed.bytes; const char *seedBytes = seed.bytes;
@ -678,7 +678,7 @@
} }
- (void)importProtectedPassword:(NSString *)protectedContent protectedByKey:(MPKey *)importKey - (void)importProtectedPassword:(NSString *)protectedContent protectedByKey:(MPKey *)importKey
intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey { intoSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
NSAssert( [siteKey.keyID isEqualToData:site.user.keyID], @"Site does not belong to current user." ); NSAssert( [siteKey.keyID isEqualToData:site.user.keyID], @"Site does not belong to current user." );
switch (site.type) { switch (site.type) {

View File

@ -25,7 +25,7 @@
return 1; return 1;
} }
- (BOOL)migrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit { - (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit {
if (site.version != [self version] - 1) if (site.version != [self version] - 1)
// Only migrate from previous version. // Only migrate from previous version.
@ -46,12 +46,13 @@
} }
- (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter - (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
variant:(MPSiteVariant)variant usingKey:(MPKey *)key { variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key {
// 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 ), ncontextLength = htonl( context.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 )];
NSData *contextLengthBytes = [NSData dataWithBytes:&ncontextLength length:sizeof( ncontextLength )];
NSString *scope = [self scopeForVariant:variant]; NSString *scope = [self scopeForVariant:variant];
trc( @"seed from: hmac-sha256(%@, %@ | %@ | %@ | %@)", trc( @"seed from: hmac-sha256(%@, %@ | %@ | %@ | %@)",
[[key keyID] encodeHex], scope, [nameLengthBytes encodeHex], name, [counterBytes encodeHex] ); [[key keyID] encodeHex], scope, [nameLengthBytes encodeHex], name, [counterBytes encodeHex] );
@ -60,6 +61,8 @@
nameLengthBytes, nameLengthBytes,
[name dataUsingEncoding:NSUTF8StringEncoding], [name dataUsingEncoding:NSUTF8StringEncoding],
counterBytes, counterBytes,
context? contextLengthBytes: nil,
[context dataUsingEncoding:NSUTF8StringEncoding],
nil] nil]
hmacWith:PearlHashSHA256 key:key.keyData]; hmacWith:PearlHashSHA256 key:key.keyData];
trc( @"seed is: %@", [seed encodeHex] ); trc( @"seed is: %@", [seed encodeHex] );

View File

@ -0,0 +1,21 @@
/**
* 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
*/
//
// MPAlgorithmV1
//
// Created by Maarten Billemont on 17/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import "MPAlgorithmV1.h"
@interface MPAlgorithmV2 : MPAlgorithmV1
@end

View File

@ -0,0 +1,97 @@
/**
* 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
*/
//
// MPAlgorithmV1
//
// Created by Maarten Billemont on 17/07/12.
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
//
#import <objc/runtime.h>
#import "MPAlgorithmV2.h"
#import "MPEntities.h"
@implementation MPAlgorithmV2
- (NSUInteger)version {
return 2;
}
- (BOOL)tryMigrateSite:(MPSiteEntity *)site explicit:(BOOL)explicit {
if (site.version != [self version] - 1)
// Only migrate from previous version.
return NO;
if (!explicit) {
if (site.type & MPSiteTypeClassGenerated && site.name.length != [site.name dataUsingEncoding:NSUTF8StringEncoding].length) {
// This migration requires explicit permission for types of the generated class.
site.requiresExplicitMigration = YES;
return NO;
}
}
// Apply migration.
site.requiresExplicitMigration = NO;
site.version = [self version];
return YES;
}
- (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key {
// Determine the seed whose bytes will be used for calculating a password
NSData *nameBytes = [name dataUsingEncoding:NSUTF8StringEncoding];
NSData *contextBytes = [context dataUsingEncoding:NSUTF8StringEncoding];
uint32_t ncounter = htonl( counter ), nnameLength = htonl( nameBytes.length ), ncontextLength = htonl( contextBytes.length );
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof( ncounter )];
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof( nnameLength )];
NSData *contextLengthBytes = [NSData dataWithBytes:&ncontextLength length:sizeof( ncontextLength )];
NSString *scope = [self scopeForVariant:variant];
NSData *scopeBytes = [scope dataUsingEncoding:NSUTF8StringEncoding];
trc( @"seed from: hmac-sha256(%@, %@ | %@ | %@ | %@)",
[[key keyID] encodeHex], scope, [nameLengthBytes encodeHex], name, [counterBytes encodeHex] );
NSData *seed = [[NSData dataByConcatenatingDatas:
scopeBytes,
nameLengthBytes,
nameBytes,
counterBytes,
context? contextLengthBytes: nil,
contextBytes,
nil]
hmacWith:PearlHashSHA256 key:key.keyData];
trc( @"seed is: %@", [seed encodeHex] );
const unsigned char *seedBytes = seed.bytes;
// Determine the cipher from the first seed byte.
NSAssert( [seed length], @"Missing seed." );
NSArray *typeCiphers = [self ciphersForType:type];
NSString *cipher = typeCiphers[seedBytes[0] % [typeCiphers count]];
trc( @"type %@ (%lu), ciphers: %@, selected: %@", [self nameOfType:type], (unsigned long)type, typeCiphers, cipher );
// Encode the content, character by character, using subsequent seed bytes and the cipher.
NSAssert( [seed length] >= [cipher length] + 1, @"Insufficient seed bytes to encode cipher." );
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
for (NSUInteger c = 0; c < [cipher length]; ++c) {
uint16_t keyByte = seedBytes[c + 1];
NSString *cipherClass = [cipher substringWithRange:NSMakeRange( c, 1 )];
NSString *cipherClassCharacters = [self charactersForCipherClass:cipherClass];
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange( keyByte % [cipherClassCharacters length], 1 )];
trc( @"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character );
[content appendString:character];
}
return content;
}
@end

View File

@ -36,7 +36,7 @@
@property(readonly) id<MPAlgorithm> algorithm; @property(readonly) id<MPAlgorithm> algorithm;
- (NSUInteger)use; - (NSUInteger)use;
- (BOOL)migrateExplicitly:(BOOL)explicit; - (BOOL)tryMigrateExplicitly:(BOOL)explicit;
- (NSString *)resolveLoginUsingKey:(MPKey *)key; - (NSString *)resolveLoginUsingKey:(MPKey *)key;
- (NSString *)resolvePasswordUsingKey:(MPKey *)key; - (NSString *)resolvePasswordUsingKey:(MPKey *)key;
- (void)resolveLoginUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result; - (void)resolveLoginUsingKey:(MPKey *)key result:(void ( ^ )(NSString *))result;

View File

@ -134,18 +134,20 @@
self.loginName, self.requiresExplicitMigration ); self.loginName, self.requiresExplicitMigration );
} }
- (BOOL)migrateExplicitly:(BOOL)explicit { - (BOOL)tryMigrateExplicitly:(BOOL)explicit {
while (self.version < MPAlgorithmDefaultVersion) while (self.version < MPAlgorithmDefaultVersion) {
if ([MPAlgorithmForVersion( self.version + 1 ) migrateSite:self explicit:explicit]) NSUInteger toVersion = self.version + 1;
inf( @"%@ migration to version: %ld succeeded for site: %@", if (![MPAlgorithmForVersion( toVersion ) tryMigrateSite:self explicit:explicit]) {
explicit? @"Explicit": @"Automatic", (long)self.version + 1, self );
else {
wrn( @"%@ migration to version: %ld failed for site: %@", wrn( @"%@ migration to version: %ld failed for site: %@",
explicit? @"Explicit": @"Automatic", (long)self.version + 1, self ); explicit? @"Explicit": @"Automatic", (long)toVersion, self );
return NO; return NO;
} }
inf( @"%@ migration to version: %ld succeeded for site: %@",
explicit? @"Explicit": @"Automatic", (long)toVersion, self );
}
return YES; return YES;
} }

View File

@ -317,7 +317,7 @@
- (IBAction)doUpgrade:(UIButton *)sender { - (IBAction)doUpgrade:(UIButton *)sender {
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
if (![[self siteInContext:context] migrateExplicitly:YES]) { if (![[self siteInContext:context] tryMigrateExplicitly:YES]) {
[PearlOverlay showTemporaryOverlayWithTitle:@"Couldn't Upgrade Site" dismissAfter:2]; [PearlOverlay showTemporaryOverlayWithTitle:@"Couldn't Upgrade Site" dismissAfter:2];
return; return;
} }

View File

@ -74,6 +74,18 @@
[self updatePasswords]; [self updatePasswords];
} }
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserInContext:context];
if (![MPAlgorithmDefault tryMigrateUser:activeUser inContext:context])
[PearlOverlay showTemporaryOverlayWithTitle:@"Some Sites Need Upgrade" dismissAfter:2];
[context saveToStore];
}];
}
- (void)viewWillDisappear:(BOOL)animated { - (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated]; [super viewWillDisappear:animated];

View File

@ -35,6 +35,7 @@
93D3992FA1546E01F498F665 /* PearlNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D398567FD02DB2647B8CF3 /* PearlNavigationController.h */; }; 93D3992FA1546E01F498F665 /* PearlNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D398567FD02DB2647B8CF3 /* PearlNavigationController.h */; };
93D399433EA75E50656040CB /* Twitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93D394077F8FAB8167647187 /* Twitter.framework */; }; 93D399433EA75E50656040CB /* Twitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93D394077F8FAB8167647187 /* Twitter.framework */; };
93D399D7E08A142776A74CB8 /* MPOverlayViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D395105935859D71679931 /* MPOverlayViewController.m */; }; 93D399D7E08A142776A74CB8 /* MPOverlayViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D395105935859D71679931 /* MPOverlayViewController.m */; };
93D39A27F2506C6FEEF9C588 /* MPAlgorithmV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D399A8E3181B442D347CD7 /* MPAlgorithmV2.m */; };
93D39A53D76CA70786423458 /* UICollectionView+PearlReloadFromArray.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D39246FC21C6E63E35D615 /* UICollectionView+PearlReloadFromArray.h */; }; 93D39A53D76CA70786423458 /* UICollectionView+PearlReloadFromArray.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D39246FC21C6E63E35D615 /* UICollectionView+PearlReloadFromArray.h */; };
93D39A5FF670957C0AF8298D /* MPPasswordCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39DEA995041A13DC9CAF7 /* MPPasswordCell.m */; }; 93D39A5FF670957C0AF8298D /* MPPasswordCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39DEA995041A13DC9CAF7 /* MPPasswordCell.m */; };
93D39A8EA1C49CE43B63F47B /* PearlUICollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39D8A953779B35403AF6E /* PearlUICollectionView.m */; }; 93D39A8EA1C49CE43B63F47B /* PearlUICollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39D8A953779B35403AF6E /* PearlUICollectionView.m */; };
@ -448,12 +449,14 @@
93D3995B1D4DCE5A30D882BA /* MPCoachmarkViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPCoachmarkViewController.m; sourceTree = "<group>"; }; 93D3995B1D4DCE5A30D882BA /* MPCoachmarkViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPCoachmarkViewController.m; sourceTree = "<group>"; };
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>"; };
93D399A8E3181B442D347CD7 /* MPAlgorithmV2.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPAlgorithmV2.m; 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>"; }; 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>"; };
93D39A4759186F6D2D34AA6B /* PearlSizedTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlSizedTextView.h; sourceTree = "<group>"; }; 93D39A4759186F6D2D34AA6B /* PearlSizedTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlSizedTextView.h; sourceTree = "<group>"; };
93D39A813CA9D7E192261ED2 /* MPFixable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPFixable.m; sourceTree = "<group>"; }; 93D39A813CA9D7E192261ED2 /* MPFixable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPFixable.m; sourceTree = "<group>"; };
93D39A97A7D48CB3B784194D /* MPAlgorithmV2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPAlgorithmV2.h; sourceTree = "<group>"; };
93D39AA10CD00D05937671B1 /* UITextView+PearlAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITextView+PearlAttributes.h"; sourceTree = "<group>"; }; 93D39AA10CD00D05937671B1 /* UITextView+PearlAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITextView+PearlAttributes.h"; 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>"; };
@ -2343,6 +2346,8 @@
93D39A813CA9D7E192261ED2 /* MPFixable.m */, 93D39A813CA9D7E192261ED2 /* MPFixable.m */,
93D394C78C7B879C9AD9152C /* MPAppDelegate_InApp.m */, 93D394C78C7B879C9AD9152C /* MPAppDelegate_InApp.m */,
93D39CECA10BCCB0BA581BF1 /* MPAppDelegate_InApp.h */, 93D39CECA10BCCB0BA581BF1 /* MPAppDelegate_InApp.h */,
93D399A8E3181B442D347CD7 /* MPAlgorithmV2.m */,
93D39A97A7D48CB3B784194D /* MPAlgorithmV2.h */,
); );
name = ObjC; name = ObjC;
path = ..; path = ..;
@ -3325,6 +3330,7 @@
93D39D38356F59DBEF934D70 /* MPAppDelegate_InApp.m in Sources */, 93D39D38356F59DBEF934D70 /* MPAppDelegate_InApp.m in Sources */,
93D390C1B93F9D3AE37DD0A5 /* MPAnswersViewController.m in Sources */, 93D390C1B93F9D3AE37DD0A5 /* MPAnswersViewController.m in Sources */,
93D399D7E08A142776A74CB8 /* MPOverlayViewController.m in Sources */, 93D399D7E08A142776A74CB8 /* MPOverlayViewController.m in Sources */,
93D39A27F2506C6FEEF9C588 /* MPAlgorithmV2.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 254 KiB