2
0

Finish TouchID support.

This commit is contained in:
Maarten Billemont 2016-01-14 09:58:04 -05:00
parent a42edec918
commit 61b4ea4525
8 changed files with 121 additions and 76 deletions

2
External/Pearl vendored

@ -1 +1 @@
Subproject commit 2642d720cc874635c336406a01f1e8d2ee5bcb09 Subproject commit 1c02f68934e0a32adf8f4901fe9e77cff43d93a3

View File

@ -18,64 +18,57 @@
@implementation MPAppDelegate_Shared(Key) @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; CFErrorRef acError = NULL;
SecAccessControlRef accessControl = SecAccessControlCreateWithFlags( nil, SecAccessControlRef accessControl = SecAccessControlCreateWithFlags( kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlTouchIDCurrentSet, &acError ); kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlTouchIDCurrentSet, &acError );
if (!accessControl || acError) if (!accessControl || acError)
err( @"Could not use TouchID on this device: %@", acError ); err( @"Could not use TouchID on this device: %@", acError );
else { else
LAContext *context = [LAContext new]; return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
dispatch_group_t waitGroup = dispatch_group_create(); attributes:@{
dispatch_group_enter( waitGroup ); (__bridge id)kSecAttrService : @"Saved Master Password",
__block BOOL contextSuccess = NO; (__bridge id)kSecAttrAccount : user.name?: @"",
__block NSError *contextError = nil; (__bridge id)kSecAttrAccessControl : (__bridge id)accessControl,
[context evaluateAccessControl:accessControl (__bridge id)kSecUseAuthenticationUI : (__bridge id)kSecUseAuthenticationUIAllow,
operation:newItem? LAAccessControlOperationCreateItem: LAAccessControlOperationUseItem (__bridge id)kSecUseOperationPrompt :
localizedReason:@"Moo" strf( @"Access %@'s master password.", user.name ),
reply:^(BOOL success, NSError *error) { }
contextSuccess = success; matches:nil];
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];
}
} }
if (keyOrigin)
*keyOrigin = MPKeyOriginKeyChain;
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 : user.name?: @"", (__bridge id)kSecAttrAccount : user.name?: @"",
#if TARGET_OS_IPHONE
(__bridge id)kSecAttrAccessible : (__bridge id)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly?: kSecAttrAccessibleWhenUnlockedThisDeviceOnly),
#endif
} }
matches:nil]; matches:nil];
} }
- (MPKey *)loadSavedKeyFor:(MPUserEntity *)user { - (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) { if (!keyData) {
inf( @"No key found in keychain for user: %@", user.userID ); inf( @"No key found in keychain for user: %@", user.userID );
return nil; return nil;
} }
inf( @"Found key in keychain for user: %@", user.userID ); 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 { - (void)storeSavedKeyFor:(MPUserEntity *)user {
@ -83,19 +76,16 @@ static NSDictionary *keyQuery(MPUserEntity *user, BOOL newItem) {
if (user.saveKey) { if (user.saveKey) {
inf( @"Saving key in keychain for user: %@", user.userID ); inf( @"Saving key in keychain for user: %@", user.userID );
[PearlKeyChain addOrUpdateItemForQuery:keyQuery( user, YES ) [PearlKeyChain addOrUpdateItemForQuery:createKeyQuery( user, YES, nil )
withAttributes:@{ withAttributes:@{
(__bridge id)kSecValueData : [self.key keyDataForAlgorithm:user.algorithm], (__bridge id)kSecValueData : [self.key keyDataForAlgorithm:user.algorithm],
#if TARGET_OS_IPHONE
(__bridge id)kSecAttrAccessible : (__bridge id)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly?: kSecAttrAccessibleWhenUnlockedThisDeviceOnly),
#endif
}]; }];
} }
} }
- (void)forgetSavedKeyFor:(MPUserEntity *)user { - (void)forgetSavedKeyFor:(MPUserEntity *)user {
OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery( user, NO )]; OSStatus result = [PearlKeyChain deleteItemForQuery:createKeyQuery( user, NO, nil )];
if (result == noErr) { if (result == noErr) {
inf( @"Removed key from keychain for user: %@", user.userID ); 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 { - (BOOL)signInAsUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc usingMasterPassword:(NSString *)password {
if (password) NSAssert( ![NSThread isMainThread], @"Authentication should not happen on the main thread." );
NSAssert( ![NSThread isMainThread], @"Computing key must not happen from the main thread." );
if (!user) if (!user)
return NO; return NO;
@ -179,7 +168,17 @@ static NSDictionary *keyQuery(MPUserEntity *user, BOOL newItem) {
} }
self.key = tryKey; 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 { @try {

View File

@ -20,12 +20,20 @@
@protocol MPAlgorithm; @protocol MPAlgorithm;
typedef NS_ENUM(NSUInteger, MPKeyOrigin) {
MPKeyOriginMasterPassword,
MPKeyOriginKeyChain,
MPKeyOriginKeyChainBiometric,
};
@interface MPKey : NSObject @interface MPKey : NSObject
@property(nonatomic, readonly) NSString *fullName; @property(nonatomic, readonly) NSString *fullName;
@property(nonatomic, readonly) MPKeyOrigin origin;
- (instancetype)initForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword; - (instancetype)initForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword;
- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData forAlgorithm:(id<MPAlgorithm>)algorithm; - (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData
forAlgorithm:(id<MPAlgorithm>)algorithm keyOrigin:(MPKeyOrigin)origin;
- (NSData *)keyIDForAlgorithm:(id<MPAlgorithm>)algorithm; - (NSData *)keyIDForAlgorithm:(id<MPAlgorithm>)algorithm;
- (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm; - (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm;

View File

@ -20,6 +20,7 @@
@interface MPKey() @interface MPKey()
@property(nonatomic) NSString *fullName; @property(nonatomic) NSString *fullName;
@property(nonatomic) MPKeyOrigin origin;
@property(nonatomic) NSString *masterPassword; @property(nonatomic) NSString *masterPassword;
@end @end
@ -35,16 +36,19 @@
_keyCache = [NSCache new]; _keyCache = [NSCache new];
self.fullName = fullName; self.fullName = fullName;
self.origin = MPKeyOriginMasterPassword;
self.masterPassword = masterPassword; self.masterPassword = masterPassword;
return self; return self;
} }
- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData forAlgorithm:(id<MPAlgorithm>)algorithm { - (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData
forAlgorithm:(id<MPAlgorithm>)algorithm keyOrigin:(MPKeyOrigin)origin {
if (!(self = [self initForFullName:fullName withMasterPassword:nil])) if (!(self = [self initForFullName:fullName withMasterPassword:nil]))
return nil; return nil;
self.origin = origin;
[_keyCache setObject:keyData forKey:algorithm]; [_keyCache setObject:keyData forKey:algorithm];
return self; return self;

View File

@ -33,7 +33,7 @@ typedef NS_ENUM(NSUInteger, MPAvatarMode) {
@interface MPAvatarCell : UICollectionViewCell @interface MPAvatarCell : UICollectionViewCell
@property (copy, nonatomic) NSString *name; @property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) long avatar; @property (assign, nonatomic) NSUInteger avatar;
@property (assign, nonatomic) MPAvatarMode mode; @property (assign, nonatomic) MPAvatarMode mode;
@property (assign, nonatomic) CGFloat visibility; @property (assign, nonatomic) CGFloat visibility;
@property (assign, nonatomic) BOOL spinnerActive; @property (assign, nonatomic) BOOL spinnerActive;

View File

@ -111,7 +111,7 @@ const long MPAvatarAdd = 10000;
#pragma mark - Properties #pragma mark - Properties
- (void)setAvatar:(long)avatar { - (void)setAvatar:(NSUInteger)avatar {
_avatar = avatar == MPAvatarAdd? MPAvatarAdd: (avatar + MPAvatarCount) % MPAvatarCount; _avatar = avatar == MPAvatarAdd? MPAvatarAdd: (avatar + MPAvatarCount) % MPAvatarCount;
@ -121,7 +121,7 @@ const long MPAvatarAdd = 10000;
_newUser = YES; _newUser = YES;
} }
else else
self.avatarImageView.image = [UIImage imageNamed:strf( @"avatar-%ld", _avatar )]; self.avatarImageView.image = [UIImage imageNamed:strf( @"avatar-%lu", (unsigned long)_avatar )];
} }
- (NSString *)name { - (NSString *)name {

View File

@ -35,14 +35,20 @@
if (![[NSUserDefaults standardUserDefaults] synchronize]) if (![[NSUserDefaults standardUserDefaults] synchronize])
wrn( @"Couldn't synchronize after preferences appearance." ); wrn( @"Couldn't synchronize after preferences appearance." );
self.tableView.contentInset = UIEdgeInsetsMake( 64, 0, 49, 0 );
[self reload];
}
- (void)reload {
MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForMainThread]; MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForMainThread];
self.generatedTypeControl.selectedSegmentIndex = [self generatedSegmentIndexForType:activeUser.defaultType]; self.generatedTypeControl.selectedSegmentIndex = [self generatedSegmentIndexForType:activeUser.defaultType];
self.storedTypeControl.selectedSegmentIndex = [self storedSegmentIndexForType: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.savePasswordSwitch.on = activeUser.saveKey;
self.touchIDSwitch.on = activeUser.touchID; self.touchIDSwitch.on = activeUser.touchID;
self.touchIDSwitch.enabled = self.savePasswordSwitch.on;
self.tableView.contentInset = UIEdgeInsetsMake( 64, 0, 49, 0 );
} }
#pragma mark - UITableViewDelegate #pragma mark - UITableViewDelegate
@ -97,6 +103,10 @@
else else
[[MPiOSAppDelegate get] forgetSavedKeyFor:activeUser]; [[MPiOSAppDelegate get] forgetSavedKeyFor:activeUser];
[context saveToStore]; [context saveToStore];
PearlMainQueue(^{
[self reload];
});
}]; }];
if (sender == self.touchIDSwitch) if (sender == self.touchIDSwitch)
@ -107,6 +117,10 @@
else else
[[MPiOSAppDelegate get] forgetSavedKeyFor:activeUser]; [[MPiOSAppDelegate get] forgetSavedKeyFor:activeUser];
[context saveToStore]; [context saveToStore];
PearlMainQueue( ^{
[self reload];
} );
}]; }];
if (sender == self.generatedTypeControl || sender == self.storedTypeControl) { if (sender == self.generatedTypeControl || sender == self.storedTypeControl) {
@ -115,13 +129,13 @@
else if (sender == self.storedTypeControl) else if (sender == self.storedTypeControl)
self.generatedTypeControl.selectedSegmentIndex = -1; self.generatedTypeControl.selectedSegmentIndex = -1;
MPSiteType defaultType = [self typeForSelectedSegment];
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteType defaultType = [[MPiOSAppDelegate get] activeUserInContext:context].defaultType = [self typeForSelectedSegment]; [[MPiOSAppDelegate get] activeUserInContext:context].defaultType = defaultType;
[context saveToStore]; [context saveToStore];
PearlMainQueue( ^{ PearlMainQueue( ^{
self.generatedTypeControl.selectedSegmentIndex = [self generatedSegmentIndexForType:defaultType]; [self reload];
self.storedTypeControl.selectedSegmentIndex = [self storedSegmentIndexForType:defaultType];
} ); } );
}]; }];
} }
@ -134,9 +148,9 @@
activeUser.avatar = (activeUser.avatar - 1 + MPAvatarCount) % MPAvatarCount; activeUser.avatar = (activeUser.avatar - 1 + MPAvatarCount) % MPAvatarCount;
[context saveToStore]; [context saveToStore];
long avatar = activeUser.avatar; NSUInteger avatar = activeUser.avatar;
PearlMainQueue( ^{ 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; activeUser.avatar = (activeUser.avatar + 1 + MPAvatarCount) % MPAvatarCount;
[context saveToStore]; [context saveToStore];
long avatar = activeUser.avatar; NSUInteger avatar = activeUser.avatar;
PearlMainQueue( ^{ PearlMainQueue( ^{
self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%ld", avatar )]; self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%lu", (unsigned long)avatar )];
} ); } );
}]; }];
} }

