From 124e5c998cbbd846bc59e0aa13ba68d5bdd0e479 Mon Sep 17 00:00:00 2001 From: Chan Aethiopicus Date: Tue, 19 Nov 2024 11:27:47 +0900 Subject: [PATCH] first commit --- .github/workflows/ci.yaml | 32 ++++++++++++ .gitignore | 2 + .lefthook.yaml | 5 ++ LICENSE | 21 ++++++++ README.md | 54 ++++++++++++++++++++ go.mod | 3 ++ touchid.go | 103 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 220 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .lefthook.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 go.mod create mode 100644 touchid.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..60eb258 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,32 @@ +name: CI +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + staticcheck: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - uses: dominikh/staticcheck-action@v1 + with: + version: "latest" + install-go: false + unittest: + name: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - run: go test ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94f1119 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.vscode diff --git a/.lefthook.yaml b/.lefthook.yaml new file mode 100644 index 0000000..1500236 --- /dev/null +++ b/.lefthook.yaml @@ -0,0 +1,5 @@ +pre-commit: + parallel: true + commands: + go-lint: + run: staticcheck ./... diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c862631 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 aethiopicuschan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c50fbe9 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# touchid go + +[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen?style=flat-square)](/LICENSE) +[![Go Reference](https://pkg.go.dev/badge/github.com/aethiopicuschan/touchid-go.svg)](https://pkg.go.dev/github.com/aethiopicuschan/touchid-go) +[![Go Report Card](https://goreportcard.com/badge/github.com/aethiopicuschan/touchid-go)](https://goreportcard.com/report/github.com/aethiopicuschan/touchid-go) +[![CI](https://github.com/aethiopicuschan/touchid-go/actions/workflows/ci.yaml/badge.svg)](https://github.com/aethiopicuschan/touchid-go/actions/workflows/ci.yaml) + +`touchid-go` is a Go library for Touch ID authentication on macOS. It provides a simple API and supports `context.Context` for managing timeouts and cancellations. + +## Installation + +```sh +go get -u github.com/aethiopicuschan/touchid-go +``` + +## Example + +```go +package main + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/aethiopicuschan/touchid-go" +) + +func main() { + // Wait for 5 seconds for the user to authenticate + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Authenticate with Touch ID + ok, err := touchid.Authenticate(ctx, "Sample Text") + if err != nil { + if ctx.Err() == context.DeadlineExceeded { + fmt.Println("Authentication timed out") + } else { + fmt.Println("Error:", err.Error()) + } + os.Exit(1) + } + + // Check the result + if ok { + fmt.Println("Authentication succeeded") + } else { + fmt.Println("Authentication failed") + os.Exit(1) + } +} +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8662253 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/aethiopicuschan/touchid-go + +go 1.23.1 diff --git a/touchid.go b/touchid.go new file mode 100644 index 0000000..3e6b80b --- /dev/null +++ b/touchid.go @@ -0,0 +1,103 @@ +//go:build darwin + +package touchid + +/* +#cgo CFLAGS: -x objective-c -fmodules -fblocks +#cgo LDFLAGS: -framework CoreFoundation -framework LocalAuthentication -framework Foundation +#include +#include +#import + +typedef struct { + LAContext *context; + dispatch_semaphore_t sema; + __block int result; +} AuthContext; + +AuthContext* InitAuthContext() { + AuthContext *authCtx = (AuthContext *)malloc(sizeof(AuthContext)); + authCtx->context = [[LAContext alloc] init]; + authCtx->sema = dispatch_semaphore_create(0); + authCtx->result = 0; + return authCtx; +} + +void ReleaseAuthContext(AuthContext *authCtx) { + if (authCtx) { + [authCtx->context release]; + dispatch_release(authCtx->sema); + free(authCtx); + } +} + +void CancelAuthentication(AuthContext *authCtx) { + if (authCtx && authCtx->context) { + [authCtx->context invalidate]; + } +} + +int AuthenticateWithContext(AuthContext *authCtx, char const* reason) { + NSError *authError = nil; + NSString *nsReason = [NSString stringWithUTF8String:reason]; + + // Use LAPolicyDeviceOwnerAuthentication to allow both biometrics and password + if ([authCtx->context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { + [authCtx->context evaluatePolicy:LAPolicyDeviceOwnerAuthentication + localizedReason:nsReason + reply:^(BOOL success, NSError *error) { + if (success) { + authCtx->result = 1; + } else { + // Handle failure due to user canceling or other errors + if (error.code == LAErrorUserFallback) { + authCtx->result = 2; // Password used + } else { + authCtx->result = 3; // Other error + } + } + dispatch_semaphore_signal(authCtx->sema); + }]; + } + + dispatch_semaphore_wait(authCtx->sema, DISPATCH_TIME_FOREVER); + return authCtx->result; +} +*/ +import ( + "C" +) +import ( + "context" + "errors" + "unsafe" +) + +func Authenticate(ctx context.Context, reason string) (bool, error) { + reasonStr := C.CString(reason) + defer C.free(unsafe.Pointer(reasonStr)) + + authCtx := C.InitAuthContext() + defer C.ReleaseAuthContext(authCtx) + + resultChan := make(chan int, 1) + go func() { + result := C.AuthenticateWithContext(authCtx, reasonStr) + resultChan <- int(result) + }() + + select { + case <-ctx.Done(): + C.CancelAuthentication(authCtx) + return false, ctx.Err() + case result := <-resultChan: + switch result { + case 1: + return true, nil + case 2: + return false, nil + } + } + + return false, errors.New("unexpected error") +}