From 61b4ea4525ac3b9503a287e7d9348411491b7653 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Thu, 14 Jan 2016 09:58:04 -0500 Subject: [PATCH] Finish TouchID support. --- External/Pearl | 2 +- MasterPassword/ObjC/MPAppDelegate_Key.m | 89 +++++++++---------- MasterPassword/ObjC/MPKey.h | 10 ++- MasterPassword/ObjC/MPKey.m | 6 +- MasterPassword/ObjC/iOS/MPAvatarCell.h | 2 +- MasterPassword/ObjC/iOS/MPAvatarCell.m | 4 +- .../ObjC/iOS/MPPreferencesViewController.m | 34 ++++--- .../ObjC/iOS/MPUsersViewController.m | 50 +++++++---- 8 files changed, 121 insertions(+), 76 deletions(-) diff --git a/External/Pearl b/External/Pearl index 2642d720..1c02f689 160000 --- a/External/Pearl +++ b/External/Pearl @@ -1 +1 @@ -Subproject commit 2642d720cc874635c336406a01f1e8d2ee5bcb09 +Subproject commit 1c02f68934e0a32adf8f4901fe9e77cff43d93a3 diff --git a/MasterPassword/ObjC/MPAppDelegate_Key.m b/MasterPassword/ObjC/MPAppDelegate_Key.m index 8963bfff..28365820 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Key.m +++ b/MasterPassword/ObjC/MPAppDelegate_Key.m @@ -18,64 +18,57 @@ @implementation MPAppDelegate_Shared(Key) -static NSDictionary *keyQuery(MPUserEntity *user, BOOL newItem) { +static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigin *keyOrigin) { + + if (user.touchID && kSecUseOperationPrompt) { + if (keyOrigin) + *keyOrigin = MPKeyOriginKeyChainBiometric; - if (user.touchID && &SecAccessControlCreateWithFlags) { CFErrorRef acError = NULL; - SecAccessControlRef accessControl = SecAccessControlCreateWithFlags( nil, + SecAccessControlRef accessControl = SecAccessControlCreateWithFlags( kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlTouchIDCurrentSet, &acError ); if (!accessControl || acError) err( @"Could not use TouchID on this device: %@", acError ); - else { - LAContext *context = [LAContext new]; - dispatch_group_t waitGroup = dispatch_group_create(); - dispatch_group_enter( waitGroup ); - __block BOOL contextSuccess = NO; - __block NSError *contextError = nil; - [context evaluateAccessControl:accessControl - operation:newItem? LAAccessControlOperationCreateItem: LAAccessControlOperationUseItem - localizedReason:@"Moo" - reply:^(BOOL success, NSError *error) { - contextSuccess = success; - contextError = error; - dispatch_group_leave( waitGroup ); - }]; - dispatch_group_wait( waitGroup, DISPATCH_TIME_FOREVER ); - - if (!contextSuccess || contextError) - err( @"TouchID authentication failed: %@", contextError ); - - else - return [PearlKeyChain createQueryForClass:kSecClassGenericPassword - attributes:@{ - (__bridge id)kSecAttrService : @"Saved Master Password", - (__bridge id)kSecAttrAccount : user.name?: @"", - (__bridge id)kSecUseAuthenticationUI : (__bridge id)kSecUseAuthenticationUIAllow, - (__bridge id)kSecAttrAccessControl : (__bridge id)accessControl, - } - matches:nil]; - } + else + return [PearlKeyChain createQueryForClass:kSecClassGenericPassword + attributes:@{ + (__bridge id)kSecAttrService : @"Saved Master Password", + (__bridge id)kSecAttrAccount : user.name?: @"", + (__bridge id)kSecAttrAccessControl : (__bridge id)accessControl, + (__bridge id)kSecUseAuthenticationUI : (__bridge id)kSecUseAuthenticationUIAllow, + (__bridge id)kSecUseOperationPrompt : + strf( @"Access %@'s master password.", user.name ), + } + matches:nil]; } + if (keyOrigin) + *keyOrigin = MPKeyOriginKeyChain; + return [PearlKeyChain createQueryForClass:kSecClassGenericPassword attributes:@{ - (__bridge id)kSecAttrService : @"Saved Master Password", - (__bridge id)kSecAttrAccount : user.name?: @"", + (__bridge id)kSecAttrService : @"Saved Master Password", + (__bridge id)kSecAttrAccount : user.name?: @"", +#if TARGET_OS_IPHONE + (__bridge id)kSecAttrAccessible : (__bridge id)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly?: kSecAttrAccessibleWhenUnlockedThisDeviceOnly), +#endif } matches:nil]; } - (MPKey *)loadSavedKeyFor:(MPUserEntity *)user { - NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user, NO )]; + MPKeyOrigin keyOrigin; + NSDictionary *keyQuery = createKeyQuery( user, NO, &keyOrigin ); + NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery]; if (!keyData) { inf( @"No key found in keychain for user: %@", user.userID ); return nil; } inf( @"Found key in keychain for user: %@", user.userID ); - return [[MPKey alloc] initForFullName:user.name withKeyData:keyData forAlgorithm:user.algorithm]; + return [[MPKey alloc] initForFullName:user.name withKeyData:keyData forAlgorithm:user.algorithm keyOrigin:keyOrigin]; } - (void)storeSavedKeyFor:(MPUserEntity *)user { @@ -83,19 +76,16 @@ static NSDictionary *keyQuery(MPUserEntity *user, BOOL newItem) { if (user.saveKey) { inf( @"Saving key in keychain for user: %@", user.userID ); - [PearlKeyChain addOrUpdateItemForQuery:keyQuery( user, YES ) + [PearlKeyChain addOrUpdateItemForQuery:createKeyQuery( user, YES, nil ) withAttributes:@{ - (__bridge id)kSecValueData : [self.key keyDataForAlgorithm:user.algorithm], -#if TARGET_OS_IPHONE - (__bridge id)kSecAttrAccessible : (__bridge id)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly?: kSecAttrAccessibleWhenUnlockedThisDeviceOnly), -#endif + (__bridge id)kSecValueData : [self.key keyDataForAlgorithm:user.algorithm], }]; } } - (void)forgetSavedKeyFor:(MPUserEntity *)user { - OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery( user, NO )]; + OSStatus result = [PearlKeyChain deleteItemForQuery:createKeyQuery( user, NO, nil )]; if (result == noErr) { inf( @"Removed key from keychain for user: %@", user.userID ); @@ -114,8 +104,7 @@ static NSDictionary *keyQuery(MPUserEntity *user, BOOL newItem) { - (BOOL)signInAsUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc usingMasterPassword:(NSString *)password { - if (password) - NSAssert( ![NSThread isMainThread], @"Computing key must not happen from the main thread." ); + NSAssert( ![NSThread isMainThread], @"Authentication should not happen on the main thread." ); if (!user) return NO; @@ -179,7 +168,17 @@ static NSDictionary *keyQuery(MPUserEntity *user, BOOL newItem) { } self.key = tryKey; - [self storeSavedKeyFor:user]; + + // Update the key chain if necessary. + switch (self.key.origin) { + case MPKeyOriginMasterPassword: + [self storeSavedKeyFor:user]; + break; + + case MPKeyOriginKeyChain: + case MPKeyOriginKeyChainBiometric: + break; + } } @try { diff --git a/MasterPassword/ObjC/MPKey.h b/MasterPassword/ObjC/MPKey.h index 084c5d17..f145102d 100644 --- a/MasterPassword/ObjC/MPKey.h +++ b/MasterPassword/ObjC/MPKey.h @@ -20,12 +20,20 @@ @protocol MPAlgorithm; +typedef NS_ENUM(NSUInteger, MPKeyOrigin) { + MPKeyOriginMasterPassword, + MPKeyOriginKeyChain, + MPKeyOriginKeyChainBiometric, +}; + @interface MPKey : NSObject @property(nonatomic, readonly) NSString *fullName; +@property(nonatomic, readonly) MPKeyOrigin origin; - (instancetype)initForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword; -- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData forAlgorithm:(id)algorithm; +- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData + forAlgorithm:(id)algorithm keyOrigin:(MPKeyOrigin)origin; - (NSData *)keyIDForAlgorithm:(id)algorithm; - (NSData *)keyDataForAlgorithm:(id)algorithm; diff --git a/MasterPassword/ObjC/MPKey.m b/MasterPassword/ObjC/MPKey.m index cdef3611..b34ef369 100644 --- a/MasterPassword/ObjC/MPKey.m +++ b/MasterPassword/ObjC/MPKey.m @@ -20,6 +20,7 @@ @interface MPKey() @property(nonatomic) NSString *fullName; +@property(nonatomic) MPKeyOrigin origin; @property(nonatomic) NSString *masterPassword; @end @@ -35,16 +36,19 @@ _keyCache = [NSCache new]; self.fullName = fullName; + self.origin = MPKeyOriginMasterPassword; self.masterPassword = masterPassword; return self; } -- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData forAlgorithm:(id)algorithm { +- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData + forAlgorithm:(id)algorithm keyOrigin:(MPKeyOrigin)origin { if (!(self = [self initForFullName:fullName withMasterPassword:nil])) return nil; + self.origin = origin; [_keyCache setObject:keyData forKey:algorithm]; return self; diff --git a/MasterPassword/ObjC/iOS/MPAvatarCell.h b/MasterPassword/ObjC/iOS/MPAvatarCell.h index b3429acb..e51fbf3c 100644 --- a/MasterPassword/ObjC/iOS/MPAvatarCell.h +++ b/MasterPassword/ObjC/iOS/MPAvatarCell.h @@ -33,7 +33,7 @@ typedef NS_ENUM(NSUInteger, MPAvatarMode) { @interface MPAvatarCell : UICollectionViewCell @property (copy, nonatomic) NSString *name; -@property (assign, nonatomic) long avatar; +@property (assign, nonatomic) NSUInteger avatar; @property (assign, nonatomic) MPAvatarMode mode; @property (assign, nonatomic) CGFloat visibility; @property (assign, nonatomic) BOOL spinnerActive; diff --git a/MasterPassword/ObjC/iOS/MPAvatarCell.m b/MasterPassword/ObjC/iOS/MPAvatarCell.m index 47acf2aa..955483f6 100644 --- a/MasterPassword/ObjC/iOS/MPAvatarCell.m +++ b/MasterPassword/ObjC/iOS/MPAvatarCell.m @@ -111,7 +111,7 @@ const long MPAvatarAdd = 10000; #pragma mark - Properties -- (void)setAvatar:(long)avatar { +- (void)setAvatar:(NSUInteger)avatar { _avatar = avatar == MPAvatarAdd? MPAvatarAdd: (avatar + MPAvatarCount) % MPAvatarCount; @@ -121,7 +121,7 @@ const long MPAvatarAdd = 10000; _newUser = YES; } else - self.avatarImageView.image = [UIImage imageNamed:strf( @"avatar-%ld", _avatar )]; + self.avatarImageView.image = [UIImage imageNamed:strf( @"avatar-%lu", (unsigned long)_avatar )]; } - (NSString *)name { diff --git a/MasterPassword/ObjC/iOS/MPPreferencesViewController.m b/MasterPassword/ObjC/iOS/MPPreferencesViewController.m index f2ba845a..17958aaf 100644 --- a/MasterPassword/ObjC/iOS/MPPreferencesViewController.m +++ b/MasterPassword/ObjC/iOS/MPPreferencesViewController.m @@ -35,14 +35,20 @@ if (![[NSUserDefaults standardUserDefaults] synchronize]) wrn( @"Couldn't synchronize after preferences appearance." ); + self.tableView.contentInset = UIEdgeInsetsMake( 64, 0, 49, 0 ); + + [self reload]; +} + +- (void)reload { + MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForMainThread]; self.generatedTypeControl.selectedSegmentIndex = [self generatedSegmentIndexForType:activeUser.defaultType]; self.storedTypeControl.selectedSegmentIndex = [self storedSegmentIndexForType:activeUser.defaultType]; - self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%ld", (long)activeUser.avatar )]; + self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%lu", (unsigned long)activeUser.avatar )]; self.savePasswordSwitch.on = activeUser.saveKey; self.touchIDSwitch.on = activeUser.touchID; - - self.tableView.contentInset = UIEdgeInsetsMake( 64, 0, 49, 0 ); + self.touchIDSwitch.enabled = self.savePasswordSwitch.on; } #pragma mark - UITableViewDelegate @@ -97,6 +103,10 @@ else [[MPiOSAppDelegate get] forgetSavedKeyFor:activeUser]; [context saveToStore]; + + PearlMainQueue(^{ + [self reload]; + }); }]; if (sender == self.touchIDSwitch) @@ -107,6 +117,10 @@ else [[MPiOSAppDelegate get] forgetSavedKeyFor:activeUser]; [context saveToStore]; + + PearlMainQueue( ^{ + [self reload]; + } ); }]; if (sender == self.generatedTypeControl || sender == self.storedTypeControl) { @@ -115,13 +129,13 @@ else if (sender == self.storedTypeControl) self.generatedTypeControl.selectedSegmentIndex = -1; + MPSiteType defaultType = [self typeForSelectedSegment]; [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { - MPSiteType defaultType = [[MPiOSAppDelegate get] activeUserInContext:context].defaultType = [self typeForSelectedSegment]; + [[MPiOSAppDelegate get] activeUserInContext:context].defaultType = defaultType; [context saveToStore]; PearlMainQueue( ^{ - self.generatedTypeControl.selectedSegmentIndex = [self generatedSegmentIndexForType:defaultType]; - self.storedTypeControl.selectedSegmentIndex = [self storedSegmentIndexForType:defaultType]; + [self reload]; } ); }]; } @@ -134,9 +148,9 @@ activeUser.avatar = (activeUser.avatar - 1 + MPAvatarCount) % MPAvatarCount; [context saveToStore]; - long avatar = activeUser.avatar; + NSUInteger avatar = activeUser.avatar; PearlMainQueue( ^{ - self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%ld", avatar )]; + self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%lu", (unsigned long)avatar )]; } ); }]; } @@ -148,9 +162,9 @@ activeUser.avatar = (activeUser.avatar + 1 + MPAvatarCount) % MPAvatarCount; [context saveToStore]; - long avatar = activeUser.avatar; + NSUInteger avatar = activeUser.avatar; PearlMainQueue( ^{ - self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%ld", avatar )]; + self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%lu", (unsigned long)avatar )]; } ); }]; } diff --git a/MasterPassword/ObjC/iOS/MPUsersViewController.m b/MasterPassword/ObjC/iOS/MPUsersViewController.m index f5a21501..e8464b8e 100644 --- a/MasterPassword/ObjC/iOS/MPUsersViewController.m +++ b/MasterPassword/ObjC/iOS/MPUsersViewController.m @@ -147,12 +147,13 @@ typedef NS_ENUM( NSUInteger, MPActiveUserState ) { case MPActiveUserStateLogin: { self.entryField.enabled = NO; [self selectedAvatar].spinnerActive = YES; - [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + NSString *masterPassword = self.entryField.text; + if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { BOOL signedIn = NO, isNew = NO; MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew]; if (!isNew && user) signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context - usingMasterPassword:self.entryField.text]; + usingMasterPassword:masterPassword]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.entryField.text = @""; @@ -165,7 +166,10 @@ typedef NS_ENUM( NSUInteger, MPActiveUserState ) { return; } }]; - }]; + }]) { + self.entryField.enabled = YES; + [self selectedAvatar].spinnerActive = NO; + } break; } case MPActiveUserStateUserName: { @@ -209,21 +213,24 @@ typedef NS_ENUM( NSUInteger, MPActiveUserState ) { self.entryField.enabled = NO; MPAvatarCell *avatarCell = [self selectedAvatar]; avatarCell.spinnerActive = YES; + NSUInteger newUserAvatar = avatarCell.avatar; + NSString *newUserName = avatarCell.name; if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { BOOL isNew = NO; MPUserEntity *user = [self userForAvatar:avatarCell inContext:context isNew:&isNew]; if (isNew) { user = [MPUserEntity insertNewObjectInContext:context]; user.algorithm = MPAlgorithmDefault; - user.avatar = avatarCell.avatar; - user.name = avatarCell.name; + user.avatar = newUserAvatar; + user.name = newUserName; } - BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context usingMasterPassword:masterPassword]; + BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context + usingMasterPassword:masterPassword]; PearlMainQueue( ^{ self.entryField.text = @""; self.entryField.enabled = YES; - [self selectedAvatar].spinnerActive = NO; + avatarCell.spinnerActive = NO; if (!signedIn) { // Sign in failed, shouldn't happen for a new user. @@ -232,8 +239,10 @@ typedef NS_ENUM( NSUInteger, MPActiveUserState ) { return; } } ); - }]) + }]) { + self.entryField.enabled = YES; avatarCell.spinnerActive = NO; + } break; } @@ -356,17 +365,28 @@ referenceSizeForFooterInSection:(NSInteger)section { self.activeUserState = MPActiveUserStateLogin; self.entryField.enabled = NO; - [self selectedAvatar].spinnerActive = YES; - BOOL signedIn = NO; - if (!isNew && mainUser) - signedIn = [[MPiOSAppDelegate get] signInAsUser:mainUser saveInContext:mainContext usingMasterPassword:nil]; + MPAvatarCell *userAvatar = [self selectedAvatar]; + userAvatar.spinnerActive = YES; + if (!isNew && mainUser && [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPUserEntity *user = [MPUserEntity existingObjectWithID:mainUser.objectID inContext:context]; + BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:mainContext usingMasterPassword:nil]; + + PearlMainQueue(^{ + self.entryField.text = @""; + self.entryField.enabled = YES; + userAvatar.spinnerActive = NO; + + if (!signedIn) + [self.entryField becomeFirstResponder]; + }); + }]) + return; self.entryField.text = @""; self.entryField.enabled = YES; - [self selectedAvatar].spinnerActive = NO; + userAvatar.spinnerActive = NO; - if (!signedIn) - [self.entryField becomeFirstResponder]; + [self.entryField becomeFirstResponder]; } } }