Skip to content

Commit

Permalink
Make "Use Password" button in TouchID prompt work
Browse files Browse the repository at this point in the history
A small but important note: the use password button will make
`pinentry-touchid` fall back to `pinentry-mac` for the passphase. If
`pinentry-mac` has it saved in the keychain and has previously been
given "Always allow" access, the entire touchid prompt can be bypassed
trivially.
  • Loading branch information
eth-p committed Apr 23, 2023
1 parent afb7d78 commit bf44be6
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 26 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ macOS keychain.
This program interacts with the `gpg-agent` for providing a password, using the following rules:

- If the password entry for the given key cannot be found in the Keychain we fallback to the
`pinentry-mac` program to get the password. We recommend preventing `pinentry-mac` from storing the
`pinentry-mac` program to get the password. We *strongly* recommend preventing `pinentry-mac` from storing the
password: uncheck the <kbd>Save in keychain</kbd> checkbox in the dialog.

- If a password entry is found the user will be shown the Touch ID dialog and upon successful
Expand Down
78 changes: 63 additions & 15 deletions go-touchid/touchid.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,98 @@ package touchid
#include <stdio.h>
#import <LocalAuthentication/LocalAuthentication.h>
int Authenticate(char const* reason) {
typedef struct {
BOOL success;
int errorCode;
} TouchIDAuthenticateResult;
void Authenticate(char const* reason, TouchIDAuthenticateResult* result) {
LAContext *myContext = [[LAContext alloc] init];
NSError *authError = nil;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSString *nsReason = [NSString stringWithUTF8String:reason];
__block int result = 0;
result->success = false;
result->errorCode = 0;
if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) {
[myContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:nsReason
reply:^(BOOL success, NSError *error) {
if (success) {
result = 1;
} else {
result = 2;
result->success = success;
if (!success && error != NULL) {
result->errorCode = [error code];
}
dispatch_semaphore_signal(sema);
}];
}
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(sema);
return result;
}
*/
import (
"C"
)
import (
"errors"
"fmt"
"unsafe"
)

// AuthError is a Go-ified version of the Local Authentication Framework's
// [LAError enum](https://developer.apple.com/documentation/localauthentication/laerror?language=objc).
type AuthError struct {
Code int
}

func (e AuthError) Error() string {
return fmt.Sprintf("Error occurred accessing biometrics: Code %d", e.Code)
}

// Authenticate is called to show a TouchID prompt to the user.
//
// If successful, `true` will be returned.
// If unsuccessful, `false` will be returned with an error indicating why.
func Authenticate(reason string) (bool, error) {
reasonStr := C.CString(reason)
defer C.free(unsafe.Pointer(reasonStr))

result := C.Authenticate(reasonStr)
switch result {
case 1:
// Call the Objective-C code.
var result C.TouchIDAuthenticateResult
C.Authenticate(reasonStr, &result)

if result.success {
return true, nil
case 2:
return false, nil
}

return false, errors.New("Error occurred accessing biometrics")
}
// Create an AuthError to return.
return false, AuthError{
Code: int(result.errorCode),
}
}

func isErrorOfType(e error, LAErrorCode int) bool {
if laerror, ok := e.(AuthError); ok {
return laerror.Code == LAErrorCode
}

return false
}

// DidUserCancel checks if the error indicates that the user cancelled the authentication dialog.
// If the type of error provided is not an AuthError, this will return false.
func DidUserCancel(e error) bool {
return isErrorOfType(e, C.kLAErrorUserCancel)
}

// DidUserFallback checks if the error indicates that the user tapped the "Enter password..." button.
// If the type of error provided is not an AuthError, this will return false.
func DidUserFallback(e error) bool {
return isErrorOfType(e, C.kLAErrorUserFallback)
}

// DidAuthenticationFail checks if the error indicates that the user failed to provide valid credentials.
// If the type of error provided is not an AuthError, this will return false.
func DidAuthenticationFail(e error) bool {
return isErrorOfType(e, C.kLAErrorAuthenticationFailed)
}
36 changes: 26 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,22 +347,38 @@ func GetPIN(authFn AuthFunc, promptFn PromptFunc, logger *log.Logger) GetPinFunc
}

var ok bool
if ok, err = authFn(fmt.Sprintf("access the PIN for %s", keychainLabel)); err != nil {
logger.Printf("Error authenticating with Touch ID: %s", err)
return "", assuanError(err)
ok, err = authFn(fmt.Sprintf("access the PIN for %s", keychainLabel))

// If the auth was successful, fetch the password from the keychain.
if ok {
password, err := passwordFromKeychain(keychainLabel)
if err != nil {
logger.Printf("Error fetching password from Keychain %s", err)
}

return password, nil
}

if !ok {
logger.Printf("Failed to authenticate")
return "", nil
// If the user opted to use manual entry, fetch the password from pinentry-mac.
if touchid.DidUserFallback(err) {
logger.Printf("User opted to enter password manually")
pin, err := promptFn(s)
if err != nil {
logger.Printf("Error calling pinentry program (%s): %s", pinentryBinary.GetBinary(), err)
}

return string(pin), nil
}

password, err := passwordFromKeychain(keychainLabel)
if err != nil {
log.Printf("Error fetching password from Keychain %s", err)
// User cancelled.
if touchid.DidUserCancel(err) {
logger.Printf("Authentication cancelled")
return "", nil
}

return password, nil
// Other error.
logger.Printf("Failed to authenticate: %s", err)
return "", assuanError(err)
}
}

Expand Down

0 comments on commit bf44be6

Please sign in to comment.