231 lines
7.9 KiB
Objective-C
231 lines
7.9 KiB
Objective-C
//
|
|
// MPAppDelegate.m
|
|
// MasterPassword
|
|
//
|
|
// Created by Maarten Billemont on 24/11/11.
|
|
// Copyright (c) 2011 Lyndir. All rights reserved.
|
|
//
|
|
|
|
#import "MPAppDelegate_Key.h"
|
|
#import "MPAppDelegate_Store.h"
|
|
|
|
@interface MPAppDelegate_Shared()
|
|
|
|
@property(strong, nonatomic) MPKey *key;
|
|
|
|
@end
|
|
|
|
@implementation MPAppDelegate_Shared(Key)
|
|
|
|
static NSDictionary *keyQuery(MPUserEntity *user) {
|
|
|
|
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
|
attributes:@{
|
|
(__bridge id)kSecAttrService : @"Saved Master Password",
|
|
(__bridge id)kSecAttrAccount : user.name?: @""
|
|
}
|
|
matches:nil];
|
|
}
|
|
|
|
- (MPKey *)loadSavedKeyFor:(MPUserEntity *)user {
|
|
|
|
NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )];
|
|
if (!keyData) {
|
|
inf( @"No key found in keychain for: %@", user.userID );
|
|
return nil;
|
|
}
|
|
|
|
inf( @"Found key in keychain for: %@", user.userID );
|
|
return [MPAlgorithmDefault keyFromKeyData:keyData];
|
|
}
|
|
|
|
- (void)storeSavedKeyFor:(MPUserEntity *)user {
|
|
|
|
if (user.saveKey) {
|
|
NSData *existingKeyData = [PearlKeyChain dataOfItemForQuery:keyQuery( user )];
|
|
|
|
if (![existingKeyData isEqualToData:self.key.keyData]) {
|
|
inf( @"Saving key in keychain for: %@", user.userID );
|
|
|
|
[PearlKeyChain addOrUpdateItemForQuery:keyQuery( user )
|
|
withAttributes:@{
|
|
(__bridge id)kSecValueData : self.key.keyData,
|
|
#if TARGET_OS_IPHONE
|
|
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
#endif
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)forgetSavedKeyFor:(MPUserEntity *)user {
|
|
|
|
OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery( user )];
|
|
if (result == noErr) {
|
|
inf( @"Removed key from keychain for: %@", user.userID );
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPKeyForgottenNotification object:self];
|
|
}
|
|
}
|
|
|
|
- (void)signOutAnimated:(BOOL)animated {
|
|
|
|
if (self.key)
|
|
self.key = nil;
|
|
|
|
self.activeUser = nil;
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPSignedOutNotification object:self userInfo:@{ @"animated" : @(animated) }];
|
|
}
|
|
|
|
- (BOOL)signInAsUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc usingMasterPassword:(NSString *)password {
|
|
|
|
if (password)
|
|
NSAssert( ![NSThread isMainThread], @"Computing key must not happen from the main thread." );
|
|
if (!user)
|
|
return NO;
|
|
|
|
MPKey *tryKey = nil;
|
|
|
|
// 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 = [MPAlgorithmDefault keyForPassword:password ofUserNamed:user.name])) {
|
|
user.keyID = tryKey.keyID;
|
|
|
|
// Migrate existing sites.
|
|
[self migrateSitesForUser:user saveInContext:moc toKey:tryKey];
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
[self forgetSavedKeyFor:user];
|
|
|
|
else if (!tryKey) {
|
|
// Key should be saved in keychain. Load it.
|
|
if ((tryKey = [self loadSavedKeyFor:user]) && ![user.keyID isEqual:tryKey.keyID]) {
|
|
// Loaded password doesn't match user's keyID. Forget saved password: it is incorrect.
|
|
inf( @"Saved password doesn't match keyID for: %@", user.userID );
|
|
|
|
tryKey = nil;
|
|
[self forgetSavedKeyFor:user];
|
|
}
|
|
}
|
|
|
|
// Method 3: Check the given master password string.
|
|
if (!tryKey && [password length] && (tryKey = [MPAlgorithmDefault keyForPassword:password ofUserNamed:user.name]) &&
|
|
![user.keyID isEqual:tryKey.keyID]) {
|
|
inf( @"Key derived from password doesn't match keyID for: %@", user.userID );
|
|
|
|
tryKey = nil;
|
|
}
|
|
|
|
// No more methods left, fail if key still not known.
|
|
if (!tryKey) {
|
|
if (password) {
|
|
inf( @"Login failed for: %@", user.userID );
|
|
MPCheckpoint( MPCheckpointSignInFailed, nil );
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
inf( @"Logged in: %@", user.userID );
|
|
|
|
if (![self.key isEqualToKey:tryKey]) {
|
|
self.key = tryKey;
|
|
[self storeSavedKeyFor:user];
|
|
}
|
|
|
|
@try {
|
|
if ([[MPConfig get].sendInfo boolValue]) {
|
|
#ifdef CRASHLYTICS
|
|
[Crashlytics setObjectValue:user.userID forKey:@"username"];
|
|
[Crashlytics setUserName:user.userID];
|
|
#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];
|
|
MPCheckpoint( MPCheckpointSignedIn, nil );
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)migrateSitesForUser:(MPUserEntity *)user saveInContext:(NSManagedObjectContext *)moc toKey:(MPKey *)newKey {
|
|
|
|
if (![user.sites count])
|
|
// Nothing to migrate.
|
|
return;
|
|
|
|
MPKey *recoverKey = newKey;
|
|
#ifdef PEARL_UIKIT
|
|
PearlOverlay *activityOverlay = [PearlOverlay showProgressOverlayWithTitle:PearlString( @"Migrating %ld sites...",
|
|
(long)[user.sites count] )];
|
|
#endif
|
|
|
|
for (MPSiteEntity *site in user.sites) {
|
|
if (site.type & MPSiteTypeClassStored) {
|
|
NSString *content;
|
|
while (!(content = [site.algorithm storedPasswordForSite:(MPStoredSiteEntity *)site usingKey:recoverKey])) {
|
|
// Failed to decrypt site with the current recoveryKey. Ask user for a new one to use.
|
|
__block NSString *masterPassword = nil;
|
|
|
|
#ifdef PEARL_UIKIT
|
|
dispatch_group_t recoverPasswordGroup = dispatch_group_create();
|
|
dispatch_group_enter( recoverPasswordGroup );
|
|
[PearlAlert showAlertWithTitle:@"Enter Old Master Password"
|
|
message:PearlString( @"Your old master password is required to migrate the stored password for %@",
|
|
site.name )
|
|
viewStyle:UIAlertViewStyleSecureTextInput
|
|
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
|
@try {
|
|
if (buttonIndex_ == [alert_ cancelButtonIndex])
|
|
// Don't Migrate
|
|
return;
|
|
|
|
masterPassword = [alert_ textFieldAtIndex:0].text;
|
|
}
|
|
@finally {
|
|
dispatch_group_leave( recoverPasswordGroup );
|
|
}
|
|
} cancelTitle:@"Don't Migrate" otherTitles:@"Migrate", nil];
|
|
dispatch_group_wait( recoverPasswordGroup, DISPATCH_TIME_FOREVER );
|
|
#endif
|
|
if (!masterPassword)
|
|
// Don't Migrate
|
|
break;
|
|
|
|
recoverKey = [site.algorithm keyForPassword:masterPassword ofUserNamed:user.name];
|
|
}
|
|
|
|
if (!content)
|
|
// Don't Migrate
|
|
break;
|
|
|
|
if (![recoverKey isEqualToKey:newKey])
|
|
[site.algorithm savePassword:content toSite:site usingKey:newKey];
|
|
}
|
|
}
|
|
|
|
[moc saveToStore];
|
|
|
|
#ifdef PEARL_UIKIT
|
|
[activityOverlay cancelOverlayAnimated:YES];
|
|
#endif
|
|
}
|
|
|
|
@end
|