From a7b8be18861bb891cadc0eb0acb083e799d6729f Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sat, 20 Jun 2020 11:36:02 +0200 Subject: [PATCH 01/22] TouchID unlock Feature for MacPass. To use it a user must first enter the correct password for the database. If the unlock succeeds, the supplied password is encrypted with the public part of a RSA keypair. On subsequent unlocks a TouchID button appears. If clicked, MacPass queries the Keychain for the private key part and uses it to decrypt the previously supplied password and tries to unlock the database with it. --- MacPass/Base.lproj/PasswordInputView.xib | 85 +++++++----- MacPass/MPPasswordInputController.m | 164 +++++++++++++++++++++++ 2 files changed, 218 insertions(+), 31 deletions(-) diff --git a/MacPass/Base.lproj/PasswordInputView.xib b/MacPass/Base.lproj/PasswordInputView.xib index 750809cd..ba6d5339 100644 --- a/MacPass/Base.lproj/PasswordInputView.xib +++ b/MacPass/Base.lproj/PasswordInputView.xib @@ -16,6 +16,7 @@ + @@ -25,19 +26,6 @@ - @@ -109,30 +97,67 @@ DQ - + + + + + + + + + + + + + + + + + + + - @@ -150,19 +175,17 @@ Gw - + - - - + + - diff --git a/MacPass/MPPasswordInputController.m b/MacPass/MPPasswordInputController.m index 96001ca3..b432f875 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -32,6 +32,8 @@ #import "NSError+Messages.h" +static NSMutableDictionary* touchIDSecuredPasswords; + @interface MPPasswordInputController () @property (strong) NSButton *showPasswordButton; @@ -44,6 +46,7 @@ @property (weak) IBOutlet NSButton *enablePasswordCheckBox; @property (weak) IBOutlet NSButton *unlockButton; @property (weak) IBOutlet NSButton *cancelButton; +@property (weak) IBOutlet NSButton *touchIdButton; @property (copy) NSString *message; @property (copy) NSString *cancelLabel; @@ -64,6 +67,9 @@ self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if(self) { _enablePassword = YES; + if(touchIDSecuredPasswords == NULL) { + touchIDSecuredPasswords = [[NSMutableDictionary alloc]init]; + } [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(_selectKeyURL) name:MPDidChangeStoredKeyFilesSettings object:nil]; } return self; @@ -129,6 +135,9 @@ BOOL cancel = (sender == self.cancelButton); BOOL result = self.completionHandler(password, self.keyPathControl.URL, cancel, &error); if(cancel || result) { + if(result && self.keyPathControl.URL == nil) { + [self _storePasswordForTouchIDUnlock:password forDatabase:@"DatabaseID"]; + } return; } [self _showError:error]; @@ -138,6 +147,150 @@ } } +- (void) _createAndAddRSAKeyPair { + CFErrorRef error = NULL; + NSString* publicKeyLabel = @"MacPass TouchID Feature Public Key"; + NSString* privateKeyLabel = @"MacPass TouchID Feature Private Key"; + NSData* publicKeyTag = [@"com.hicknhacksoftware.macpass.publickey" dataUsingEncoding:NSUTF8StringEncoding]; + NSData* privateKeyTag = [@"com.hicknhacksoftware.macpass.privatekey" dataUsingEncoding:NSUTF8StringEncoding]; + SecAccessControlRef access = NULL; + if (@available(macOS 10.13.4, *)) { + access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + kSecAccessControlBiometryCurrentSet, + &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 privateKey = NULL; + SecKeyRef publicKey = NULL; + OSStatus result = SecKeyGeneratePair((__bridge CFDictionaryRef)attributes, &privateKey, &publicKey); + if(result == errSecSuccess) { + CFRelease(publicKey); + CFRelease(privateKey); + } + else { + NSString* description = (__bridge NSString*)SecCopyErrorMessageString(result, NULL); + NSLog(@"Error while trying to create a RSA keypair for TouchID unlock feature: %@", description); + } + } + else { + return; + } +} + +- (void) _storePasswordForTouchIDUnlock: (NSString*) password forDatabase: (NSString*) databaseId { + NSData* passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; + NSData* tag = [@"com.hicknhacksoftware.macpass.publickey" 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 _createAndAddRSAKeyPair]; + 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; + } + } + SecKeyAlgorithm algorithm = kSecKeyAlgorithmRSAEncryptionOAEPSHA512; + BOOL canEncrypt = SecKeyIsAlgorithmSupported(publicKey, kSecKeyOperationTypeEncrypt, algorithm); + if(canEncrypt) { + int k = (int)SecKeyGetBlockSize(publicKey); + int hlen = 512 / 8; + int maxMessageLengthInByte = k - 2 * hlen - 2; + if([passwordData length] <= maxMessageLengthInByte) { + CFErrorRef error = NULL; + NSData* cipherText = (NSData*)CFBridgingRelease(SecKeyCreateEncryptedData(publicKey, algorithm, (__bridge CFDataRef)passwordData, &error)); + if (cipherText) { + [touchIDSecuredPasswords setObject:cipherText forKey:databaseId]; + } + else { + NSError *err = CFBridgingRelease(error); + NSLog(@"Error while trying decrypt password for TouchID unlock: %@", [err description]); + } + } + else { + NSLog(@"The password is to large to be encrypted"); + return; + } + } + else { + NSLog(@"The key retreived from the Keychain is unable to encrypt data"); + } + if (publicKey) { CFRelease(publicKey); } +} + +- (NSString*) _loadPasswordForTochIDUnlock: (NSString*) databaseId { + NSString* result = nil; + NSData* cipherText = [touchIDSecuredPasswords valueForKey:databaseId]; + if(cipherText != nil) { + NSData* tag = [@"com.hicknhacksoftware.macpass.privatekey" 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 = kSecKeyAlgorithmRSAEncryptionOAEPSHA512; + BOOL canDecrypt = SecKeyIsAlgorithmSupported(privateKey, kSecKeyOperationTypeDecrypt, algorithm); + if(canDecrypt) { + if([cipherText length] == SecKeyGetBlockSize(privateKey)) { + CFErrorRef error = NULL; + NSData* clearText = (NSData*)CFBridgingRelease(SecKeyCreateDecryptedData(privateKey, algorithm, (__bridge CFDataRef)cipherText, &error)); + if (clearText) { + result = [[NSString alloc]initWithData:clearText encoding:NSUTF8StringEncoding]; + } + else { + NSError *err = CFBridgingRelease(error); + NSLog(@"Error while trying decrypt password for TouchID unlock: %@", [err description]); + } + } + else { + NSLog(@"Block size of the cipher text has a unexpected value: %lu", (unsigned long)[cipherText length]); + } + } + 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; +} + - (IBAction)resetKeyFile:(id)sender { /* If the reset was triggered by ourselves we want to preselect the keyfile */ if(sender == self) { @@ -153,6 +306,8 @@ self.enablePassword = YES; self.passwordTextField.stringValue = @""; self.messageInfoTextField.hidden = (nil == self.message); + self.touchIdButton.hidden = [touchIDSecuredPasswords valueForKey:@"DatabaseID"] == nil; + if(self.message) { self.messageInfoTextField.stringValue = self.message; self.messageImageView.image = [NSImage imageNamed:NSImageNameInfo]; @@ -236,5 +391,14 @@ } } +- (IBAction)unlockWithTouchID:(id)sender { + NSString* password = [self _loadPasswordForTochIDUnlock:@"DatabaseID"]; + if(password != nil) { + NSError* error; + self.completionHandler(password, nil, false, &error); + [self _showError:error]; + } +} + @end From 4e5674057736fb8c60c2e1dd1113d5821976b8ee Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Thu, 30 Jul 2020 18:22:23 +0200 Subject: [PATCH 02/22] Added necessary entitlement and additional buildstep Enabled the "Keychain Sharing" Entitlement that is required for the Keychain APIs to work properly. The additional buildstep signs the KissXML.framework nested inside the KeePassKit.framework before this Framework itself is signed and embedded. This is necessary because, to my knowledge, Xcode does not support signing nested frameworks. --- MacPass.xcodeproj/project.pbxproj | 20 ++++++++++++++++++++ MacPass/MacPass.entitlements | 2 ++ 2 files changed, 22 insertions(+) diff --git a/MacPass.xcodeproj/project.pbxproj b/MacPass.xcodeproj/project.pbxproj index 99120138..84354ebe 100644 --- a/MacPass.xcodeproj/project.pbxproj +++ b/MacPass.xcodeproj/project.pbxproj @@ -1904,6 +1904,7 @@ 4C77E35F15B84A240093A587 /* Frameworks */, 4C77E36015B84A240093A587 /* Resources */, 4C44DB1B1C08999F00774EB3 /* Versioning */, + AF786C6F24D327A400240320 /* ShellScript */, 4CC5D36618A1332000AF7FA8 /* CopyFiles */, ); buildRules = ( @@ -2120,6 +2121,23 @@ shellScript = "git=`sh /etc/profile; which git`\nbranch_name=`$git rev-parse --abbrev-ref HEAD`\ngit_count=`$git rev-list $branch_name |wc -l | sed 's/^ *//;s/ *$//'`\n\nbuild_number=\"${git_count}0\"\nif [ $CONFIGURATION != \"Release\" ]; then\n if [ $branch_name != \"master\" ] && [ $branch_name != release* ]; then\n build_number+=\"-$branch_name\"\n fi\nfi\nif [ \"$CI\" = \"true\" ]; then\nbuild_date=`date +\"%Y%m%d%H%m%S\"`\nbuild_number=\"$build_date-continuous\"\nfi\n\nplist=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\ndsym_plist=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$plist\"\nif [ -f \"$DSYM_INFO_PLIST\" ] ; then\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $build_number\" \"$dsym_plist\"\nfi\n"; showEnvVarsInLog = 0; }; + AF786C6F24D327A400240320 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [ \"$CODE_SIGNING_REQUIRED\" = \"NO\" ]\nthen\n exit 0\nfi\n/usr/bin/codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" --timestamp=none --preserve-metadata=identifier,entitlements,flags ./Carthage/Build/Mac/KeePassKit.framework/Versions/A/Frameworks/KissXML.framework\nexit $?\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -3064,6 +3082,7 @@ CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_ENTITLEMENTS = MacPass/MacPass.entitlements; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = "${CURRENT_PROJECT_VERSION}"; FRAMEWORK_SEARCH_PATHS = ( @@ -3094,6 +3113,7 @@ CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_ENTITLEMENTS = MacPass/MacPass.entitlements; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = "${CURRENT_PROJECT_VERSION}"; FRAMEWORK_SEARCH_PATHS = ( diff --git a/MacPass/MacPass.entitlements b/MacPass/MacPass.entitlements index 1551075c..d7bc1b26 100644 --- a/MacPass/MacPass.entitlements +++ b/MacPass/MacPass.entitlements @@ -6,5 +6,7 @@ com.apple.security.cs.disable-library-validation + keychain-access-groups + From 94956a673b5fa760382793e04c5fda4a7004fbb8 Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Thu, 30 Jul 2020 20:00:12 +0200 Subject: [PATCH 03/22] fixed typos --- MacPass/MPPasswordInputController.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MacPass/MPPasswordInputController.m b/MacPass/MPPasswordInputController.m index b432f875..ed6c3787 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -234,7 +234,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; } } else { - NSLog(@"The password is to large to be encrypted"); + NSLog(@"The password is too large to be encrypted"); return; } } @@ -269,7 +269,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; } else { NSError *err = CFBridgingRelease(error); - NSLog(@"Error while trying decrypt password for TouchID unlock: %@", [err description]); + NSLog(@"Error while trying to decrypt password for TouchID unlock: %@", [err description]); } } else { From 5157ec823ffb9243cdddc889f83287d4c21e1623 Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sun, 16 Aug 2020 12:19:48 +0200 Subject: [PATCH 04/22] Enables TouchID unlock for multiple Database files. This changeset adds the optional fileURL parameter to the requestPasswordWithMessage function in MPPasswordInputController. The controller uses this URL as a key to store the encrypted masterpassword in a dictionary. In my opinion edge cases like when a file is moved or replaced do not have to get special handling since the worst case scenario is that TouchID unlock does not work and users have still the option to unlock with the masterpassword. Also this changeset removes the unused requestPasswordWithCompletionHandler function --- MacPass/MPDocument.m | 2 +- MacPass/MPDocumentWindowController.m | 6 +++++- MacPass/MPPasswordInputController.h | 3 +-- MacPass/MPPasswordInputController.m | 17 ++++++++++++----- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/MacPass/MPDocument.m b/MacPass/MPDocument.m index 4b27c35c..602c88f0 100644 --- a/MacPass/MPDocument.m +++ b/MacPass/MPDocument.m @@ -440,7 +440,7 @@ NSString *const MPDocumentGroupKey = @"MPDocumentGrou } // just return yes regardless since we will display the sheet again if needed! return YES; - }]; + } forFile:nil]; sheet.contentViewController = passwordInputController; [self.windowForSheet beginSheet:sheet completionHandler:^(NSModalResponse returnCode) { /* nothing to do, rest is done in other handler! */ }]; } diff --git a/MacPass/MPDocumentWindowController.m b/MacPass/MPDocumentWindowController.m index 0b7cb1db..d7b2e2f4 100644 --- a/MacPass/MPDocumentWindowController.m +++ b/MacPass/MPDocumentWindowController.m @@ -325,13 +325,17 @@ typedef void (^MPPasswordChangedBlock)(BOOL didChangePassword); self.passwordInputController = [[MPPasswordInputController alloc] init]; } self.contentViewController = self.passwordInputController; + NSURL* fileURL = nil; + if(self.document != nil) { + fileURL = [self.document fileURL]; + } [self.passwordInputController requestPasswordWithMessage:message cancelLabel:nil completionHandler:^BOOL(NSString *password, NSURL *keyURL, BOOL didCancel, NSError *__autoreleasing *error) { if(didCancel) { return NO; } return [((MPDocument *)self.document) unlockWithPassword:password keyFileURL:keyURL error:error]; - }]; + } forFile:fileURL]; } - (void)editPassword:(id)sender { diff --git a/MacPass/MPPasswordInputController.h b/MacPass/MPPasswordInputController.h index 26ffd2b1..a8e3ee62 100644 --- a/MacPass/MPPasswordInputController.h +++ b/MacPass/MPPasswordInputController.h @@ -28,8 +28,7 @@ typedef BOOL (^passwordInputCompletionBlock)(NSString *password, NSURL *keyURL, BOOL didCancel, NSError *__autoreleasing*error); -- (void)requestPasswordWithCompletionHandler:(passwordInputCompletionBlock)completionHandler; -- (void)requestPasswordWithMessage:(NSString *)message cancelLabel:(NSString *)cancelLabel completionHandler:(passwordInputCompletionBlock)completionHandler; +- (void)requestPasswordWithMessage:(NSString *)message cancelLabel:(NSString *)cancelLabel completionHandler:(passwordInputCompletionBlock)completionHandler forFile:(NSURL*) fileURL; @end diff --git a/MacPass/MPPasswordInputController.m b/MacPass/MPPasswordInputController.m index ed6c3787..6002b161 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -50,6 +50,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; @property (copy) NSString *message; @property (copy) NSString *cancelLabel; +@property (copy) NSString *absoluteURLString; @property (assign) BOOL showPassword; @property (nonatomic, assign) BOOL enablePassword; @@ -94,15 +95,21 @@ static NSMutableDictionary* touchIDSecuredPasswords; return self.passwordTextField; } -- (void)requestPasswordWithMessage:(NSString *)message cancelLabel:(NSString *)cancelLabel completionHandler:(passwordInputCompletionBlock)completionHandler { +- (void)requestPasswordWithMessage:(NSString *)message cancelLabel:(NSString *)cancelLabel completionHandler:(passwordInputCompletionBlock)completionHandler forFile:(NSURL*) fileURL{ self.completionHandler = completionHandler; self.message = message; self.cancelLabel = cancelLabel; + if(fileURL) { + self.absoluteURLString = [fileURL absoluteString]; + } + else { + self.absoluteURLString = nil; + } [self _reset]; } - (void)requestPasswordWithCompletionHandler:(passwordInputCompletionBlock)completionHandler { - [self requestPasswordWithMessage:nil cancelLabel:nil completionHandler:completionHandler]; + [self requestPasswordWithMessage:nil cancelLabel:nil completionHandler:completionHandler forFile:nil]; } #pragma mark Properties @@ -136,7 +143,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; BOOL result = self.completionHandler(password, self.keyPathControl.URL, cancel, &error); if(cancel || result) { if(result && self.keyPathControl.URL == nil) { - [self _storePasswordForTouchIDUnlock:password forDatabase:@"DatabaseID"]; + [self _storePasswordForTouchIDUnlock:password forDatabase:self.absoluteURLString]; } return; } @@ -306,7 +313,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; self.enablePassword = YES; self.passwordTextField.stringValue = @""; self.messageInfoTextField.hidden = (nil == self.message); - self.touchIdButton.hidden = [touchIDSecuredPasswords valueForKey:@"DatabaseID"] == nil; + self.touchIdButton.hidden = [touchIDSecuredPasswords valueForKey:self.absoluteURLString] == nil; if(self.message) { self.messageInfoTextField.stringValue = self.message; @@ -392,7 +399,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; } - (IBAction)unlockWithTouchID:(id)sender { - NSString* password = [self _loadPasswordForTochIDUnlock:@"DatabaseID"]; + NSString* password = [self _loadPasswordForTochIDUnlock:self.absoluteURLString]; if(password != nil) { NSError* error; self.completionHandler(password, nil, false, &error); From 700dd432823540c10dd1003d404972843fb6b242 Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sat, 22 Aug 2020 11:23:48 +0200 Subject: [PATCH 05/22] TouchID unlock is now optional Added a CheckBox to the PasswordInput view, so the user can see and manipulate, whether the TouchID feature is enabled or disabled. The choice is remembered in the standard user defaults. --- MacPass/Base.lproj/PasswordInputView.xib | 20 +++++++++++++++++--- MacPass/MPPasswordInputController.m | 12 +++++++++++- MacPass/MPSettingsHelper.h | 3 +++ MacPass/MPSettingsHelper.m | 2 ++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/MacPass/Base.lproj/PasswordInputView.xib b/MacPass/Base.lproj/PasswordInputView.xib index ba6d5339..ff8411de 100644 --- a/MacPass/Base.lproj/PasswordInputView.xib +++ b/MacPass/Base.lproj/PasswordInputView.xib @@ -16,7 +16,9 @@ + + @@ -41,7 +43,7 @@ - + @@ -49,7 +51,7 @@ - + @@ -156,12 +158,22 @@ DQ + - + @@ -180,8 +192,10 @@ DQ + + diff --git a/MacPass/MPPasswordInputController.m b/MacPass/MPPasswordInputController.m index 6002b161..d1b1b9f5 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -27,6 +27,7 @@ #import "MPSettingsHelper.h" #import "MPPathControl.h" #import "MPTouchBarButtonCreator.h" +#import "MPSettingsHelper.h" #import "HNHUi/HNHUi.h" @@ -47,6 +48,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; @property (weak) IBOutlet NSButton *unlockButton; @property (weak) IBOutlet NSButton *cancelButton; @property (weak) IBOutlet NSButton *touchIdButton; +@property (weak) IBOutlet NSButton *touchIdEnabled; @property (copy) NSString *message; @property (copy) NSString *cancelLabel; @@ -88,6 +90,11 @@ static NSMutableDictionary* touchIDSecuredPasswords; [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]; + self.touchIdEnabled.hidden = true; + if (@available(macOS 10.13.4, *)) { + self.touchIdEnabled.hidden = false; + self.touchIdEnabled.state = [NSUserDefaults.standardUserDefaults boolForKey:kMPSettingsKeyEntryTouchIdEnabled]; + } [self _reset]; } @@ -142,7 +149,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; BOOL cancel = (sender == self.cancelButton); BOOL result = self.completionHandler(password, self.keyPathControl.URL, cancel, &error); if(cancel || result) { - if(result && self.keyPathControl.URL == nil) { + if(result && self.keyPathControl.URL == nil && self.touchIdEnabled.state) { [self _storePasswordForTouchIDUnlock:password forDatabase:self.absoluteURLString]; } return; @@ -307,6 +314,9 @@ static NSMutableDictionary* touchIDSecuredPasswords; self.keyPathControl.URL = nil; } } +- (IBAction)touchIdEnabledChanged:(id)sender { + [NSUserDefaults.standardUserDefaults setBool:self.touchIdEnabled.state forKey:kMPSettingsKeyEntryTouchIdEnabled]; +} - (void)_reset { self.showPassword = NO; diff --git a/MacPass/MPSettingsHelper.h b/MacPass/MPSettingsHelper.h index b7dcacfb..bcaba4f2 100644 --- a/MacPass/MPSettingsHelper.h +++ b/MacPass/MPSettingsHelper.h @@ -22,6 +22,9 @@ #import +/* TouchID */ +APPKIT_EXTERN NSString *const kMPSettingsKeyEntryTouchIdEnabled; + /* Clipboard */ APPKIT_EXTERN NSString *const kMPSettingsKeyPasteboardClearTimeout; APPKIT_EXTERN NSString *const kMPSettingsKeyClearPasteboardOnQuit; diff --git a/MacPass/MPSettingsHelper.m b/MacPass/MPSettingsHelper.m index c17f8f91..7e94d164 100644 --- a/MacPass/MPSettingsHelper.m +++ b/MacPass/MPSettingsHelper.m @@ -62,6 +62,8 @@ NSString *const kMPSettingsKeyAutotypeMatchHost = @"Au NSString *const kMPSettingsKeyAutotypeMatchTags = @"AutotypeMatchTags"; NSString *const kMPSettingsKeyGloablAutotypeAlwaysShowCandidateSelection = @"GloablAutotypeAlwaysShowCandidateSelection"; +NSString *const kMPSettingsKeyEntryTouchIdEnabled = @"EnableSubsequentUnlocksWithTouchID"; + NSString *const kMPSettingsKeyEntrySearchFilterContext = @"EntrySearchFilterContext"; NSString *const kMPSettingsKeyEnableQuicklookPreview = @"EnableQuicklookPreview"; From 51bdf12198dad9b357ff9b8427fce5f57b7e3b40 Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sat, 22 Aug 2020 15:00:20 +0200 Subject: [PATCH 06/22] MPPasswordInputController completion callback refactoring Changed the completion callback definition to take a KPKCompositeKey pointer instead of a password string and keyfile URL. This is a intermedate step to support key files with TouchID unlock. The next step is to make KPKCompositeKey conform to the NSCoding protocol. The serialized data can then be stored instead of the password. --- MacPass/MPDocument.h | 6 +++--- MacPass/MPDocument.m | 4 ++-- MacPass/MPDocumentWindowController.m | 5 ++--- MacPass/MPPasswordInputController.h | 3 ++- MacPass/MPPasswordInputController.m | 13 ++++++------- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/MacPass/MPDocument.h b/MacPass/MPDocument.h index ffbc6255..3930187d 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 602c88f0..aaf10707 100644 --- a/MacPass/MPDocument.m +++ b/MacPass/MPDocument.m @@ -429,7 +429,7 @@ 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) { + completionHandler:^BOOL(KPKCompositeKey *compositeKey, NSURL* keyURL, BOOL didCancel, NSError *__autoreleasing *error) { [self.windowForSheet endSheet:sheet returnCode:(didCancel ? NSModalResponseCancel : NSModalResponseOK)]; if(!didCancel) { NSData *keyFileData = keyURL ? [NSData dataWithContentsOfURL:keyURL] : nil; @@ -501,7 +501,7 @@ 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; diff --git a/MacPass/MPDocumentWindowController.m b/MacPass/MPDocumentWindowController.m index d7b2e2f4..82977db1 100644 --- a/MacPass/MPDocumentWindowController.m +++ b/MacPass/MPDocumentWindowController.m @@ -329,12 +329,11 @@ typedef void (^MPPasswordChangedBlock)(BOOL didChangePassword); if(self.document != nil) { fileURL = [self.document fileURL]; } - [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 ]; } forFile:fileURL]; } diff --git a/MacPass/MPPasswordInputController.h b/MacPass/MPPasswordInputController.h index a8e3ee62..d3f10046 100644 --- a/MacPass/MPPasswordInputController.h +++ b/MacPass/MPPasswordInputController.h @@ -21,12 +21,13 @@ // #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)requestPasswordWithMessage:(NSString *)message cancelLabel:(NSString *)cancelLabel completionHandler:(passwordInputCompletionBlock)completionHandler forFile:(NSURL*) fileURL; diff --git a/MacPass/MPPasswordInputController.m b/MacPass/MPPasswordInputController.m index d1b1b9f5..6630fe02 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -115,10 +115,6 @@ static NSMutableDictionary* touchIDSecuredPasswords; [self _reset]; } -- (void)requestPasswordWithCompletionHandler:(passwordInputCompletionBlock)completionHandler { - [self requestPasswordWithMessage:nil cancelLabel:nil completionHandler:completionHandler forFile:nil]; -} - #pragma mark Properties - (void)setEnablePassword:(BOOL)enablePassword { if(_enablePassword != enablePassword) { @@ -147,7 +143,10 @@ static NSMutableDictionary* touchIDSecuredPasswords; NSString *password = self.enablePassword ? self.passwordTextField.stringValue : nil; BOOL cancel = (sender == self.cancelButton); - BOOL result = self.completionHandler(password, self.keyPathControl.URL, cancel, &error); + NSURL* keyURL = self.keyPathControl.URL; + NSData *keyFileData = keyURL ? [NSData dataWithContentsOfURL:keyURL] : nil; + KPKCompositeKey *compositeKey = [[KPKCompositeKey alloc] initWithPassword:password keyFileData:keyFileData]; + BOOL result = self.completionHandler(compositeKey, keyURL, cancel, &error); if(cancel || result) { if(result && self.keyPathControl.URL == nil && self.touchIdEnabled.state) { [self _storePasswordForTouchIDUnlock:password forDatabase:self.absoluteURLString]; @@ -412,10 +411,10 @@ static NSMutableDictionary* touchIDSecuredPasswords; NSString* password = [self _loadPasswordForTochIDUnlock:self.absoluteURLString]; if(password != nil) { NSError* error; - self.completionHandler(password, nil, false, &error); + KPKCompositeKey *compositeKey = [[KPKCompositeKey alloc] initWithPassword:password keyFileData:nil]; + self.completionHandler(compositeKey, nil, false, &error); [self _showError:error]; } } - @end From 431b636057abf5b118a5e1bd87cf4235800dcb68 Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sat, 22 Aug 2020 17:19:16 +0200 Subject: [PATCH 07/22] Switched to archiving the complete composite key for TouchID unlock --- MacPass/Base.lproj/PasswordInputView.xib | 1 - MacPass/MPPasswordInputController.m | 21 ++++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/MacPass/Base.lproj/PasswordInputView.xib b/MacPass/Base.lproj/PasswordInputView.xib index ff8411de..a8915f13 100644 --- a/MacPass/Base.lproj/PasswordInputView.xib +++ b/MacPass/Base.lproj/PasswordInputView.xib @@ -16,7 +16,6 @@ - diff --git a/MacPass/MPPasswordInputController.m b/MacPass/MPPasswordInputController.m index 6630fe02..c3d6c158 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -149,7 +149,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; BOOL result = self.completionHandler(compositeKey, keyURL, cancel, &error); if(cancel || result) { if(result && self.keyPathControl.URL == nil && self.touchIdEnabled.state) { - [self _storePasswordForTouchIDUnlock:password forDatabase:self.absoluteURLString]; + [self _storePasswordForTouchIDUnlock:compositeKey forDatabase:self.absoluteURLString]; } return; } @@ -210,8 +210,8 @@ static NSMutableDictionary* touchIDSecuredPasswords; } } -- (void) _storePasswordForTouchIDUnlock: (NSString*) password forDatabase: (NSString*) databaseId { - NSData* passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; +- (void) _storePasswordForTouchIDUnlock: (KPKCompositeKey*) compositeKey forDatabase: (NSString*) databaseId { + NSData* keyData = [NSKeyedArchiver archivedDataWithRootObject:compositeKey]; NSData* tag = [@"com.hicknhacksoftware.macpass.publickey" dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *getquery = @{ (id)kSecClass: (id)kSecClassKey, @@ -235,9 +235,9 @@ static NSMutableDictionary* touchIDSecuredPasswords; int k = (int)SecKeyGetBlockSize(publicKey); int hlen = 512 / 8; int maxMessageLengthInByte = k - 2 * hlen - 2; - if([passwordData length] <= maxMessageLengthInByte) { + if([keyData length] <= maxMessageLengthInByte) { CFErrorRef error = NULL; - NSData* cipherText = (NSData*)CFBridgingRelease(SecKeyCreateEncryptedData(publicKey, algorithm, (__bridge CFDataRef)passwordData, &error)); + NSData* cipherText = (NSData*)CFBridgingRelease(SecKeyCreateEncryptedData(publicKey, algorithm, (__bridge CFDataRef)keyData, &error)); if (cipherText) { [touchIDSecuredPasswords setObject:cipherText forKey:databaseId]; } @@ -257,8 +257,8 @@ static NSMutableDictionary* touchIDSecuredPasswords; if (publicKey) { CFRelease(publicKey); } } -- (NSString*) _loadPasswordForTochIDUnlock: (NSString*) databaseId { - NSString* result = nil; +- (KPKCompositeKey*) _loadPasswordForTochIDUnlock: (NSString*) databaseId { + KPKCompositeKey* result = nil; NSData* cipherText = [touchIDSecuredPasswords valueForKey:databaseId]; if(cipherText != nil) { NSData* tag = [@"com.hicknhacksoftware.macpass.privatekey" dataUsingEncoding:NSUTF8StringEncoding]; @@ -278,7 +278,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; CFErrorRef error = NULL; NSData* clearText = (NSData*)CFBridgingRelease(SecKeyCreateDecryptedData(privateKey, algorithm, (__bridge CFDataRef)cipherText, &error)); if (clearText) { - result = [[NSString alloc]initWithData:clearText encoding:NSUTF8StringEncoding]; + result = [NSKeyedUnarchiver unarchiveObjectWithData:clearText]; } else { NSError *err = CFBridgingRelease(error); @@ -408,10 +408,9 @@ static NSMutableDictionary* touchIDSecuredPasswords; } - (IBAction)unlockWithTouchID:(id)sender { - NSString* password = [self _loadPasswordForTochIDUnlock:self.absoluteURLString]; - if(password != nil) { + KPKCompositeKey* compositeKey = [self _loadPasswordForTochIDUnlock:self.absoluteURLString]; + if(compositeKey != nil) { NSError* error; - KPKCompositeKey *compositeKey = [[KPKCompositeKey alloc] initWithPassword:password keyFileData:nil]; self.completionHandler(compositeKey, nil, false, &error); [self _showError:error]; } From 3fc73a7fd973b247360725e2eee59f330ab6bfd1 Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sat, 22 Aug 2020 17:44:46 +0200 Subject: [PATCH 08/22] Switched Encryption algorithm to support larger message texts. kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM is now used. Apples CryptoKit makes it very easy to use asymmetric cryptography to encrypt a symmetric key and with it encrypt a message. So now the Database key material is no longer directly encrypted with the asymmetric key but with a randomly generated symmetric one. --- MacPass/MPPasswordInputController.m | 44 ++++++++++------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/MacPass/MPPasswordInputController.m b/MacPass/MPPasswordInputController.m index c3d6c158..e68e24c9 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -148,7 +148,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; KPKCompositeKey *compositeKey = [[KPKCompositeKey alloc] initWithPassword:password keyFileData:keyFileData]; BOOL result = self.completionHandler(compositeKey, keyURL, cancel, &error); if(cancel || result) { - if(result && self.keyPathControl.URL == nil && self.touchIdEnabled.state) { + if(result && self.touchIdEnabled.state) { [self _storePasswordForTouchIDUnlock:compositeKey forDatabase:self.absoluteURLString]; } return; @@ -229,26 +229,17 @@ static NSMutableDictionary* touchIDSecuredPasswords; return; } } - SecKeyAlgorithm algorithm = kSecKeyAlgorithmRSAEncryptionOAEPSHA512; + SecKeyAlgorithm algorithm = kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM; BOOL canEncrypt = SecKeyIsAlgorithmSupported(publicKey, kSecKeyOperationTypeEncrypt, algorithm); if(canEncrypt) { - int k = (int)SecKeyGetBlockSize(publicKey); - int hlen = 512 / 8; - int maxMessageLengthInByte = k - 2 * hlen - 2; - if([keyData length] <= maxMessageLengthInByte) { - CFErrorRef error = NULL; - NSData* cipherText = (NSData*)CFBridgingRelease(SecKeyCreateEncryptedData(publicKey, algorithm, (__bridge CFDataRef)keyData, &error)); - if (cipherText) { - [touchIDSecuredPasswords setObject:cipherText forKey:databaseId]; - } - else { - NSError *err = CFBridgingRelease(error); - NSLog(@"Error while trying decrypt password for TouchID unlock: %@", [err description]); - } + CFErrorRef error = NULL; + NSData* cipherText = (NSData*)CFBridgingRelease(SecKeyCreateEncryptedData(publicKey, algorithm, (__bridge CFDataRef)keyData, &error)); + if (cipherText) { + [touchIDSecuredPasswords setObject:cipherText forKey:databaseId]; } else { - NSLog(@"The password is too large to be encrypted"); - return; + NSError *err = CFBridgingRelease(error); + NSLog(@"Error while trying decrypt password for TouchID unlock: %@", [err description]); } } else { @@ -271,22 +262,17 @@ static NSMutableDictionary* touchIDSecuredPasswords; SecKeyRef privateKey = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)queryPrivateKey, (CFTypeRef *)&privateKey); if (status == errSecSuccess) { - SecKeyAlgorithm algorithm = kSecKeyAlgorithmRSAEncryptionOAEPSHA512; + SecKeyAlgorithm algorithm = kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM; BOOL canDecrypt = SecKeyIsAlgorithmSupported(privateKey, kSecKeyOperationTypeDecrypt, algorithm); if(canDecrypt) { - if([cipherText length] == SecKeyGetBlockSize(privateKey)) { - CFErrorRef error = NULL; - NSData* clearText = (NSData*)CFBridgingRelease(SecKeyCreateDecryptedData(privateKey, algorithm, (__bridge CFDataRef)cipherText, &error)); - if (clearText) { - result = [NSKeyedUnarchiver unarchiveObjectWithData:clearText]; - } - else { - NSError *err = CFBridgingRelease(error); - NSLog(@"Error while trying to decrypt password for TouchID unlock: %@", [err description]); - } + CFErrorRef error = NULL; + NSData* clearText = (NSData*)CFBridgingRelease(SecKeyCreateDecryptedData(privateKey, algorithm, (__bridge CFDataRef)cipherText, &error)); + if (clearText) { + result = [NSKeyedUnarchiver unarchiveObjectWithData:clearText]; } else { - NSLog(@"Block size of the cipher text has a unexpected value: %lu", (unsigned long)[cipherText length]); + NSError *err = CFBridgingRelease(error); + NSLog(@"Error while trying to decrypt password for TouchID unlock: %@", [err description]); } } else { From 92a120c405b3283bc0af3287441527239d16e8b0 Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sat, 29 Aug 2020 13:27:10 +0200 Subject: [PATCH 09/22] Support for persistent TouchID unlock. While originally not intended, this changeset enables MacPass to unlock a database with TouchID even after the process is completly wiped. It does this by introducing multiple modes of operation. First: TouchId can be completly disabled. The TouchID checkbox is off and MacPass works like the TouchID feature had never been added. Second: The TouchID checkbox gets put into the mixed state. MacPass will now remember the database key in memory as long as the process remains alive and the database can be unlocked with TouchID until the applications terminates. Third: The TouchID checkbox is checked and MacPass will store the encrypted database key on a successfull unlock attempt in the standard userdefaults. TouchID unlock works now even after MacPass is completly terminated and restarted. --- MacPass/Base.lproj/PasswordInputView.xib | 4 +- MacPass/MPPasswordInputController.m | 105 +++++++++++++++-------- MacPass/MPSettingsHelper.h | 1 + MacPass/MPSettingsHelper.m | 1 + 4 files changed, 71 insertions(+), 40 deletions(-) diff --git a/MacPass/Base.lproj/PasswordInputView.xib b/MacPass/Base.lproj/PasswordInputView.xib index a8915f13..f455e2ef 100644 --- a/MacPass/Base.lproj/PasswordInputView.xib +++ b/MacPass/Base.lproj/PasswordInputView.xib @@ -152,14 +152,14 @@ DQ - + @@ -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/MPIntegrationPreferencesController.m b/MacPass/MPIntegrationPreferencesController.m index 8f6bb026..759612b6 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" @@ -129,4 +130,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.m b/MacPass/MPPasswordInputController.m index 0241b151..6fdb30f0 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -28,6 +28,7 @@ #import "MPPathControl.h" #import "MPTouchBarButtonCreator.h" #import "MPSettingsHelper.h" +#import "MPConstants.h" #import "HNHUi/HNHUi.h" @@ -175,8 +176,8 @@ static NSMutableDictionary* touchIDSecuredPasswords; CFErrorRef error = NULL; NSString* publicKeyLabel = @"MacPass TouchID Feature Public Key"; NSString* privateKeyLabel = @"MacPass TouchID Feature Private Key"; - NSData* publicKeyTag = [@"com.hicknhacksoftware.macpass.publickey" dataUsingEncoding:NSUTF8StringEncoding]; - NSData* privateKeyTag = [@"com.hicknhacksoftware.macpass.privatekey" dataUsingEncoding:NSUTF8StringEncoding]; + NSData* publicKeyTag = [TouchIdUnlockPublicKeyTag dataUsingEncoding:NSUTF8StringEncoding]; + NSData* privateKeyTag = [TouchIdUnlockPrivateKeyTag dataUsingEncoding:NSUTF8StringEncoding]; SecAccessControlRef access = NULL; if (@available(macOS 10.13.4, *)) { SecAccessControlCreateFlags flags = kSecAccessControlBiometryCurrentSet; @@ -228,7 +229,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; - (NSData*) _touchIdEncryptCompositeKey: (KPKCompositeKey*) compositeKey { NSData* encryptedKey = nil; NSData* keyData = [NSKeyedArchiver archivedDataWithRootObject:compositeKey]; - NSData* tag = [@"com.hicknhacksoftware.macpass.publickey" dataUsingEncoding:NSUTF8StringEncoding]; + NSData* tag = [TouchIdUnlockPublicKeyTag dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *getquery = @{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrApplicationTag: tag, @@ -265,7 +266,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; - (KPKCompositeKey*) _touchIdDecryptCompositeKey: (NSData*) encryptedKey { KPKCompositeKey* result = nil; if(encryptedKey != nil) { - NSData* tag = [@"com.hicknhacksoftware.macpass.privatekey" dataUsingEncoding:NSUTF8StringEncoding]; + NSData* tag = [TouchIdUnlockPrivateKeyTag dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *queryPrivateKey = @{ (id)kSecClass: (id)kSecClassKey, (id)kSecAttrApplicationTag: tag, @@ -333,6 +334,9 @@ static NSMutableDictionary* touchIDSecuredPasswords; self.completionHandler(compositeKey, nil, false, &error); [self _showError:error]; } + else { + self.touchIdButton.hidden = true; + } } - (IBAction)touchIdEnabledChanged:(id)sender { From 7b79c0b81438b6f2ddfc2e415a4434e71508ee1f Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sun, 14 Feb 2021 17:56:29 +0100 Subject: [PATCH 16/22] Added tooltip to TouchID enabled button. This is a first attempt to make it better to understand what the mixed state is all about. --- MacPass/MPPasswordInputController.m | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/MacPass/MPPasswordInputController.m b/MacPass/MPPasswordInputController.m index 6fdb30f0..356fa72c 100644 --- a/MacPass/MPPasswordInputController.m +++ b/MacPass/MPPasswordInputController.m @@ -95,6 +95,7 @@ static NSMutableDictionary* touchIDSecuredPasswords; if (@available(macOS 10.13.4, *)) { self.touchIdEnabled.hidden = false; self.touchIdEnabled.state = [NSUserDefaults.standardUserDefaults integerForKey:kMPSettingsKeyEntryTouchIdEnabled]; + [self _updateTouchIdTooltip]; } [self _reset]; } @@ -341,6 +342,19 @@ static NSMutableDictionary* touchIDSecuredPasswords; - (IBAction)touchIdEnabledChanged:(id)sender { [NSUserDefaults.standardUserDefaults setInteger: self.touchIdEnabled.state forKey:kMPSettingsKeyEntryTouchIdEnabled]; + [self _updateTouchIdTooltip]; +} + +- (void) _updateTouchIdTooltip { + if(self.touchIdEnabled.state == NSControlStateValueOn) { + self.touchIdEnabled.toolTip = @"Unlocking via TouchID is enabled"; + } + else if(self.touchIdEnabled.state == NSControlStateValueOff) { + self.touchIdEnabled.toolTip = @"Unlocking via TouchID is disabled"; + } + else { + self.touchIdEnabled.toolTip = @"Unlocking via TouchID is possible until MacPass is restarted"; + } } - (IBAction)resetKeyFile:(id)sender { From 88a5af995e99226997092edbb5ab450410dc942c Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sun, 14 Feb 2021 17:58:06 +0100 Subject: [PATCH 17/22] Small layout changes. This prevents the error message overlapping the TouchID enabled button. --- MacPass/Base.lproj/PasswordInputView.xib | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/MacPass/Base.lproj/PasswordInputView.xib b/MacPass/Base.lproj/PasswordInputView.xib index f455e2ef..9f9791c5 100644 --- a/MacPass/Base.lproj/PasswordInputView.xib +++ b/MacPass/Base.lproj/PasswordInputView.xib @@ -1,8 +1,8 @@ - + - + @@ -42,7 +42,7 @@ - + @@ -50,7 +50,7 @@ - + @@ -75,14 +75,14 @@ - + - +