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

View File

@ -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<MPAlgorithm>)algorithm;
- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData
forAlgorithm:(id<MPAlgorithm>)algorithm keyOrigin:(MPKeyOrigin)origin;
- (NSData *)keyIDForAlgorithm:(id<MPAlgorithm>)algorithm;
- (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm;

View File

@ -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<MPAlgorithm>)algorithm {
- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData
forAlgorithm:(id<MPAlgorithm>)algorithm keyOrigin:(MPKeyOrigin)origin {
if (!(self = [self initForFullName:fullName withMasterPassword:nil]))
return nil;
self.origin = origin;
[_keyCache setObject:keyData forKey:algorithm];
return self;

View File

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

View File

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

View File

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

View File

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