View File

@ -147,12 +147,13 @@ typedef NS_ENUM( NSUInteger, MPActiveUserState ) {
case MPActiveUserStateLogin: { case MPActiveUserStateLogin: {
self.entryField.enabled = NO; self.entryField.enabled = NO;
[self selectedAvatar].spinnerActive = YES; [self selectedAvatar].spinnerActive = YES;
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { NSString *masterPassword = self.entryField.text;
if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
BOOL signedIn = NO, isNew = NO; BOOL signedIn = NO, isNew = NO;
MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew]; MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew];
if (!isNew && user) if (!isNew && user)
signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context
usingMasterPassword:self.entryField.text]; usingMasterPassword:masterPassword];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.entryField.text = @""; self.entryField.text = @"";
@ -165,7 +166,10 @@ typedef NS_ENUM( NSUInteger, MPActiveUserState ) {
return; return;
} }
}]; }];
}]; }]) {
self.entryField.enabled = YES;
[self selectedAvatar].spinnerActive = NO;
}
break; break;
} }
case MPActiveUserStateUserName: { case MPActiveUserStateUserName: {
@ -209,21 +213,24 @@ typedef NS_ENUM( NSUInteger, MPActiveUserState ) {
self.entryField.enabled = NO; self.entryField.enabled = NO;
MPAvatarCell *avatarCell = [self selectedAvatar]; MPAvatarCell *avatarCell = [self selectedAvatar];
avatarCell.spinnerActive = YES; avatarCell.spinnerActive = YES;
NSUInteger newUserAvatar = avatarCell.avatar;
NSString *newUserName = avatarCell.name;
if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
BOOL isNew = NO; BOOL isNew = NO;
MPUserEntity *user = [self userForAvatar:avatarCell inContext:context isNew:&isNew]; MPUserEntity *user = [self userForAvatar:avatarCell inContext:context isNew:&isNew];
if (isNew) { if (isNew) {
user = [MPUserEntity insertNewObjectInContext:context]; user = [MPUserEntity insertNewObjectInContext:context];
user.algorithm = MPAlgorithmDefault; user.algorithm = MPAlgorithmDefault;
user.avatar = avatarCell.avatar; user.avatar = newUserAvatar;
user.name = avatarCell.name; user.name = newUserName;
} }
BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context usingMasterPassword:masterPassword]; BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context
usingMasterPassword:masterPassword];
PearlMainQueue( ^{ PearlMainQueue( ^{
self.entryField.text = @""; self.entryField.text = @"";
self.entryField.enabled = YES; self.entryField.enabled = YES;
[self selectedAvatar].spinnerActive = NO; avatarCell.spinnerActive = NO;
if (!signedIn) { if (!signedIn) {
// Sign in failed, shouldn't happen for a new user. // Sign in failed, shouldn't happen for a new user.
@ -232,8 +239,10 @@ typedef NS_ENUM( NSUInteger, MPActiveUserState ) {
return; return;
} }
} ); } );
}]) }]) {
self.entryField.enabled = YES;
avatarCell.spinnerActive = NO; avatarCell.spinnerActive = NO;
}
break; break;
} }
@ -356,17 +365,28 @@ referenceSizeForFooterInSection:(NSInteger)section {
self.activeUserState = MPActiveUserStateLogin; self.activeUserState = MPActiveUserStateLogin;
self.entryField.enabled = NO; self.entryField.enabled = NO;
[self selectedAvatar].spinnerActive = YES; MPAvatarCell *userAvatar = [self selectedAvatar];
BOOL signedIn = NO; userAvatar.spinnerActive = YES;
if (!isNew && mainUser) if (!isNew && mainUser && [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
signedIn = [[MPiOSAppDelegate get] signInAsUser:mainUser saveInContext:mainContext usingMasterPassword:nil]; 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.text = @"";
self.entryField.enabled = YES; self.entryField.enabled = YES;
[self selectedAvatar].spinnerActive = NO; userAvatar.spinnerActive = NO;
if (!signedIn) [self.entryField becomeFirstResponder];
[self.entryField becomeFirstResponder];
} }
} }
} }