From a7b8be18861bb891cadc0eb0acb083e799d6729f Mon Sep 17 00:00:00 2001 From: Julius Zint Date: Sat, 20 Jun 2020 11:36:02 +0200 Subject: [PATCH] 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