From 9d056052cc1fc698b9b5e49b1a786ddd24fa4b2e Mon Sep 17 00:00:00 2001 From: James Pickett Date: Fri, 13 Sep 2024 13:50:02 -0700 Subject: [PATCH] first pass poc at macos presence detection --- ee/presence/presence_darwin.go | 102 ++++++++++++++++++++++++++++ ee/presence/presence_darwin_test.go | 48 +++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 ee/presence/presence_darwin.go create mode 100644 ee/presence/presence_darwin_test.go diff --git a/ee/presence/presence_darwin.go b/ee/presence/presence_darwin.go new file mode 100644 index 000000000..1264d4c52 --- /dev/null +++ b/ee/presence/presence_darwin.go @@ -0,0 +1,102 @@ +package presence + +/* +#cgo CFLAGS: -x objective-c -fmodules -fblocks +#cgo LDFLAGS: -framework CoreFoundation -framework LocalAuthentication -framework Foundation +#include +#include +#import + +struct AuthResult { + bool success; // true for success, false for failure + char* error_msg; // Error message if any + int error_code; // Error code if any +}; + +struct AuthResult Authenticate(char const* reason) { + struct AuthResult authResult; + LAContext *myContext = [[LAContext alloc] init]; + NSError *authError = nil; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + NSString *nsReason = [NSString stringWithUTF8String:reason]; + __block bool success = false; + __block NSString *errorMessage = nil; + __block int errorCode = 0; + + // Use LAPolicyDeviceOwnerAuthentication to allow biometrics and password fallback + if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { + [myContext evaluatePolicy:LAPolicyDeviceOwnerAuthentication + localizedReason:nsReason + reply:^(BOOL policySuccess, NSError *error) { + if (policySuccess) { + success = true; // Authentication successful + } else { + success = false; + errorCode = (int)[error code]; + errorMessage = [error localizedDescription]; + if (error.code == LAErrorUserFallback || error.code == LAErrorAuthenticationFailed) { + // Prompting for password + [myContext evaluatePolicy:LAPolicyDeviceOwnerAuthentication + localizedReason:@"Please enter your password" + reply:^(BOOL pwdSuccess, NSError *error) { + if (pwdSuccess) { + success = true; + } else { + success = false; + errorCode = (int)[error code]; + errorMessage = [error localizedDescription]; + } + dispatch_semaphore_signal(sema); + }]; + } else { + errorCode = (int)[error code]; + errorMessage = [error localizedDescription]; + } + } + dispatch_semaphore_signal(sema); + }]; + } else { + success = false; // Cannot evaluate policy + errorCode = (int)[authError code]; + errorMessage = [authError localizedDescription]; + } + + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_release(sema); + + authResult.success = success; + authResult.error_code = errorCode; + if (errorMessage != nil) { + authResult.error_msg = strdup([errorMessage UTF8String]); // Copy error message to C string + } else { + authResult.error_msg = NULL; + } + + return authResult; +} +*/ +import "C" +import ( + "fmt" + "unsafe" +) + +func detect(reason string) (bool, error) { + reasonStr := C.CString(reason) + defer C.free(unsafe.Pointer(reasonStr)) + + result := C.Authenticate(reasonStr) + + // Convert C error message to Go string + if result.error_msg != nil { + defer C.free(unsafe.Pointer(result.error_msg)) + } + errorMessage := C.GoString(result.error_msg) + + // Return success or failure, with an error if applicable + if result.success { + return true, nil + } + + return false, fmt.Errorf("authentication failed: %d %s", int(result.error_code), errorMessage) +} diff --git a/ee/presence/presence_darwin_test.go b/ee/presence/presence_darwin_test.go new file mode 100644 index 000000000..fad50ab5f --- /dev/null +++ b/ee/presence/presence_darwin_test.go @@ -0,0 +1,48 @@ +package presence + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testPresenceEnvVar = "launcher_test_presence" + +// Since there is no way to test user presence in a CI / automated fashion, +// these test are expected to be run manually via cmd line when needed. + +// To test this run +// +// launcher_test_presence=true go test ./ee/presence/ -run Test_biometricDetectSuccess +// +// then successfully auth with the pop up +func Test_biometricDetectSuccess(t *testing.T) { + t.Parallel() + + if os.Getenv(testPresenceEnvVar) == "" { + t.Skip("Skipping test_biometricDetectSuccess") + } + + success, err := detect("IS TRYING TO TEST SUCCESS, PLEASE AUTHENTICATE") + require.NoError(t, err, "should not get an error on successful detect") + assert.True(t, success, "should be successful") +} + +// To test this run +// +// launcher_test_presence=true go test ./ee/presence/ -run Test_biometricDetectCancel +// +// then cancel the biometric auth that pops up +func Test_biometricDetectCancel(t *testing.T) { + t.Parallel() + + if os.Getenv(testPresenceEnvVar) == "" { + t.Skip("Skipping test_biometricDetectCancel") + } + + success, err := detect("IS TRYING TO TEST CANCEL, PLEASE PRESS CANCEL") + require.Error(t, err, "should get an error on failed detect") + assert.False(t, success, "should not be successful") +}