diff --git a/MacPass.xcodeproj/project.pbxproj b/MacPass.xcodeproj/project.pbxproj index eda9cae1..d90c5085 100644 --- a/MacPass.xcodeproj/project.pbxproj +++ b/MacPass.xcodeproj/project.pbxproj @@ -319,6 +319,7 @@ 6021FE9818E1650F00C3BC51 /* DatabaseSettingsWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6021FE9A18E1650F00C3BC51 /* DatabaseSettingsWindow.xib */; }; 7837112C225540D1009BD28D /* PluginRepositoryBrowserView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7837112E225540D1009BD28D /* PluginRepositoryBrowserView.xib */; }; 78E1F8B022E3A5D600E738AE /* AutotypeDoctorReportViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 78E1F8B222E3A5D600E738AE /* AutotypeDoctorReportViewController.xib */; }; + AF105CF325FE5B2000C4FD3C /* MPTouchIdCompositeKeyStore.m in Sources */ = {isa = PBXBuildFile; fileRef = AF105CF125FE5B2000C4FD3C /* MPTouchIdCompositeKeyStore.m */; }; FA13910C1F9CD9EB0033D256 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = FA13910A1F9CD9EB0033D256 /* Localizable.stringsdict */; }; FA9FD3271FB5E8F4003CEDD6 /* AutotypeCandidateSelectionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = FA9FD3291FB5E8F4003CEDD6 /* AutotypeCandidateSelectionView.xib */; }; FA9FD32C1FB5EDD3003CEDD6 /* AutotypeBuilderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = FA9FD32E1FB5EDD3003CEDD6 /* AutotypeBuilderView.xib */; }; @@ -1114,6 +1115,8 @@ ABE8662E2316617500201125 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.stringsdict"; sourceTree = ""; }; ABE8662F2316617500201125 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/AutotypeDoctorReportViewController.strings"; sourceTree = ""; }; ABE86630231662D200201125 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/PluginDataView.strings"; sourceTree = ""; }; + AF105CF125FE5B2000C4FD3C /* MPTouchIdCompositeKeyStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPTouchIdCompositeKeyStore.m; sourceTree = ""; }; + AF105CF225FE5B2000C4FD3C /* MPTouchIdCompositeKeyStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPTouchIdCompositeKeyStore.h; sourceTree = ""; }; BB3E050C1FE9D1CA00F0B46F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/AutotypeCandidateSelectionView.strings; sourceTree = ""; }; BB3E050D1FE9D1CB00F0B46F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = ""; }; BB3E050E1FE9D1CC00F0B46F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/ReferenceBuilderView.strings; sourceTree = ""; }; @@ -1369,6 +1372,8 @@ 4C4B7EF717A4B335000234C7 /* MPUniqueCharactersFormatter.m */, 4C3C4EAD18D7039300153127 /* MPValueTransformerHelper.h */, 4C3C4EAE18D7039300153127 /* MPValueTransformerHelper.m */, + AF105CF225FE5B2000C4FD3C /* MPTouchIdCompositeKeyStore.h */, + AF105CF125FE5B2000C4FD3C /* MPTouchIdCompositeKeyStore.m */, 4C52197F273D192C00C719D3 /* MPOpenURLHandler.h */, 4C521980273D192C00C719D3 /* MPOpenURLHandler.m */, ); @@ -2324,6 +2329,7 @@ 4C978E0D19AE54AB003067DF /* MPFlagsHelper.m in Sources */, 4C6F228919A4A7F90012310C /* MPAutotypeClear.m in Sources */, 4C0B038C18E36DA400B9F9C9 /* MPFixAutotypeWindowController.m in Sources */, + AF105CF325FE5B2000C4FD3C /* MPTouchIdCompositeKeyStore.m in Sources */, 4C7679BF1D76D6D8001F33D6 /* MPErrorRecoveryAttempter.m in Sources */, 4CAD338F205169D30068587E /* MPPluginRepositoryItem.m in Sources */, 4C9BFFFB1FD19B5400264B16 /* MPPrettyPasswordTransformer.m in Sources */, @@ -3232,6 +3238,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = MacPassAppIcon; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_ENTITLEMENTS = MacPass/MacPass.entitlements; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = "${CURRENT_PROJECT_VERSION}"; @@ -3262,6 +3269,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = MacPassAppIcon; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_ENTITLEMENTS = MacPass/MacPass.entitlements; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = "${CURRENT_PROJECT_VERSION}"; diff --git a/MacPass/Base.lproj/IntegrationPreferences.xib b/MacPass/Base.lproj/IntegrationPreferences.xib index 8705ad7d..63b9f590 100644 --- a/MacPass/Base.lproj/IntegrationPreferences.xib +++ b/MacPass/Base.lproj/IntegrationPreferences.xib @@ -27,19 +27,19 @@ - + - + - + - + - + Autotype might not work properly. Some issues where found that prevent Autotype or Global Autotype to work. Please run the Autotype Doctor to fix those issues. @@ -48,7 +48,7 @@ - + @@ -107,14 +107,14 @@ - + If enabled, a dialog will show up before Autotype is executed even if only a single match was found to prevent accidental input and wrong matches @@ -123,42 +123,42 @@ - + @@ -205,20 +205,20 @@ - + - + - + @@ -239,17 +239,57 @@ + + + + + + + + + + + + MacPass will no longer be able to unlock any Database with TouchID until it is successfully unlocked with the password and or keyfile. + + + + + + + + + + + + + + + + + + - + + - + diff --git a/MacPass/MPConstants.h b/MacPass/MPConstants.h index ef3e1577..de914f18 100644 --- a/MacPass/MPConstants.h +++ b/MacPass/MPConstants.h @@ -40,4 +40,11 @@ FOUNDATION_EXPORT NSString *const MPPluginUTI; FOUNDATION_EXPORT NSString *const MPBundleHelpURLKey; FOUNDATION_EXPORT NSString *const MPBundlePluginRepositoryURLKey; FOUNDATION_EXPORT NSString *const MPPluginCompatibilityURLKey; + +/** + Keychain Keys + */ +extern NSString *const TouchIdUnlockPublicKeyTag; +extern NSString *const TouchIdUnlockPrivateKeyTag; + #endif diff --git a/MacPass/MPConstants.m b/MacPass/MPConstants.m index 99041ee1..f4729f82 100644 --- a/MacPass/MPConstants.m +++ b/MacPass/MPConstants.m @@ -31,3 +31,6 @@ NSString *const MPBundleHelpURLKey = @"MPHelpURL"; NSString *const MPBundlePluginRepositoryURLKey = @"MPPluginRepositoryURL"; NSString *const MPPluginCompatibilityURLKey = @"MPPluginCompatibilityURLKey"; +NSString *const TouchIdUnlockPublicKeyTag = @"com.hicknhacksoftware.macpass.publickey"; +NSString *const TouchIdUnlockPrivateKeyTag = @"com.hicknhacksoftware.macpass.privatekey"; + diff --git a/MacPass/MPDocument.h b/MacPass/MPDocument.h index 4d079eca..fc42df61 100644 --- a/MacPass/MPDocument.h +++ b/MacPass/MPDocument.h @@ -108,13 +108,13 @@ FOUNDATION_EXPORT NSString *const MPDocumentGroupKey; /** * Decrypts the database with the given password and keyfile * - * @param password The password to unlock the db with, can be nil. This is not the same as an empty string @"" - * @param keyFileURL URL for the keyfile to use, can be nil + * @param compositeKey The CompositeKey to unlock the db. + * @param keyFileURL URL for the keyfile that was used to create the compositeKey. Can be nil. * @param error Pointer to an NSError pointer of error reporting. * * @return YES if the document was unlocked sucessfully, NO otherwise. Consult the error object for details */ -- (BOOL)unlockWithPassword:(NSString *)password keyFileURL:(NSURL *)keyFileURL error:(NSError *__autoreleasing*)error; +- (BOOL)unlockWithPassword:(KPKCompositeKey *)compositeKey keyFileURL:(NSURL *)keyFileURL error:(NSError *__autoreleasing*)error; /** * Changes the password of the database. Some sanity checks are applied and the change is aborted if the new values aren't valid * diff --git a/MacPass/MPDocument.m b/MacPass/MPDocument.m index 3197723e..36fee8b6 100644 --- a/MacPass/MPDocument.m +++ b/MacPass/MPDocument.m @@ -422,13 +422,9 @@ NSString *const MPDocumentGroupKey = @"MPDocumentGrou MPPasswordInputController *passwordInputController = [[MPPasswordInputController alloc] init]; [passwordInputController requestPasswordWithMessage:NSLocalizedString(@"EXTERN_CHANGE_OF_MASTERKEY", @"The master key was changed by an external program!") cancelLabel:NSLocalizedString(@"ABORT_MERGE_KEEP_MINE", @"Button label to abort a merge on a file with changed master key!") - completionHandler:^BOOL(NSString *password, NSURL *keyURL, BOOL didCancel, NSError *__autoreleasing *error) { - [self.windowForSheet endSheet:sheet returnCode:(didCancel ? NSModalResponseCancel : NSModalResponseOK)]; - if(!didCancel) { - NSData *keyFileData = keyURL ? [NSData dataWithContentsOfURL:keyURL] : nil; - KPKCompositeKey *compositeKey = [[KPKCompositeKey alloc] init]; - [compositeKey addKey:[KPKKey keyWithPassword:password]]; - [compositeKey addKey:[KPKKey keyWithKeyFileData:keyFileData]]; + completionHandler:^BOOL(KPKCompositeKey *compositeKey, NSURL* keyURL, BOOL didCancel, NSError *__autoreleasing *error) { + [self.windowForSheet endSheet:sheet returnCode:(didCancel ? NSModalResponseCancel : NSModalResponseOK)]; + if(!didCancel) { [self _mergeWithContentsFromURL:url key:compositeKey options:options]; } // just return yes regardless since we will display the sheet again if needed! @@ -494,13 +490,9 @@ NSString *const MPDocumentGroupKey = @"MPDocumentGrou } -- (BOOL)unlockWithPassword:(NSString *)password keyFileURL:(NSURL *)keyFileURL error:(NSError *__autoreleasing*)error { +- (BOOL)unlockWithPassword:(KPKCompositeKey *)compositeKey keyFileURL:(NSURL *)keyFileURL error:(NSError *__autoreleasing*)error{ // TODO: Make this API asynchronous - NSData *keyFileData = keyFileURL ? [NSData dataWithContentsOfURL:keyFileURL] : nil; - - self.compositeKey = [[KPKCompositeKey alloc] init]; - [self.compositeKey addKey:[KPKKey keyWithPassword:password]]; - [self.compositeKey addKey:[KPKKey keyWithKeyFileData:keyFileData]]; + self.compositeKey = compositeKey; self.tree = [[KPKTree alloc] initWithData:self.encryptedData key:self.compositeKey error:error]; BOOL isUnlocked = (nil != self.tree); @@ -856,7 +848,7 @@ NSString *const MPDocumentGroupKey = @"MPDocumentGrou userInfo:@{ MPDocumentEntryKey: lastDuplicate }]; } } - + - (void)duplicateGroup:(id)sender { for(KPKGroup *group in self.selectedGroups) { diff --git a/MacPass/MPDocumentWindowController.m b/MacPass/MPDocumentWindowController.m index 625f521e..867cdccb 100644 --- a/MacPass/MPDocumentWindowController.m +++ b/MacPass/MPDocumentWindowController.m @@ -337,12 +337,11 @@ typedef void (^MPPasswordChangedBlock)(BOOL didChangePassword); self.passwordInputController = [[MPPasswordInputController alloc] init]; } self.contentViewController = self.passwordInputController; - [self.passwordInputController requestPasswordWithMessage:message cancelLabel:nil completionHandler:^BOOL(NSString *password, NSURL *keyURL, BOOL didCancel, NSError *__autoreleasing *error) { + [self.passwordInputController requestPasswordWithMessage:message cancelLabel:nil completionHandler:^BOOL(KPKCompositeKey* compositeKey, NSURL* keyURL, BOOL didCancel, NSError *__autoreleasing *error) { if(didCancel) { return NO; } - return [((MPDocument *)self.document) unlockWithPassword:password keyFileURL:keyURL error:error]; - + return [((MPDocument *)self.document) unlockWithPassword:compositeKey keyFileURL:keyURL error:error ]; }]; } diff --git a/MacPass/MPIntegrationPreferencesController.m b/MacPass/MPIntegrationPreferencesController.m index f6e57bf1..83abc174 100644 --- a/MacPass/MPIntegrationPreferencesController.m +++ b/MacPass/MPIntegrationPreferencesController.m @@ -24,6 +24,7 @@ #import "MPSettingsHelper.h" #import "MPIconHelper.h" #import "MPAutotypeDoctor.h" +#import "MPConstants.h" #import "DDHotKeyCenter.h" #import "DDHotKey+MacPassAdditions.h" @@ -133,4 +134,32 @@ - (void)runAutotypeDoctor:(id)sender { [MPAutotypeDoctor.defaultDoctor runChecksAndPresentResults]; } + +#pragma mark - +#pragma mark Keychain Actions +- (IBAction)RenewTouchIdKey:(id)sender { + NSData* publicKeyTag = [TouchIdUnlockPublicKeyTag dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *publicKeyQuery = @{ + (id)kSecClass: (id)kSecClassKey, + (id)kSecAttrApplicationTag: publicKeyTag, + (id)kSecReturnRef: @YES, + }; + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)publicKeyQuery); + if (status != errSecSuccess) { + NSString* description = (__bridge NSString*)SecCopyErrorMessageString(status, NULL); + NSLog(@"Error while trying to delete public key from Keychain: %@", description); + } + + NSData* privateKeyTag = [TouchIdUnlockPrivateKeyTag dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *privateKeyQuery = @{ + (id)kSecClass: (id)kSecClassKey, + (id)kSecAttrApplicationTag: privateKeyTag, + (id)kSecReturnRef: @YES, + }; + status = SecItemDelete((__bridge CFDictionaryRef)privateKeyQuery); + if (status != errSecSuccess) { + NSString* description = (__bridge NSString*)SecCopyErrorMessageString(status, NULL); + NSLog(@"Error while trying to delete private key from Keychain: %@", description); + } +} @end diff --git a/MacPass/MPPasswordInputController.h b/MacPass/MPPasswordInputController.h index 26ffd2b1..7501aee3 100644 --- a/MacPass/MPPasswordInputController.h +++ b/MacPass/MPPasswordInputController.h @@ -21,14 +21,14 @@ // #import "MPViewController.h" +#import "KeePassKit/KeePassKit.h" @class KPKCompositeKey; @interface MPPasswordInputController : MPViewController -typedef BOOL (^passwordInputCompletionBlock)(NSString *password, NSURL *keyURL, BOOL didCancel, NSError *__autoreleasing*error); +typedef BOOL (^passwordInputCompletionBlock)(KPKCompositeKey *key, NSURL* keyFileURL, BOOL didCancel, NSError *__autoreleasing*error); -- (void)requestPasswordWithCompletionHandler:(passwordInputCompletionBlock)completionHandler; - (void)requestPasswordWithMessage:(NSString *)message cancelLabel:(NSString *)cancelLabel completionHandler:(passwordInputCompletionBlock)completionHandler; diff --git a/MacPass/MPPasswordInputController.m b/MacPass/MPPasswordInputController.m index 96001ca3..445020f3 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -27,6 +27,9 @@ #import "MPSettingsHelper.h" #import "MPPathControl.h" #import "MPTouchBarButtonCreator.h" +#import "MPSettingsHelper.h" +#import "MPConstants.h" +#import "MPTouchIdCompositeKeyStore.h" #import "HNHUi/HNHUi.h" @@ -44,6 +47,8 @@ @property (weak) IBOutlet NSButton *enablePasswordCheckBox; @property (weak) IBOutlet NSButton *unlockButton; @property (weak) IBOutlet NSButton *cancelButton; +@property (weak) IBOutlet NSButton *touchIdButton; +@property (weak) IBOutlet NSButton *touchIdEnabledButton; @property (copy) NSString *message; @property (copy) NSString *cancelLabel; @@ -81,6 +86,13 @@ [self.enablePasswordCheckBox bind:NSValueBinding toObject:self withKeyPath:NSStringFromSelector(@selector(enablePassword)) options:nil]; [self.togglePasswordButton bind:NSEnabledBinding toObject:self withKeyPath:NSStringFromSelector(@selector(enablePassword)) options:nil]; [self.passwordTextField bind:NSEnabledBinding toObject:self withKeyPath:NSStringFromSelector(@selector(enablePassword)) options:nil]; + NSUserDefaultsController *defaultsController = [NSUserDefaultsController sharedUserDefaultsController]; + [self.touchIdEnabledButton bind:NSValueBinding toObject:defaultsController withKeyPath:[MPSettingsHelper defaultControllerPathForKey:kMPSettingsKeyEntryTouchIdEnabled] options:nil]; + self.touchIdEnabledButton.hidden = true; + if (@available(macOS 10.13.4, *)) { + self.touchIdEnabledButton.hidden = false; + [self _touchIdUpdateToolTip]; + } [self _reset]; } @@ -95,10 +107,6 @@ [self _reset]; } -- (void)requestPasswordWithCompletionHandler:(passwordInputCompletionBlock)completionHandler { - [self requestPasswordWithMessage:nil cancelLabel:nil completionHandler:completionHandler]; -} - #pragma mark Properties - (void)setEnablePassword:(BOOL)enablePassword { if(_enablePassword != enablePassword) { @@ -127,8 +135,24 @@ NSString *password = self.enablePassword ? self.passwordTextField.stringValue : nil; BOOL cancel = (sender == self.cancelButton); - BOOL result = self.completionHandler(password, self.keyPathControl.URL, cancel, &error); - if(cancel || result) { + NSURL* keyURL = self.keyPathControl.URL; + NSData *keyFileData = keyURL ? [NSData dataWithContentsOfURL:keyURL] : nil; + KPKKey* passwordKey = [KPKKey keyWithPassword:password]; + KPKKey* fileKey = [KPKKey keyWithKeyFileData:keyFileData]; + KPKCompositeKey* compositeKey = [[KPKCompositeKey alloc] init]; + [compositeKey addKey:passwordKey]; + [compositeKey addKey:fileKey]; + /* After the completion handler finished we no longer have a windowController set */ + NSString* documentKey = NULL; + bool documentKeyValid = [self _touchIdGetKeyForCurrentDocument:&documentKey]; + BOOL result = self.completionHandler(compositeKey, keyURL, cancel, &error); + if(result) { + if(documentKeyValid) { + [self _touchIdUpdateKeyForCurrentDocument:compositeKey forDocumentKey:documentKey]; + } + return; + } + if(cancel) { return; } [self _showError:error]; @@ -138,6 +162,203 @@ } } +- (void) _touchIdUpdateKeyForCurrentDocument: (KPKCompositeKey*)compositeKey forDocumentKey: (NSString*) documentKey{ + NSData* encryptedKey = [self _touchIdEncryptCompositeKey:compositeKey]; + [MPTouchIdCompositeKeyStore.defaultStore save:encryptedKey forDocumentKey:documentKey]; +} + +- (void) _touchIdCreateAndAddRSAKeyPair { + CFErrorRef error = NULL; + NSString* publicKeyLabel = @"MacPass TouchID Feature Public Key"; + NSString* privateKeyLabel = @"MacPass TouchID Feature Private Key"; + NSData* publicKeyTag = [TouchIdUnlockPublicKeyTag dataUsingEncoding:NSUTF8StringEncoding]; + NSData* privateKeyTag = [TouchIdUnlockPrivateKeyTag dataUsingEncoding:NSUTF8StringEncoding]; + SecAccessControlRef access = NULL; + if (@available(macOS 10.13.4, *)) { + SecAccessControlCreateFlags flags = kSecAccessControlBiometryCurrentSet; + if (@available(macOS 10.15, *)) { + flags |= kSecAccessControlWatch | kSecAccessControlOr; + } + access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + flags, + &error); + if(access == NULL) { + NSError *err = CFBridgingRelease(error); + NSLog(@"Error while trying to create AccessControl for TouchID unlock feature: %@", [err description]); + return; + } + NSDictionary* attributes = @{ + (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA, + (id)kSecAttrKeySizeInBits: @2048, + (id)kSecAttrSynchronizable: @NO, + (id)kSecPrivateKeyAttrs: + @{ (id)kSecAttrIsPermanent: @YES, + (id)kSecAttrApplicationTag: privateKeyTag, + (id)kSecAttrLabel: privateKeyLabel, + (id)kSecAttrAccessControl: (__bridge id)access + }, + (id)kSecPublicKeyAttrs: + @{ (id)kSecAttrIsPermanent: @YES, + (id)kSecAttrApplicationTag: publicKeyTag, + (id)kSecAttrLabel: publicKeyLabel, + }, + }; + SecKeyRef result = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error); + if(result == NULL) { + NSError *err = CFBridgingRelease(error); + NSLog(@"Error while trying to create a RSA keypair for TouchID unlock feature: %@", [err description]); + } + else { + CFRelease(result); + } + } + else { + return; + } +} + +- (NSData*) _touchIdEncryptCompositeKey: (KPKCompositeKey*) compositeKey { + NSData* encryptedKey = nil; + NSData* keyData = [NSKeyedArchiver archivedDataWithRootObject:compositeKey]; + NSData* tag = [TouchIdUnlockPublicKeyTag dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *getquery = @{ + (id)kSecClass: (id)kSecClassKey, + (id)kSecAttrApplicationTag: tag, + (id)kSecReturnRef: @YES, + }; + SecKeyRef publicKey = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)getquery, (CFTypeRef *)&publicKey); + if (status != errSecSuccess) { + [self _touchIdCreateAndAddRSAKeyPair]; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)getquery, (CFTypeRef *)&publicKey); + if (status != errSecSuccess) { + NSString* description = (__bridge NSString*)SecCopyErrorMessageString(status, NULL); + NSLog(@"Error while trying to query public key from Keychain: %@", description); + return nil; + } + } + SecKeyAlgorithm algorithm = kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM; + BOOL canEncrypt = SecKeyIsAlgorithmSupported(publicKey, kSecKeyOperationTypeEncrypt, algorithm); + if(canEncrypt) { + CFErrorRef error = NULL; + encryptedKey = (NSData*)CFBridgingRelease(SecKeyCreateEncryptedData(publicKey, algorithm, (__bridge CFDataRef)keyData, &error)); + if (!encryptedKey) { + NSError *err = CFBridgingRelease(error); + NSLog(@"Error while trying to decrypt the CompositeKey for TouchID unlock: %@", [err description]); + } + } + else { + NSLog(@"The key retreived from the Keychain is unable to encrypt data"); + } + if (publicKey) { + CFRelease(publicKey); + } + return encryptedKey; +} + +- (KPKCompositeKey*) _touchIdDecryptCompositeKey: (NSData*) encryptedKey { + KPKCompositeKey* result = nil; + if(encryptedKey != nil) { + NSData* tag = [TouchIdUnlockPrivateKeyTag dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *queryPrivateKey = @{ + (id)kSecClass: (id)kSecClassKey, + (id)kSecAttrApplicationTag: tag, + (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA, + (id)kSecReturnRef: @YES, + }; + SecKeyRef privateKey = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)queryPrivateKey, (CFTypeRef *)&privateKey); + if (status == errSecSuccess) { + SecKeyAlgorithm algorithm = kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM; + BOOL canDecrypt = SecKeyIsAlgorithmSupported(privateKey, kSecKeyOperationTypeDecrypt, algorithm); + if(canDecrypt) { + CFErrorRef error = NULL; + NSData* clearText = (NSData*)CFBridgingRelease(SecKeyCreateDecryptedData(privateKey, algorithm, (__bridge CFDataRef)encryptedKey, &error)); + if (clearText) { + result = [NSKeyedUnarchiver unarchiveObjectWithData:clearText]; + } + else { + NSError *err = CFBridgingRelease(error); + NSLog(@"Error while trying to decrypt password for TouchID unlock: %@", [err description]); + } + } + else { + NSLog(@"Key does not support decryption"); + } + } + else { + NSString* description = (__bridge NSString*)SecCopyErrorMessageString(status, NULL); + NSLog(@"Error while trying to retrive private key for decryption: %@", description); + } + if (privateKey) { + CFRelease(privateKey); + } + } + return result; +} + +- (bool) _touchIdGetKeyForCurrentDocument: (NSString**) result { + *result = NULL; + NSDocument* currentDocument = self.windowController.document; + if(currentDocument != NULL && currentDocument.fileURL != NULL && currentDocument.fileURL.lastPathComponent != NULL) { + *result = [NSString stringWithFormat:kMPSettingsKeyEntryTouchIdDatabaseEncryptedKeyFormat, currentDocument.fileURL.lastPathComponent]; + return true; + } + return false; +} + +- (bool) _touchIdIsUnlockAvailable { + NSData* unused = NULL; + bool encryptedKeyAvailableForDocument = [self _touchIdGetEncrypedKeyMaterial:&unused]; + return encryptedKeyAvailableForDocument; +} + +- (bool) _touchIdGetEncrypedKeyMaterial: (NSData**) result { + NSString* documentKey = NULL; + *result = NULL; + if(![self _touchIdGetKeyForCurrentDocument:&documentKey]) { + return false; + } + return [MPTouchIdCompositeKeyStore.defaultStore load:result forDocumentKey:documentKey]; +} + +- (IBAction)unlockWithTouchID:(id)sender { + NSData* encryptedKey = NULL; + if(![self _touchIdGetEncrypedKeyMaterial:&encryptedKey]) { + [self.touchIdButton setEnabled:false]; + return; + } + KPKCompositeKey* compositeKey = [self _touchIdDecryptCompositeKey:encryptedKey]; + if(compositeKey == NULL) { + [self.touchIdButton setEnabled:false]; + return; + } + NSError* error; + bool success = self.completionHandler(compositeKey, NULL, false, &error); + if(success) { + return; + } + [self.touchIdButton setEnabled:false]; + [self _showError:error]; +} + +- (IBAction)touchIdEnabledChanged:(id)sender { + [self _touchIdUpdateToolTip]; +} + +- (void) _touchIdUpdateToolTip { + switch(self.touchIdEnabledButton.state) { + case NSControlStateValueOn: + self.touchIdEnabledButton.toolTip = NSLocalizedString(@"TOOLTIP_TOUCHID_ENABELD", @"Tooltip displayed when TouchID is is fully enabeld"); + case NSControlStateValueOff: + self.touchIdEnabledButton.toolTip = NSLocalizedString(@"TOOLTIP_TOUCHID_DISABLED", @"Tooltip displayed when TouchID is disabled"); + case NSControlStateValueMixed: + default: + self.touchIdEnabledButton.toolTip = NSLocalizedString(@"TOOLTIP_TOUCHID_TRANSIENT", @"Tooltip displayed when TouchID is in transient (inmemory) mode"); + } +} + - (IBAction)resetKeyFile:(id)sender { /* If the reset was triggered by ourselves we want to preselect the keyfile */ if(sender == self) { @@ -153,6 +374,9 @@ self.enablePassword = YES; self.passwordTextField.stringValue = @""; self.messageInfoTextField.hidden = (nil == self.message); + self.touchIdButton.hidden = ![self _touchIdIsUnlockAvailable]; + [self.touchIdButton setEnabled:true]; + if(self.message) { self.messageInfoTextField.stringValue = self.message; self.messageImageView.image = [NSImage imageNamed:NSImageNameInfo]; @@ -236,5 +460,4 @@ } } - @end diff --git a/MacPass/MPSettingsHelper.h b/MacPass/MPSettingsHelper.h index f97aba44..fff5779b 100644 --- a/MacPass/MPSettingsHelper.h +++ b/MacPass/MPSettingsHelper.h @@ -22,6 +22,10 @@ #import +/* TouchID */ +APPKIT_EXTERN NSString *const kMPSettingsKeyEntryTouchIdEnabled; +APPKIT_EXTERN NSString *const kMPSettingsKeyEntryTouchIdDatabaseEncryptedKeyFormat; + /* Clipboard */ APPKIT_EXTERN NSString *const kMPSettingsKeyPasteboardClearTimeout; APPKIT_EXTERN NSString *const kMPSettingsKeyClearPasteboardOnQuit; diff --git a/MacPass/MPSettingsHelper.m b/MacPass/MPSettingsHelper.m index af0803d0..0537304c 100644 --- a/MacPass/MPSettingsHelper.m +++ b/MacPass/MPSettingsHelper.m @@ -67,6 +67,9 @@ NSString *const kMPSettingsKeyAutotypeMatchHost = @"Au NSString *const kMPSettingsKeyAutotypeMatchTags = @"AutotypeMatchTags"; NSString *const kMPSettingsKeyGloablAutotypeAlwaysShowCandidateSelection = @"GloablAutotypeAlwaysShowCandidateSelection"; +NSString *const kMPSettingsKeyEntryTouchIdEnabled = @"EnableSubsequentUnlocksWithTouchID"; +NSString *const kMPSettingsKeyEntryTouchIdDatabaseEncryptedKeyFormat = @"EncryptedDatabaseKeyForTouchID-%@"; + NSString *const kMPSettingsKeyEntrySearchFilterContext = @"EntrySearchFilterContext"; NSString *const kMPSettingsKeyEnableQuicklookPreview = @"EnableQuicklookPreview"; diff --git a/MacPass/MPTouchIdCompositeKeyStore.h b/MacPass/MPTouchIdCompositeKeyStore.h new file mode 100644 index 00000000..e7957cea --- /dev/null +++ b/MacPass/MPTouchIdCompositeKeyStore.h @@ -0,0 +1,21 @@ +// +// MPTouchIdCompositeKeyStore.h +// MacPass +// +// Created by Julius Zint on 14.03.21. +// Copyright © 2021 HicknHack Software GmbH. All rights reserved. +// + +#ifndef MPTouchIdCompositeKeyStore_h +#define MPTouchIdCompositeKeyStore_h + +static NSMutableDictionary* touchIDSecuredPasswords; + +@interface MPTouchIdCompositeKeyStore : NSObject + @property (class, strong, readonly) MPTouchIdCompositeKeyStore *defaultStore; + + - (void) save:(NSData*) encryptedCompositeKey forDocumentKey:(NSString*) documentKey; + - (bool) load:(NSData**) encryptedCompositeKey forDocumentKey:(NSString*) documentKey; +@end + +#endif /* MPTouchIdCompositeKeyStore_h */ diff --git a/MacPass/MPTouchIdCompositeKeyStore.m b/MacPass/MPTouchIdCompositeKeyStore.m new file mode 100644 index 00000000..c265ee8c --- /dev/null +++ b/MacPass/MPTouchIdCompositeKeyStore.m @@ -0,0 +1,64 @@ +// +// MPTouchIdCompositeKeyStore.m +// MacPass +// +// Created by Julius Zint on 14.03.21. +// Copyright © 2021 HicknHack Software GmbH. All rights reserved. +// +#import "MPSettingsHelper.h" +#import "MPTouchIdCompositeKeyStore.h" + +@implementation MPTouchIdCompositeKeyStore + ++ (instancetype)defaultStore { + static MPTouchIdCompositeKeyStore *instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[MPTouchIdCompositeKeyStore alloc] init]; + if(touchIDSecuredPasswords == NULL) { + touchIDSecuredPasswords = [[NSMutableDictionary alloc]init]; + } + }); + return instance; +} + +- (void) save: (NSData*) encryptedCompositeKey forDocumentKey:(NSString*) documentKey { + long touchIdMode = [NSUserDefaults.standardUserDefaults integerForKey:kMPSettingsKeyEntryTouchIdEnabled]; + if (touchIdMode == NSControlStateValueMixed) { + [NSUserDefaults.standardUserDefaults removeObjectForKey:documentKey]; + if(encryptedCompositeKey != NULL) { + [touchIDSecuredPasswords setObject:encryptedCompositeKey forKey:documentKey]; + } + } + else if(touchIdMode == NSControlStateValueOn) { + [touchIDSecuredPasswords removeObjectForKey:documentKey]; + if(encryptedCompositeKey != NULL) { + [NSUserDefaults.standardUserDefaults setObject:encryptedCompositeKey forKey:documentKey]; + } + } + else { + [NSUserDefaults.standardUserDefaults removeObjectForKey:documentKey]; + [touchIDSecuredPasswords removeObjectForKey:documentKey]; + } +} + +- (bool) load: (NSData**) encryptedCompositeKey forDocumentKey: (NSString*) documentKey { + long touchIdMode = [NSUserDefaults.standardUserDefaults integerForKey:kMPSettingsKeyEntryTouchIdEnabled]; + NSData* transientKey = [touchIDSecuredPasswords valueForKey:documentKey]; + NSData* persistentKey =[NSUserDefaults.standardUserDefaults dataForKey:documentKey]; + if(transientKey == NULL && persistentKey == NULL) { + return false; + } + if(transientKey == NULL || persistentKey == NULL) { + *encryptedCompositeKey = transientKey == NULL ? persistentKey : transientKey; + return true; + } + if(touchIdMode == NSControlStateValueOn) { + *encryptedCompositeKey = persistentKey; + return true; + } + *encryptedCompositeKey = transientKey; + return true; +} + +@end diff --git a/MacPass/MacPass.entitlements b/MacPass/MacPass.entitlements index 1551075c..df91acf4 100644 --- a/MacPass/MacPass.entitlements +++ b/MacPass/MacPass.entitlements @@ -6,5 +6,9 @@ com.apple.security.cs.disable-library-validation + keychain-access-groups + + $(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER) +