diff --git a/lib/auth/touchid/api.go b/lib/auth/touchid/api.go index 9892b6f0dab5a..f70a2f08053ee 100644 --- a/lib/auth/touchid/api.go +++ b/lib/auth/touchid/api.go @@ -57,17 +57,31 @@ func promptPlatform() { } } +// AuthContext is an optional, shared authentication context. +// Allows reusing a single authentication prompt/gesture between different +// functions, provided the functions are invoked in a short time interval. +// Only used by native touchid implementations. +type AuthContext interface { + // Guard guards the invocation of fn behind an authentication check. + Guard(fn func()) error + // Close closes the context, releasing any held resources. + Close() +} + // nativeTID represents the native Touch ID interface. // Implementors must provide a global variable called `native`. type nativeTID interface { Diag() (*DiagResult, error) + // NewAuthContext creates a new AuthContext. + NewAuthContext() AuthContext + // Register creates a new credential in the Secure Enclave. Register(rpID, user string, userHandle []byte) (*CredentialInfo, error) // Authenticate authenticates using the specified credential. // Requires user interaction. - Authenticate(credentialID string, digest []byte) ([]byte, error) + Authenticate(actx AuthContext, credentialID string, digest []byte) ([]byte, error) // FindCredentials finds credentials without user interaction. // An empty user means "all users". @@ -281,7 +295,7 @@ func Register(origin string, cc *wanlib.CredentialCreation) (*Registration, erro } promptPlatform() - sig, err := native.Authenticate(credentialID, attData.digest) + sig, err := native.Authenticate(nil /* actx */, credentialID, attData.digest) if err != nil { return nil, trace.Wrap(err) } @@ -433,8 +447,6 @@ func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib. return nil, "", errors.New("relying party ID required") } - // TODO(codingllama): Share the same LAContext between search and - // authentication, so we can protect both with user interaction. rpID := assertion.Response.RelyingPartyID infos, err := native.FindCredentials(rpID, user) switch { @@ -457,15 +469,25 @@ func Login(origin, user string, assertion *wanlib.CredentialAssertion) (*wanlib. if !ok { return nil, "", ErrCredentialNotFound } - log.Debugf("Touch ID: using credential %q", cred.CredentialID) + + // Guard first read of chosen credential with an explicit check. + // A more meaningful check can be made once the credential picker is + // implemented. + actx := native.NewAuthContext() + defer actx.Close() + promptPlatform() + if err := actx.Guard(func() { + log.Debugf("Touch ID: using credential %q", cred.CredentialID) + }); err != nil { + return nil, "", trace.Wrap(err) + } attData, err := makeAttestationData(protocol.AssertCeremony, origin, rpID, assertion.Response.Challenge, nil /* cred */) if err != nil { return nil, "", trace.Wrap(err) } - promptPlatform() - sig, err := native.Authenticate(cred.CredentialID, attData.digest) + sig, err := native.Authenticate(actx, cred.CredentialID, attData.digest) if err != nil { return nil, "", trace.Wrap(err) } diff --git a/lib/auth/touchid/api_darwin.go b/lib/auth/touchid/api_darwin.go index af17f2bac1561..3a36ad3b8d768 100644 --- a/lib/auth/touchid/api_darwin.go +++ b/lib/auth/touchid/api_darwin.go @@ -21,6 +21,7 @@ package touchid // #cgo LDFLAGS: -framework CoreFoundation -framework Foundation -framework LocalAuthentication -framework Security // #include // #include "authenticate.h" +// #include "context.h" // #include "credential_info.h" // #include "credentials.h" // #include "diag.h" @@ -29,8 +30,8 @@ import "C" import ( "encoding/base64" - "errors" "fmt" + "runtime/cgo" "strings" "time" "unsafe" @@ -50,6 +51,10 @@ const ( // rpID are domain names, so it's safe to assume they won't have spaces in them. // https://www.w3.org/TR/webauthn-2/#relying-party-identifier labelSeparator = " " + + // promptReason is the LAContext / Touch ID prompt. + // The final prompt is: "$binary is trying to authenticate user". + promptReason = "authenticate user" ) type parsedLabel struct { @@ -100,6 +105,66 @@ func (touchIDImpl) Diag() (*DiagResult, error) { }, nil } +//export runGoFuncHandle +func runGoFuncHandle(handle C.uintptr_t) { + val := cgo.Handle(handle).Value() + fn, ok := val.(func()) + if !ok { + log.Warnf("Touch ID: received unexpected function handle: %T", val) + return + } + fn() +} + +// touchIDContext wraps C.AuthContext into an authContext shell. +type touchIDContext struct { + ctx *C.AuthContext +} + +func (c *touchIDContext) Guard(fn func()) error { + reasonC := C.CString(promptReason) + defer C.free(unsafe.Pointer(reasonC)) + + // Passing Go function pointers directly to CGO is not doable, so we pass a + // handle and have an exported Go function run it. + // See https://github.com/golang/go/wiki/cgo#function-variables. + handle := cgo.NewHandle(fn) + defer handle.Delete() + + var errMsgC *C.char + defer C.free(unsafe.Pointer(errMsgC)) + + res := C.AuthContextGuard(c.ctx, reasonC, C.uintptr_t(handle), &errMsgC) + if res != 0 { + errMsg := C.GoString(errMsgC) + return errorFromStatus("guard", int(res), errMsg) + } + + return nil +} + +func (c *touchIDContext) Close() { + if c.ctx == nil { + return + } + C.AuthContextClose(c.ctx) + c.ctx = nil +} + +// getNativeContext returns the C.AuthContext within ctx, or nil. +func getNativeContext(ctx AuthContext) *C.AuthContext { + if tctx, ok := ctx.(*touchIDContext); ok { + return tctx.ctx + } + return nil +} + +func (touchIDImpl) NewAuthContext() AuthContext { + return &touchIDContext{ + ctx: &C.AuthContext{}, + } +} + func (touchIDImpl) Register(rpID, user string, userHandle []byte) (*CredentialInfo, error) { credentialID := uuid.NewString() userHandleB64 := base64.RawURLEncoding.EncodeToString(userHandle) @@ -122,7 +187,7 @@ func (touchIDImpl) Register(rpID, user string, userHandle []byte) (*CredentialIn if res := C.Register(req, &pubKeyC, &errMsgC); res != 0 { errMsg := C.GoString(errMsgC) - return nil, errors.New(errMsg) + return nil, errorFromStatus("register", int(res), errMsg) } pubKeyB64 := C.GoString(pubKeyC) @@ -137,7 +202,9 @@ func (touchIDImpl) Register(rpID, user string, userHandle []byte) (*CredentialIn }, nil } -func (touchIDImpl) Authenticate(credentialID string, digest []byte) ([]byte, error) { +func (touchIDImpl) Authenticate(actx AuthContext, credentialID string, digest []byte) ([]byte, error) { + authCtx := getNativeContext(actx) + var req C.AuthenticateRequest req.app_label = C.CString(credentialID) req.digest = (*C.char)(C.CBytes(digest)) @@ -153,9 +220,9 @@ func (touchIDImpl) Authenticate(credentialID string, digest []byte) ([]byte, err C.free(unsafe.Pointer(errMsgC)) }() - if res := C.Authenticate(req, &sigOutC, &errMsgC); res != 0 { + if res := C.Authenticate(authCtx, req, &sigOutC, &errMsgC); res != 0 { errMsg := C.GoString(errMsgC) - return nil, errors.New(errMsg) + return nil, errorFromStatus("authenticate", int(res), errMsg) } sigB64 := C.GoString(sigOutC) @@ -174,14 +241,13 @@ func (touchIDImpl) FindCredentials(rpID, user string) ([]CredentialInfo, error) return C.FindCredentials(filterC, infosC) }) if res < 0 { - return nil, trace.BadParameter("failed to find credentials: status %d", res) + return nil, errorFromStatus("finding credentials", res, "" /* msg */) } return infos, nil } func (touchIDImpl) ListCredentials() ([]CredentialInfo, error) { - // User prompt becomes: ""$binary" is trying to list credentials". - reasonC := C.CString("list credentials") + reasonC := C.CString(promptReason) defer C.free(unsafe.Pointer(reasonC)) var errMsgC *C.char @@ -197,7 +263,7 @@ func (touchIDImpl) ListCredentials() ([]CredentialInfo, error) { }) if res < 0 { errMsg := C.GoString(errMsgC) - return nil, errors.New(errMsg) + return nil, errorFromStatus("listing credentials", int(res), errMsg) } return infos, nil @@ -283,8 +349,7 @@ func readCredentialInfos(find func(**C.CredentialInfo) C.int) ([]CredentialInfo, const errSecItemNotFound = -25300 func (touchIDImpl) DeleteCredential(credentialID string) error { - // User prompt becomes: ""$binary" is trying to delete credential". - reasonC := C.CString("delete credential") + reasonC := C.CString(promptReason) defer C.free(unsafe.Pointer(reasonC)) idC := C.CString(credentialID) @@ -293,14 +358,14 @@ func (touchIDImpl) DeleteCredential(credentialID string) error { var errC *C.char defer C.free(unsafe.Pointer(errC)) - switch C.DeleteCredential(reasonC, idC, &errC) { + switch res := C.DeleteCredential(reasonC, idC, &errC); res { case 0: // aka success return nil case errSecItemNotFound: return ErrCredentialNotFound default: errMsg := C.GoString(errC) - return errors.New(errMsg) + return errorFromStatus("delete credential", int(res), errMsg) } } @@ -308,12 +373,19 @@ func (touchIDImpl) DeleteNonInteractive(credentialID string) error { idC := C.CString(credentialID) defer C.free(unsafe.Pointer(idC)) - switch status := C.DeleteNonInteractive(idC); status { + switch res := C.DeleteNonInteractive(idC); res { case 0: // aka success return nil case errSecItemNotFound: return ErrCredentialNotFound default: - return fmt.Errorf("non-interactive delete failed: status %d", status) + return errorFromStatus("non-interactive delete", int(res), "" /* msg */) + } +} + +func errorFromStatus(prefix string, status int, msg string) error { + if msg != "" { + return fmt.Errorf("%v: %v", prefix, msg) } + return fmt.Errorf("%v: status %d", prefix, status) } diff --git a/lib/auth/touchid/api_other.go b/lib/auth/touchid/api_other.go index ed00e5742cdf0..8ed0b5a6d9485 100644 --- a/lib/auth/touchid/api_other.go +++ b/lib/auth/touchid/api_other.go @@ -25,11 +25,23 @@ func (noopNative) Diag() (*DiagResult, error) { return &DiagResult{}, nil } +type noopAuthContext struct{} + +func (noopAuthContext) Guard(fn func()) error { + return ErrNotAvailable +} + +func (noopAuthContext) Close() {} + +func (noopNative) NewAuthContext() AuthContext { + return noopAuthContext{} +} + func (noopNative) Register(rpID, user string, userHandle []byte) (*CredentialInfo, error) { return nil, ErrNotAvailable } -func (noopNative) Authenticate(credentialID string, digest []byte) ([]byte, error) { +func (noopNative) Authenticate(actx AuthContext, credentialID string, digest []byte) ([]byte, error) { return nil, ErrNotAvailable } diff --git a/lib/auth/touchid/api_test.go b/lib/auth/touchid/api_test.go index 8f54ef7aa9f96..fb4eebae3555c 100644 --- a/lib/auth/touchid/api_test.go +++ b/lib/auth/touchid/api_test.go @@ -22,10 +22,12 @@ import ( "crypto/rand" "encoding/json" "errors" + "io" "testing" "time" "github.com/duo-labs/webauthn/protocol" + "github.com/duo-labs/webauthn/protocol/webauthncose" "github.com/duo-labs/webauthn/webauthn" "github.com/google/uuid" "github.com/gravitational/teleport/lib/auth/touchid" @@ -35,6 +37,11 @@ import ( wanlib "github.com/gravitational/teleport/lib/auth/webauthn" ) +func init() { + // Make tests silent. + touchid.PromptWriter = io.Discard +} + func TestRegisterAndLogin(t *testing.T) { n := *touchid.Native t.Cleanup(func() { @@ -69,7 +76,8 @@ func TestRegisterAndLogin(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - *touchid.Native = &fakeNative{} + fake := &fakeNative{} + *touchid.Native = fake webUser := test.webUser origin := test.origin @@ -81,6 +89,7 @@ func TestRegisterAndLogin(t *testing.T) { reg, err := touchid.Register(origin, (*wanlib.CredentialCreation)(cc)) require.NoError(t, err, "Register failed") + assert.Equal(t, 1, fake.userPrompts, "unexpected number of Registation prompts") // We have to marshal and parse ccr due to an unavoidable quirk of the // webauthn API. @@ -106,6 +115,7 @@ func TestRegisterAndLogin(t *testing.T) { assertionResp, actualUser, err := touchid.Login(origin, user, assertion) require.NoError(t, err, "Login failed") assert.Equal(t, test.wantUser, actualUser, "actualUser mismatch") + assert.Equal(t, 2, fake.userPrompts, "unexpected number of Login prompts") // Same as above: easiest way to validate the assertion is to marshal // and then parse the body. @@ -334,6 +344,158 @@ func TestLogin_findsCorrectCredential(t *testing.T) { } } +func TestLogin_noCredentials_failsWithoutUserInteraction(t *testing.T) { + n := *touchid.Native + t.Cleanup(func() { + *touchid.Native = n + }) + + fake := &fakeNative{} + *touchid.Native = fake + + const origin = "https://goteleport.com" + baseAssertion := &wanlib.CredentialAssertion{ + Response: protocol.PublicKeyCredentialRequestOptions{ + Challenge: []byte{1, 2, 3, 4, 5}, // arbitrary + RelyingPartyID: "goteleport.com", + UserVerification: protocol.VerificationRequired, + }, + } + mfaAssertion := *baseAssertion + mfaAssertion.Response.UserVerification = protocol.VerificationDiscouraged + mfaAssertion.Response.AllowedCredentials = []protocol.CredentialDescriptor{ + { + Type: protocol.PublicKeyCredentialType, + CredentialID: []byte{1, 2, 3, 4, 5}, // arbitrary + }, + } + + // Run empty credentials tests first. + for _, test := range []struct { + name string + user string + assertion *wanlib.CredentialAssertion + }{ + { + name: "passwordless empty user", + assertion: baseAssertion, + }, + { + name: "passwordless explicit user", + user: "llama", + assertion: baseAssertion, + }, + { + name: "MFA empty user", + user: "", // Typically MFA comes with an empty user + assertion: &mfaAssertion, + }, + { + name: "MFA explicit user", + user: "llama", + assertion: &mfaAssertion, + }, + } { + t.Run(test.name, func(t *testing.T) { + fake.userPrompts = 0 // reset before test + _, _, err := touchid.Login(origin, test.user, test.assertion) + assert.ErrorIs(t, err, touchid.ErrCredentialNotFound, "Login error mismatch") + assert.Zero(t, fake.userPrompts, "Login caused user interaction with no credentials") + }) + } + + // Register a couple of credentials for the following tests. + const userLlama = "llama" + const userAlpaca = "alpaca" + rrk := true + cc1 := &wanlib.CredentialCreation{ + Response: protocol.PublicKeyCredentialCreationOptions{ + Challenge: []byte{1, 2, 3, 4, 5}, // arbitrary, not important here + RelyingParty: protocol.RelyingPartyEntity{ + CredentialEntity: protocol.CredentialEntity{ + Name: "Teleport", + }, + ID: baseAssertion.Response.RelyingPartyID, + }, + User: protocol.UserEntity{ + CredentialEntity: protocol.CredentialEntity{ + Name: userLlama, + }, + DisplayName: "Llama", + ID: []byte{1, 1, 1, 1, 1}, + }, + Parameters: []protocol.CredentialParameter{ + { + Type: protocol.PublicKeyCredentialType, + Algorithm: webauthncose.AlgES256, + }, + }, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + RequireResidentKey: &rrk, + UserVerification: protocol.VerificationRequired, + }, + Attestation: protocol.PreferDirectAttestation, + }, + } + cc2 := *cc1 + cc2.Response.User = protocol.UserEntity{ + CredentialEntity: protocol.CredentialEntity{ + Name: userAlpaca, + }, + DisplayName: "Alpaca", + ID: []byte{1, 1, 1, 1, 2}, + } + for _, cc := range []*wanlib.CredentialCreation{cc1, &cc2} { + reg, err := touchid.Register(origin, cc) + require.NoError(t, err, "Register failed") + require.NoError(t, reg.Confirm(), "Confirm failed") + } + + mfaAllCreds := mfaAssertion + mfaAllCreds.Response.AllowedCredentials = nil + for _, c := range fake.creds { + mfaAllCreds.Response.AllowedCredentials = append(mfaAllCreds.Response.AllowedCredentials, protocol.CredentialDescriptor{ + Type: protocol.PublicKeyCredentialType, + CredentialID: []byte(c.id), + }) + } + + // Test absence of user prompts with existing credentials. + for _, test := range []struct { + name string + user string + assertion *wanlib.CredentialAssertion + }{ + { + name: "passwordless existing credentials", + user: "camel", // not registered + assertion: baseAssertion, + }, + { + name: "MFA unknown credential IDs (1)", + user: "", // any user + assertion: &mfaAssertion, // missing correct credential IDs + }, + { + name: "MFA unknown credential IDs (2)", + user: userLlama, // known user + assertion: &mfaAssertion, // missing correct credential IDs + }, + { + name: "MFA credentials for another user", + user: "camel", // unknown user + assertion: &mfaAllCreds, // credential IDs correct but for other users + }, + } { + t.Run(test.name, func(t *testing.T) { + fake.userPrompts = 0 // reset before test + _, _, err := touchid.Login(origin, test.user, test.assertion) + assert.ErrorIs(t, err, touchid.ErrCredentialNotFound, "Login error mismatch") + assert.Zero(t, fake.userPrompts, "Login caused user interaction with no credentials") + }) + } +} + type credentialHandle struct { rpID, user string id string @@ -350,6 +512,10 @@ type fakeNative struct { // lastAuthnCredential is the last credential ID used in a successful // Authenticate call. lastAuthnCredential string + + // userPrompts counts the number of user-visible prompts that would be caused + // by various methods. + userPrompts int } func (f *fakeNative) Diag() (*touchid.DiagResult, error) { @@ -363,7 +529,28 @@ func (f *fakeNative) Diag() (*touchid.DiagResult, error) { }, nil } -func (f *fakeNative) Authenticate(credentialID string, data []byte) ([]byte, error) { +type fakeAuthContext struct { + countPrompts func(ctx touchid.AuthContext) + prompted bool +} + +func (c *fakeAuthContext) Guard(fn func()) error { + c.countPrompts(c) + fn() + return nil +} + +func (c *fakeAuthContext) Close() { + c.prompted = false +} + +func (f *fakeNative) NewAuthContext() touchid.AuthContext { + return &fakeAuthContext{ + countPrompts: f.countPrompts, + } +} + +func (f *fakeNative) Authenticate(actx touchid.AuthContext, credentialID string, data []byte) ([]byte, error) { var key *ecdsa.PrivateKey for _, cred := range f.creds { if cred.id == credentialID { @@ -375,6 +562,7 @@ func (f *fakeNative) Authenticate(credentialID string, data []byte) ([]byte, err return nil, touchid.ErrCredentialNotFound } + f.countPrompts(actx) sig, err := key.Sign(rand.Reader, data, crypto.SHA256) if err != nil { return nil, err @@ -383,6 +571,18 @@ func (f *fakeNative) Authenticate(credentialID string, data []byte) ([]byte, err return sig, nil } +func (f *fakeNative) countPrompts(actx touchid.AuthContext) { + switch c, ok := actx.(*fakeAuthContext); { + case ok && c.prompted: + return // Already prompted + case ok: + c.prompted = true + fallthrough + default: + f.userPrompts++ + } +} + func (f *fakeNative) DeleteCredential(credentialID string) error { return errors.New("not implemented") } diff --git a/lib/auth/touchid/authenticate.h b/lib/auth/touchid/authenticate.h index 8c091c73c1e62..3cb1b970008f4 100644 --- a/lib/auth/touchid/authenticate.h +++ b/lib/auth/touchid/authenticate.h @@ -17,6 +17,7 @@ #include +#include "context.h" #include "credential_info.h" typedef struct AuthenticateRequest { @@ -29,6 +30,7 @@ typedef struct AuthenticateRequest { // it. The digest is expected to be in SHA256. // Authenticate requires user interaction. // Returns zero if successful, non-zero otherwise. -int Authenticate(AuthenticateRequest req, char **sigB64Out, char **errOut); +int Authenticate(AuthContext *actx, AuthenticateRequest req, char **sigB64Out, + char **errOut); #endif // AUTHENTICATE_H_ diff --git a/lib/auth/touchid/authenticate.m b/lib/auth/touchid/authenticate.m index d1285378b4861..59459b88ce3f6 100644 --- a/lib/auth/touchid/authenticate.m +++ b/lib/auth/touchid/authenticate.m @@ -22,8 +22,10 @@ #import #include "common.h" +#include "context.h" -int Authenticate(AuthenticateRequest req, char **sigB64Out, char **errOut) { +int Authenticate(AuthContext *actx, AuthenticateRequest req, char **sigB64Out, + char **errOut) { NSData *appLabel = [NSData dataWithBytes:req.app_label length:strlen(req.app_label)]; NSDictionary *query = @{ @@ -32,7 +34,10 @@ int Authenticate(AuthenticateRequest req, char **sigB64Out, char **errOut) { (id)kSecMatchLimit : (id)kSecMatchLimitOne, (id)kSecReturnRef : @YES, (id)kSecAttrApplicationLabel : appLabel, + // ctx takes effect in the SecKeyCreateSignature call below. + (id)kSecUseAuthenticationContext : (id)GetLAContextFromAuth(actx), }; + SecKeyRef privateKey = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey); diff --git a/lib/auth/touchid/context.h b/lib/auth/touchid/context.h new file mode 100644 index 0000000000000..2f1f03df5fc65 --- /dev/null +++ b/lib/auth/touchid/context.h @@ -0,0 +1,43 @@ +// Copyright 2022 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CONTEXT_H_ +#define CONTEXT_H_ + +#import + +#include + +// AuthContext is an optional, shared authentication context. +// Allows reusing a single authentication prompt/gesture between different +// functions, provided the functions are invoked in a short time interval. +typedef struct AuthContext { + LAContext *la_ctx; +} AuthContext; + +// GetLAContextFromAuth gets the LAContext from ctx, or returns a new LAContext +// instance. +LAContext *GetLAContextFromAuth(AuthContext *actx); + +// AuthContextGuard guards the invocation of a Go function handle behind an +// authentication prompt. +// The expected Go function signature is `func ()`. +// Returns zero if successful, non-zero otherwise. +int AuthContextGuard(AuthContext *actx, const char *reason, uintptr_t handle, + char **errOut); + +// AuthContextClose releases resources held by ctx. +void AuthContextClose(AuthContext *actx); + +#endif // CONTEXT_H_ diff --git a/lib/auth/touchid/context.m b/lib/auth/touchid/context.m new file mode 100644 index 0000000000000..0257168b8f11f --- /dev/null +++ b/lib/auth/touchid/context.m @@ -0,0 +1,85 @@ +//go:build touchid +// +build touchid + +// Copyright 2022 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "context.h" + +#import +#import +#import + +#include + +#include + +#include "common.h" + +// runGoFuncHandle is provided via CGO exports. +// (#include "_cgo_export.h" also works.) +extern void runGoFuncHandle(uintptr_t handle); + +LAContext *GetLAContextFromAuth(AuthContext *actx) { + if (actx == NULL) { + return [[LAContext alloc] init]; + } + if (actx->la_ctx == NULL) { + actx->la_ctx = [[LAContext alloc] init]; + actx->la_ctx.touchIDAuthenticationAllowableReuseDuration = 10; // seconds + } + return actx->la_ctx; +} + +int AuthContextGuard(AuthContext *actx, const char *reason, uintptr_t handle, + char **errOut) { + LAContext *ctx = GetLAContextFromAuth(actx); + + __block int res = 0; + __block NSString *nsError = NULL; + + // A semaphore is needed, otherwise we return before the prompt has a chance + // to resolve. + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + [ctx evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + localizedReason:[NSString stringWithUTF8String:reason] + reply:^void(BOOL success, NSError *_Nullable error) { + if (success) { + runGoFuncHandle(handle); + } else { + res = -1; + nsError = [error localizedDescription]; + } + dispatch_semaphore_signal(sema); + }]; + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + // sema released by ARC. + + if (nsError) { + *errOut = CopyNSString(nsError); + } else if (res != errSecSuccess) { + CFStringRef err = SecCopyErrorMessageString(res, NULL); + NSString *nsErr = (__bridge_transfer NSString *)err; + *errOut = CopyNSString(nsErr); + } + + return res; +} + +void AuthContextClose(AuthContext *actx) { + if (actx == NULL) { + return; + } + actx->la_ctx = NULL; // Let ARC collect the LAContext. +}