Skip to content

Commit

Permalink
TouchID unlock Feature for MacPass.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Julius Zint committed Feb 14, 2021
1 parent 4addd90 commit a7b8be1
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 31 deletions.
85 changes: 54 additions & 31 deletions MacPass/Base.lproj/PasswordInputView.xib
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<outlet property="messageInfoTextField" destination="268" id="ahE-sq-QzR"/>
<outlet property="passwordTextField" destination="338" id="495"/>
<outlet property="togglePasswordButton" destination="408" id="493"/>
<outlet property="touchIdButton" destination="mQA-C0-JyU" id="fM3-PG-1OB"/>
<outlet property="unlockButton" destination="2" id="ZRr-Ui-ExP"/>
<outlet property="view" destination="1" id="143"/>
</connections>
Expand All @@ -25,19 +26,6 @@
<customView horizontalCompressionResistancePriority="751" translatesAutoresizingMaskIntoConstraints="NO" id="1">
<rect key="frame" x="0.0" y="0.0" width="508" height="526"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2">
<rect key="frame" x="310" y="168" width="83" height="32"/>
<buttonCell key="cell" type="push" title="Unlock" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="3">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="_submit:" target="-2" id="KZN-ap-nDc"/>
</connections>
</button>
<textField verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="17">
<rect key="frame" x="108" y="226" width="45" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Keyfile" id="18">
Expand Down Expand Up @@ -109,30 +97,67 @@ DQ
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2pb-ZG-spA">
<rect key="frame" x="228" y="168" width="82" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="erj-mR-UyO">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
<stackView distribution="fill" orientation="horizontal" alignment="top" spacing="12" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tck-n8-s0U" userLabel="SubmitButtonContainer">
<rect key="frame" x="140" y="175" width="247" height="21"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2pb-ZG-spA">
<rect key="frame" x="-6" y="-7" width="82" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="erj-mR-UyO">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="_submit:" target="-2" id="aVF-1d-1Hq"/>
</connections>
</button>
</buttonCell>
<connections>
<action selector="_submit:" target="-2" id="aVF-1d-1Hq"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mQA-C0-JyU">
<rect key="frame" x="76" y="-7" width="94" height="32"/>
<buttonCell key="cell" type="push" title="Touch ID" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="W9h-1u-9kq">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="unlockWithTouchID:" target="-2" id="1yL-6V-t8q"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2">
<rect key="frame" x="170" y="-7" width="83" height="32"/>
<buttonCell key="cell" type="push" title="Unlock" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="3">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="_submit:" target="-2" id="KZN-ap-nDc"/>
</connections>
</button>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<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="smallSystem"/>
<font key="font" metaFont="menu" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="2" secondAttribute="bottom" constant="20" symbolic="YES" id="122"/>
<constraint firstItem="262" firstAttribute="top" relation="greaterThanOrEqual" secondItem="1" secondAttribute="top" constant="20" symbolic="YES" id="276"/>
<constraint firstAttribute="centerX" secondItem="262" secondAttribute="centerX" id="286"/>
<constraint firstAttribute="centerY" secondItem="338" secondAttribute="centerY" id="386"/>
Expand All @@ -150,19 +175,17 @@ Gw
<constraint firstItem="486" firstAttribute="leading" secondItem="241" secondAttribute="trailing" constant="8" symbolic="YES" id="489"/>
<constraint firstItem="486" firstAttribute="leading" secondItem="408" secondAttribute="leading" id="490"/>
<constraint firstItem="408" firstAttribute="trailing" secondItem="486" secondAttribute="trailing" id="492"/>
<constraint firstItem="2" firstAttribute="trailing" secondItem="486" secondAttribute="trailing" id="496"/>
<constraint firstItem="tck-n8-s0U" firstAttribute="top" secondItem="txI-yI-5nE" secondAttribute="bottom" constant="8" id="6tz-an-3SW"/>
<constraint firstItem="408" firstAttribute="leading" secondItem="338" secondAttribute="trailing" constant="8" symbolic="YES" id="7qE-8F-QgB"/>
<constraint firstItem="2pb-ZG-spA" firstAttribute="baseline" secondItem="2" secondAttribute="baseline" id="9nK-MH-Ozs"/>
<constraint firstItem="txI-yI-5nE" firstAttribute="trailing" secondItem="241" secondAttribute="trailing" id="AVL-HO-SMq"/>
<constraint firstItem="17" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="1" secondAttribute="leading" constant="20" symbolic="YES" id="EOa-K4-v7J"/>
<constraint firstItem="338" firstAttribute="leading" secondItem="d8O-Ha-rrS" secondAttribute="trailing" constant="8" symbolic="YES" id="KYs-Ia-SVl"/>
<constraint firstItem="2pb-ZG-spA" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="1" secondAttribute="leading" constant="20" symbolic="YES" id="SUS-76-os4"/>
<constraint firstItem="2" firstAttribute="top" secondItem="txI-yI-5nE" secondAttribute="bottom" constant="8" symbolic="YES" id="jJs-hc-O2O"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="tck-n8-s0U" secondAttribute="bottom" constant="20" symbolic="YES" id="Sny-FR-cY1"/>
<constraint firstItem="tck-n8-s0U" firstAttribute="trailing" secondItem="486" secondAttribute="trailing" id="UtJ-18-p5u"/>
<constraint firstItem="d8O-Ha-rrS" firstAttribute="centerY" secondItem="338" secondAttribute="centerY" id="kgB-jV-OGy"/>
<constraint firstItem="txI-yI-5nE" firstAttribute="top" secondItem="241" secondAttribute="bottom" constant="8" symbolic="YES" id="lfg-eB-T2O"/>
<constraint firstItem="txI-yI-5nE" firstAttribute="leading" secondItem="241" secondAttribute="leading" id="nGY-6Q-Vwy"/>
<constraint firstItem="d8O-Ha-rrS" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="1" secondAttribute="leading" constant="20" symbolic="YES" id="vxq-YP-UhR"/>
<constraint firstItem="2" firstAttribute="leading" secondItem="2pb-ZG-spA" secondAttribute="trailing" constant="12" id="ytJ-5Z-5rT"/>
</constraints>
<point key="canvasLocation" x="-127" y="-46"/>
</customView>
Expand Down
164 changes: 164 additions & 0 deletions MacPass/MPPasswordInputController.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

#import "NSError+Messages.h"

static NSMutableDictionary* touchIDSecuredPasswords;

@interface MPPasswordInputController ()

@property (strong) NSButton *showPasswordButton;
Expand All @@ -44,6 +46,7 @@ @interface MPPasswordInputController ()
@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;
Expand All @@ -64,6 +67,9 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
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;
Expand Down Expand Up @@ -129,6 +135,9 @@ - (IBAction)_submit:(id)sender {
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];
Expand All @@ -138,6 +147,150 @@ - (IBAction)_submit:(id)sender {
}
}

- (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) {
Expand All @@ -153,6 +306,8 @@ - (void)_reset {
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];
Expand Down Expand Up @@ -236,5 +391,14 @@ - (void)_didSetKeyURL:(NSNotification *)notification {
}
}

- (IBAction)unlockWithTouchID:(id)sender {
NSString* password = [self _loadPasswordForTochIDUnlock:@"DatabaseID"];
if(password != nil) {
NSError* error;
self.completionHandler(password, nil, false, &error);
[self _showError:error];
}
}


@end

0 comments on commit a7b8be1

Please sign in to comment.