Finish TouchID support.
This commit is contained in:
parent
a42edec918
commit
61b4ea4525
2
External/Pearl
vendored
2
External/Pearl
vendored
@ -1 +1 @@
|
||||
Subproject commit 2642d720cc874635c336406a01f1e8d2ee5bcb09
|
||||
Subproject commit 1c02f68934e0a32adf8f4901fe9e77cff43d93a3
|
@ -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,
|
||||
(__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?: @"",
|
||||
#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
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (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;
|
||||
|
||||
// Update the key chain if necessary.
|
||||
switch (self.key.origin) {
|
||||
case MPKeyOriginMasterPassword:
|
||||
[self storeSavedKeyFor:user];
|
||||
break;
|
||||
|
||||
case MPKeyOriginKeyChain:
|
||||
case MPKeyOriginKeyChainBiometric:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@try {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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 )];
|
||||
} );
|
||||
}];
|
||||
}
|
||||
|
@ -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,16 +365,27 @@ 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];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user