2
0
MasterPassword/platform-darwin/Source/MPAppDelegate_Key.m

298 lines
12 KiB
Mathematica
Raw Normal View History

2017-04-05 20:56:22 +00:00
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
2017-04-05 20:56:22 +00:00
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
2017-04-05 20:56:22 +00:00
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
2017-04-05 20:56:22 +00:00
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
#import <Crashlytics/Answers.h>
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
@interface MPAppDelegate_Shared()
@property(strong, atomic) MPKey *key;
@end
2013-04-20 18:11:19 +00:00
@implementation MPAppDelegate_Shared(Key)
- (NSDictionary *)createKeyQueryforUser:(MPUserEntity *)user origin:(out MPKeyOrigin *)keyOrigin {
2016-01-14 14:58:04 +00:00
#if TARGET_OS_IPHONE
2016-08-17 22:34:32 +00:00
if (user.touchID && kSecUseAuthenticationUI) {
2016-01-14 14:58:04 +00:00
if (keyOrigin)
*keyOrigin = MPKeyOriginKeyChainBiometric;
CFErrorRef acError = NULL;
id accessControl = (__bridge_transfer id)SecAccessControlCreateWithFlags( kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlTouchIDCurrentSet, &acError );
if (!accessControl)
MPError( (__bridge_transfer NSError *)acError, @"Could not use TouchID on this device." );
2016-01-14 14:58:04 +00:00
else
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:@{
(__bridge id)kSecAttrService : @"Saved Master Password",
(__bridge id)kSecAttrAccount : user.name?: @"",
(__bridge id)kSecAttrAccessControl : accessControl,
(__bridge id)kSecUseAuthenticationUI: (__bridge id)kSecUseAuthenticationUIAllow,
(__bridge id)kSecUseOperationPrompt :
2016-01-14 14:58:04 +00:00
strf( @"Access %@'s master password.", user.name ),
}
matches:nil];
}
#endif
2012-06-07 22:40:30 +00:00
2016-01-14 14:58:04 +00:00
if (keyOrigin)
*keyOrigin = MPKeyOriginKeyChain;
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
attributes:@{
(__bridge id)kSecAttrService : @"Saved Master Password",
(__bridge id)kSecAttrAccount : user.name?: @"",
2016-01-14 14:58:04 +00:00
#if TARGET_OS_IPHONE
(__bridge id)kSecAttrAccessible: (__bridge id)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly?: kSecAttrAccessibleWhenUnlockedThisDeviceOnly),
2016-01-14 14:58:04 +00:00
#endif
}
matches:nil];
}
- (MPKey *)loadSavedKeyFor:(MPUserEntity *)user {
2012-06-07 22:40:30 +00:00
2016-01-14 14:58:04 +00:00
MPKeyOrigin keyOrigin;
NSDictionary *keyQuery = [self createKeyQueryforUser:user origin:&keyOrigin];
id<MPAlgorithm> keyAlgorithm = user.algorithm;
MPKey *key = [[MPKey alloc] initForFullName:user.name withKeyResolver:^NSData *(id<MPAlgorithm> algorithm) {
return ![algorithm isEqual:keyAlgorithm]? nil:
PearlMainQueueAwait( (id)^{
return [PearlKeyChain dataOfItemForQuery:keyQuery];
} );
} keyOrigin:keyOrigin];
if ([key keyIDForAlgorithm:user.algorithm])
inf( @"Found key in keychain for user: %@", user.userID );
else {
inf( @"No key found in keychain for user: %@", user.userID );
key = nil;
}
2012-06-07 22:40:30 +00:00
return key;
}
2012-06-07 22:40:30 +00:00
- (void)storeSavedKeyFor:(MPUserEntity *)user {
2012-06-07 22:40:30 +00:00
if (user.saveKey) {
NSData *masterKey = [self.key keyForAlgorithm:user.algorithm];
if (masterKey) {
[self forgetSavedKeyFor:user];
inf( @"Saving key in keychain for user: %@", user.userID );
[PearlKeyChain addOrUpdateItemForQuery:[self createKeyQueryforUser:user origin:nil] withAttributes:@{
(__bridge id)kSecValueData: masterKey
}];
}
}
}
2012-06-07 22:40:30 +00:00
- (void)forgetSavedKeyFor:(MPUserEntity *)user {
OSStatus result = [PearlKeyChain deleteItemForQuery:[self createKeyQueryforUser:user origin:nil]];
if (result == noErr) {
inf( @"Removed key from keychain for user: %@", user.userID );
2012-06-07 22:40:30 +00:00
[[NSNotificationCenter defaultCenter] postNotificationName:MPKeyForgottenNotification object:self];
}
2012-06-07 22:40:30 +00:00
}
- (void)signOutAnimated:(BOOL)animated {
2012-06-07 22:40:30 +00:00
if (self.key)
self.key = nil;
2012-06-07 22:40:30 +00:00
self.activeUser = nil;
2017-04-01 04:30:25 +00:00
[[NSNotificationCenter defaultCenter] postNotificationName:MPSignedOutNotification object:self userInfo:@{ @"animated": @(animated) }];
2012-06-07 22:40:30 +00:00
}
- (BOOL)signInAsUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc usingMasterPassword:(NSString *)password {
2016-01-14 14:58:04 +00:00
NSAssert( ![NSThread isMainThread], @"Authentication should not happen on the main thread." );
if (!user)
return NO;
2012-06-07 22:40:30 +00:00
MPKey *tryKey = nil;
2012-06-07 22:40:30 +00:00
// Method 1: When the user has no keyID set, set a new key from the given master password.
if (!user.keyID) {
if ([password length] && (tryKey = [[MPKey alloc] initForFullName:user.name withMasterPassword:password])) {
user.keyID = [tryKey keyIDForAlgorithm:MPAlgorithmDefault];
2014-09-21 15:47:53 +00:00
// Migrate existing sites.
[self migrateSitesForUser:user saveInContext:moc toKey:tryKey];
2013-04-20 18:11:19 +00:00
}
2012-06-07 22:40:30 +00:00
}
// Method 2: Depending on the user's saveKey, load or remove the key from the keychain.
if (!user.saveKey)
// Key should not be stored in keychain. Delete it.
2012-06-07 22:40:30 +00:00
[self forgetSavedKeyFor:user];
2013-04-20 18:11:19 +00:00
else if (!tryKey) {
// Key should be saved in keychain. Load it.
if ((tryKey = [self loadSavedKeyFor:user]) && ![user.keyID isEqual:[tryKey keyIDForAlgorithm:user.algorithm]]) {
2013-04-20 18:11:19 +00:00
// Loaded password doesn't match user's keyID. Forget saved password: it is incorrect.
inf( @"Saved password doesn't match keyID for user: %@", user.userID );
trc( @"user keyID: %@ (version: %d) != authentication keyID: %@",
user.keyID, user.algorithm.version, [tryKey keyIDForAlgorithm:user.algorithm] );
2013-04-20 18:11:19 +00:00
tryKey = nil;
[self forgetSavedKeyFor:user];
2012-06-08 21:46:13 +00:00
}
2013-04-20 18:11:19 +00:00
}
2012-06-07 22:40:30 +00:00
// Method 3: Check the given master password string.
if (!tryKey && [password length] && (tryKey = [[MPKey alloc] initForFullName:user.name withMasterPassword:password]) &&
![user.keyID isEqual:[tryKey keyIDForAlgorithm:user.algorithm]]) {
inf( @"Key derived from password doesn't match keyID for user: %@", user.userID );
trc( @"user keyID: %@ (version: %u) != authentication keyID: %@",
user.keyID, user.algorithm.version, [tryKey keyIDForAlgorithm:user.algorithm] );
2012-06-07 22:40:30 +00:00
tryKey = nil;
2012-06-07 22:40:30 +00:00
}
// No more methods left, fail if key still not known.
if (!tryKey) {
if (password)
inf( @"Password login failed for user: %@", user.userID );
else
dbg( @"Automatic login failed for user: %@", user.userID );
if ([[MPConfig get].sendInfo boolValue]) {
#ifdef CRASHLYTICS
[Answers logLoginWithMethod:password? @"Password": @"Automatic" success:@NO customAttributes:@{
@"algorithm": @(user.algorithm.version),
}];
#endif
}
2012-06-07 22:40:30 +00:00
return NO;
}
inf( @"Logged in user: %@", user.userID );
2012-06-07 22:40:30 +00:00
if (![self.key isEqualToKey:tryKey]) {
// Upgrade the user's keyID if not at the default version yet.
if (user.algorithm.version != MPAlgorithmDefaultVersion) {
user.algorithm = MPAlgorithmDefault;
user.keyID = [tryKey keyIDForAlgorithm:user.algorithm];
inf( @"Upgraded keyID to version %u for user: %@", user.algorithm.version, user.userID );
}
self.key = tryKey;
2016-01-14 14:58:04 +00:00
// Update the key chain if necessary.
[self storeSavedKeyFor:user];
}
2012-06-07 22:40:30 +00:00
@try {
if ([[MPConfig get].sendInfo boolValue]) {
#ifdef CRASHLYTICS
[[Crashlytics sharedInstance] setUserName:user.userID];
[Answers logLoginWithMethod:password? @"Password": @"Automatic" success:@YES customAttributes:@{
@"algorithm": @(user.algorithm.version),
}];
#endif
}
}
@catch (id exception) {
err( @"While setting username: %@", exception );
}
user.lastUsed = [NSDate date];
self.activeUser = user;
[moc saveToStore];
// Perform a data sanity check now that we're logged in as the user to allow fixes that require the user's key.
if ([[MPConfig get].checkInconsistency boolValue])
[MPAppDelegate_Shared managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
[self findAndFixInconsistenciesSaveInContext:context];
}];
[[NSNotificationCenter defaultCenter] postNotificationName:MPSignedInNotification object:self];
2012-06-07 22:40:30 +00:00
return YES;
}
2014-09-21 15:47:53 +00:00
- (void)migrateSitesForUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc toKey:(MPKey *)newKey {
2013-04-13 18:03:50 +00:00
2014-09-21 15:47:53 +00:00
if (![user.sites count])
// Nothing to migrate.
2013-04-13 18:03:50 +00:00
return;
MPKey *recoverKey = newKey;
#ifdef PEARL_UIKIT
PearlOverlay *activityOverlay = [PearlOverlay showProgressOverlayWithTitle:PearlString( @"Migrating %ld sites...",
2014-09-21 15:47:53 +00:00
(long)[user.sites count] )];
2013-04-13 18:03:50 +00:00
#endif
2014-09-21 15:47:53 +00:00
for (MPSiteEntity *site in user.sites) {
if (site.type & MPResultTypeClassStateful) {
NSString *content;
while (!(content = [site.algorithm resolvePasswordForSite:(MPStoredSiteEntity *)site usingKey:recoverKey])) {
2014-09-21 15:47:53 +00:00
// Failed to decrypt site with the current recoveryKey. Ask user for a new one to use.
NSString *masterPassword = nil;
2013-04-13 18:03:50 +00:00
#ifdef PEARL_UIKIT
masterPassword = PearlAwait( ^(void (^setResult)(id)) {
2020-01-14 18:59:32 +00:00
UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"Enter Old Master Password" message:
PearlString( @"Your old master password is required to migrate the stored password for %@", site.name )
preferredStyle:UIAlertControllerStyleAlert];
[controller addTextFieldWithConfigurationHandler:nil];
[controller addAction:[UIAlertAction actionWithTitle:@"Migrate" style:UIAlertActionStyleDefault handler:
^(UIAlertAction *_Nonnull action) {
setResult( controller.textFields.firstObject.text );
}]];
[controller addAction:[UIAlertAction actionWithTitle:@"Don't Migrate" style:UIAlertActionStyleCancel handler:
^(UIAlertAction *_Nonnull action) {
setResult( nil );
}]];
[self.navigationController presentViewController:controller animated:YES completion:nil];
} );
2013-04-13 18:03:50 +00:00
#endif
if (!masterPassword)
// Don't Migrate
2013-04-13 18:03:50 +00:00
break;
recoverKey = [[MPKey alloc] initForFullName:user.name withMasterPassword:masterPassword];
2013-04-13 18:03:50 +00:00
}
if (!content)
// Don't Migrate
2013-04-13 18:03:50 +00:00
break;
if (![recoverKey isEqualToKey:newKey])
2014-09-21 15:47:53 +00:00
[site.algorithm savePassword:content toSite:site usingKey:newKey];
2013-04-13 18:03:50 +00:00
}
}
[moc saveToStore];
#ifdef PEARL_UIKIT
[activityOverlay cancelOverlayAnimated:YES];
2013-04-13 18:03:50 +00:00
#endif
}
@end