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.
This commit is contained in:
Julius Zint
2020-08-29 13:27:10 +02:00
parent 3fc73a7fd9
commit 92a120c405
4 changed files with 71 additions and 40 deletions

View File

@@ -152,14 +152,14 @@ DQ
<textField hidden="YES" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="txI-yI-5nE">
<rect key="frame" x="157" y="204" width="195" height="14"/>
<textFieldCell key="cell" selectable="YES" title="key_file_warnig" id="f6J-5f-ZvP">
<font key="font" metaFont="menu" size="11"/>
<font key="font" metaFont="toolTip"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Hs8-Tc-ezo">
<rect key="frame" x="72" y="287" width="72" height="18"/>
<buttonCell key="cell" type="check" title="TouchID" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="h3C-Z4-x7N">
<buttonCell key="cell" type="check" title="TouchID" bezelStyle="regularSquare" imagePosition="left" allowsMixedState="YES" inset="2" id="h3C-Z4-x7N">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>

View File

@@ -52,7 +52,7 @@ static NSMutableDictionary* touchIDSecuredPasswords;
@property (copy) NSString *message;
@property (copy) NSString *cancelLabel;
@property (copy) NSString *absoluteURLString;
@property (copy) NSURL *databaseFileURL;
@property (assign) BOOL showPassword;
@property (nonatomic, assign) BOOL enablePassword;
@@ -93,7 +93,7 @@ static NSMutableDictionary* touchIDSecuredPasswords;
self.touchIdEnabled.hidden = true;
if (@available(macOS 10.13.4, *)) {
self.touchIdEnabled.hidden = false;
self.touchIdEnabled.state = [NSUserDefaults.standardUserDefaults boolForKey:kMPSettingsKeyEntryTouchIdEnabled];
self.touchIdEnabled.state = [NSUserDefaults.standardUserDefaults integerForKey:kMPSettingsKeyEntryTouchIdEnabled];
}
[self _reset];
}
@@ -106,12 +106,7 @@ static NSMutableDictionary* touchIDSecuredPasswords;
self.completionHandler = completionHandler;
self.message = message;
self.cancelLabel = cancelLabel;
if(fileURL) {
self.absoluteURLString = [fileURL absoluteString];
}
else {
self.absoluteURLString = nil;
}
self.databaseFileURL = fileURL;
[self _reset];
}
@@ -147,10 +142,8 @@ static NSMutableDictionary* touchIDSecuredPasswords;
NSData *keyFileData = keyURL ? [NSData dataWithContentsOfURL:keyURL] : nil;
KPKCompositeKey *compositeKey = [[KPKCompositeKey alloc] initWithPassword:password keyFileData:keyFileData];
BOOL result = self.completionHandler(compositeKey, keyURL, cancel, &error);
[self _touchIdHandleUnlockAttempt:compositeKey withResult:result];
if(cancel || result) {
if(result && self.touchIdEnabled.state) {
[self _storePasswordForTouchIDUnlock:compositeKey forDatabase:self.absoluteURLString];
}
return;
}
[self _showError:error];
@@ -160,7 +153,21 @@ static NSMutableDictionary* touchIDSecuredPasswords;
}
}
- (void) _createAndAddRSAKeyPair {
- (void) _touchIdHandleUnlockAttempt: (KPKCompositeKey*)compositeKey withResult:(bool)success {
if(success && self.databaseFileURL && self.databaseFileURL.lastPathComponent) {
NSData* encryptedKey = [self _touchIdEncryptCompositeKey:compositeKey];
if(encryptedKey) {
if (self.touchIdEnabled.state == NSControlStateValueMixed) {
[touchIDSecuredPasswords setObject:encryptedKey forKey:self.databaseFileURL.lastPathComponent];
}
else if(self.touchIdEnabled.state == NSControlStateValueOn) {
[NSUserDefaults.standardUserDefaults setObject:encryptedKey forKey:[self _userDefaultsKeyForEncryptedCompositeKey]];
}
}
}
}
- (void) _touchIdCreateAndAddRSAKeyPair {
CFErrorRef error = NULL;
NSString* publicKeyLabel = @"MacPass TouchID Feature Public Key";
NSString* privateKeyLabel = @"MacPass TouchID Feature Private Key";
@@ -210,7 +217,8 @@ static NSMutableDictionary* touchIDSecuredPasswords;
}
}
- (void) _storePasswordForTouchIDUnlock: (KPKCompositeKey*) compositeKey forDatabase: (NSString*) databaseId {
- (NSData*) _touchIdEncryptCompositeKey: (KPKCompositeKey*) compositeKey {
NSData* encryptedKey = nil;
NSData* keyData = [NSKeyedArchiver archivedDataWithRootObject:compositeKey];
NSData* tag = [@"com.hicknhacksoftware.macpass.publickey" dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *getquery = @{
@@ -221,23 +229,20 @@ static NSMutableDictionary* touchIDSecuredPasswords;
SecKeyRef publicKey = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)getquery, (CFTypeRef *)&publicKey);
if (status != errSecSuccess) {
[self _createAndAddRSAKeyPair];
[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;
return nil;
}
}
SecKeyAlgorithm algorithm = kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM;
BOOL canEncrypt = SecKeyIsAlgorithmSupported(publicKey, kSecKeyOperationTypeEncrypt, algorithm);
if(canEncrypt) {
CFErrorRef error = NULL;
NSData* cipherText = (NSData*)CFBridgingRelease(SecKeyCreateEncryptedData(publicKey, algorithm, (__bridge CFDataRef)keyData, &error));
if (cipherText) {
[touchIDSecuredPasswords setObject:cipherText forKey:databaseId];
}
else {
encryptedKey = (NSData*)CFBridgingRelease(SecKeyCreateEncryptedData(publicKey, algorithm, (__bridge CFDataRef)keyData, &error));
if (!encryptedKey) {
NSError *err = CFBridgingRelease(error);
NSLog(@"Error while trying decrypt password for TouchID unlock: %@", [err description]);
}
@@ -246,12 +251,12 @@ static NSMutableDictionary* touchIDSecuredPasswords;
NSLog(@"The key retreived from the Keychain is unable to encrypt data");
}
if (publicKey) { CFRelease(publicKey); }
return encryptedKey;
}
- (KPKCompositeKey*) _loadPasswordForTochIDUnlock: (NSString*) databaseId {
- (KPKCompositeKey*) _touchIdDecryptCompositeKey: (NSData*) encryptedKey {
KPKCompositeKey* result = nil;
NSData* cipherText = [touchIDSecuredPasswords valueForKey:databaseId];
if(cipherText != nil) {
if(encryptedKey != nil) {
NSData* tag = [@"com.hicknhacksoftware.macpass.privatekey" dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *queryPrivateKey = @{
(id)kSecClass: (id)kSecClassKey,
@@ -266,7 +271,7 @@ static NSMutableDictionary* touchIDSecuredPasswords;
BOOL canDecrypt = SecKeyIsAlgorithmSupported(privateKey, kSecKeyOperationTypeDecrypt, algorithm);
if(canDecrypt) {
CFErrorRef error = NULL;
NSData* clearText = (NSData*)CFBridgingRelease(SecKeyCreateDecryptedData(privateKey, algorithm, (__bridge CFDataRef)cipherText, &error));
NSData* clearText = (NSData*)CFBridgingRelease(SecKeyCreateDecryptedData(privateKey, algorithm, (__bridge CFDataRef)encryptedKey, &error));
if (clearText) {
result = [NSKeyedUnarchiver unarchiveObjectWithData:clearText];
}
@@ -290,6 +295,42 @@ static NSMutableDictionary* touchIDSecuredPasswords;
return result;
}
- (NSString*) _userDefaultsKeyForEncryptedCompositeKey {
NSString* result = [NSString stringWithFormat:kMPSettingsKeyEntryTouchIdDatabaseEncryptedKeyFormat, self.databaseFileURL.lastPathComponent];
return result;
}
- (bool) _touchIdIsUnlockAvailable {
bool result = false;
if(self.databaseFileURL != nil && self.databaseFileURL.lastPathComponent != nil)
{
if ([touchIDSecuredPasswords valueForKey:self.databaseFileURL.lastPathComponent] != nil) {
result = true;
}
else if([NSUserDefaults.standardUserDefaults dataForKey:[self _userDefaultsKeyForEncryptedCompositeKey]] != nil) {
result = true;
}
}
return result;
}
- (IBAction)unlockWithTouchID:(id)sender {
NSData* encryptedKey = [touchIDSecuredPasswords valueForKey:self.databaseFileURL.lastPathComponent];
if(!encryptedKey) {
encryptedKey = [NSUserDefaults.standardUserDefaults dataForKey:[self _userDefaultsKeyForEncryptedCompositeKey]];
}
KPKCompositeKey* compositeKey = [self _touchIdDecryptCompositeKey:encryptedKey];
if(compositeKey != nil) {
NSError* error;
self.completionHandler(compositeKey, nil, false, &error);
[self _showError:error];
}
}
- (IBAction)touchIdEnabledChanged:(id)sender {
[NSUserDefaults.standardUserDefaults setInteger: self.touchIdEnabled.state forKey:kMPSettingsKeyEntryTouchIdEnabled];
}
- (IBAction)resetKeyFile:(id)sender {
/* If the reset was triggered by ourselves we want to preselect the keyfile */
if(sender == self) {
@@ -299,17 +340,14 @@ static NSMutableDictionary* touchIDSecuredPasswords;
self.keyPathControl.URL = nil;
}
}
- (IBAction)touchIdEnabledChanged:(id)sender {
[NSUserDefaults.standardUserDefaults setBool:self.touchIdEnabled.state forKey:kMPSettingsKeyEntryTouchIdEnabled];
}
- (void)_reset {
self.showPassword = NO;
self.enablePassword = YES;
self.passwordTextField.stringValue = @"";
self.messageInfoTextField.hidden = (nil == self.message);
self.touchIdButton.hidden = [touchIDSecuredPasswords valueForKey:self.absoluteURLString] == nil;
self.touchIdButton.hidden = ![self _touchIdIsUnlockAvailable];
if(self.message) {
self.messageInfoTextField.stringValue = self.message;
self.messageImageView.image = [NSImage imageNamed:NSImageNameInfo];
@@ -393,13 +431,4 @@ static NSMutableDictionary* touchIDSecuredPasswords;
}
}
- (IBAction)unlockWithTouchID:(id)sender {
KPKCompositeKey* compositeKey = [self _loadPasswordForTochIDUnlock:self.absoluteURLString];
if(compositeKey != nil) {
NSError* error;
self.completionHandler(compositeKey, nil, false, &error);
[self _showError:error];
}
}
@end

View File

@@ -24,6 +24,7 @@
/* TouchID */
APPKIT_EXTERN NSString *const kMPSettingsKeyEntryTouchIdEnabled;
APPKIT_EXTERN NSString *const kMPSettingsKeyEntryTouchIdDatabaseEncryptedKeyFormat;
/* Clipboard */
APPKIT_EXTERN NSString *const kMPSettingsKeyPasteboardClearTimeout;

View File

@@ -63,6 +63,7 @@ NSString *const kMPSettingsKeyAutotypeMatchTags = @"Au
NSString *const kMPSettingsKeyGloablAutotypeAlwaysShowCandidateSelection = @"GloablAutotypeAlwaysShowCandidateSelection";
NSString *const kMPSettingsKeyEntryTouchIdEnabled = @"EnableSubsequentUnlocksWithTouchID";
NSString *const kMPSettingsKeyEntryTouchIdDatabaseEncryptedKeyFormat = @"EncryptedDatabaseKeyForTouchID-%@";
NSString *const kMPSettingsKeyEntrySearchFilterContext = @"EntrySearchFilterContext";