diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc2e158..b140fc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: test: strategy: matrix: - go-version: [1.21.x, 1.22.x, 1.23.x] + go-version: [1.23.x, 1.24.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: @@ -19,8 +19,8 @@ jobs: go-version: ${{ matrix.go-version }} - uses: actions/checkout@v3 - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v8 with: - version: v1.63 + version: latest - run: go vet ./... - run: go test -tags skipsecretserviceintegrationtests ./... diff --git a/.golangci.yml b/.golangci.yml index 4eb78ca..8596999 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,104 @@ -linters-settings: - gocritic: - disabled-checks: - - ifElseChain - - elseif - -linters: +formatters: enable: - gofmt + exclusions: + paths: + - .*_mock\.go + - mock_.*\.go + - .*/pkg/mod/.*$ + - .*/go/src/.*\.go + - third_party$ + - builtin$ + - examples$ + settings: + gofmt: + simplify: true + goimports: + local-prefixes: + - go.opentelemetry.io +issues: + max-issues-per-linter: 50 +linters: + default: none + enable: + - asciicheck + - bodyclose + - dogsled + - durationcheck + - errcheck + - errorlint + - exhaustive + - forbidigo + - forcetypeassert + - goconst - gocritic - - unconvert - - revive + - gocyclo + - godot + - gosec - govet + - ineffassign + - misspell + - nestif + - nilerr + - nlreturn + - noctx + - prealloc + - predeclared + - revive + - sqlclosecheck + - staticcheck + - unconvert + - unused + - whitespace + - wrapcheck + - wsl_v5 + exclusions: + paths: + - .*_mock\.go + - mock_.*\.go + - .*/pkg/mod/.*$ + - .*/go/src/.*\.go + - third_party$ + - builtin$ + - examples$ + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + settings: + depguard: + rules: + main: + allow: + - $all + dupl: + threshold: 99 + errcheck: + check-blank: false + check-type-assertions: false + goconst: + min-len: 3 + min-occurrences: 2 + gocyclo: + min-complexity: 18 + govet: + disable: + - shadow + misspell: + ignore-rules: + - cancelled + locale: US + revive: + severity: warning +output: + formats: + text: + path: stdout + print-issued-lines: true + print-linter-name: true +run: + concurrency: 4 + issues-exit-code: 1 + tests: false +version: "2" diff --git a/LICENSE b/LICENSE index 2d54c65..50b9c88 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2015 Keybase +Copyright (c) 2025 Mailstone Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +20,3 @@ 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 index 5412b1f..7d47d9a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Go Keychain -[![Build Status](https://github.com/keybase/go-keychain/actions/workflows/ci.yml/badge.svg)](https://github.com/keybase/go-keychain/actions) +[![Build Status](https://github.com/mailstone/go-keychain/actions/workflows/ci.yml/badge.svg)](https://github.com/mailstone/go-keychain/actions) A library for accessing the Keychain for macOS, iOS, and Linux in Go (golang). @@ -8,7 +8,7 @@ Requires macOS 10.9 or greater and iOS 8 or greater. On Linux, communicates to a provider of the DBUS SecretService spec like gnome-keyring or ksecretservice. ```go -import "github.com/keybase/go-keychain" +import "github.com/mailstone/go-keychain" ``` ## Mac/iOS Usage diff --git a/bind/bind.go b/bind/bind.go index c677d69..847d3bb 100644 --- a/bind/bind.go +++ b/bind/bind.go @@ -4,36 +4,40 @@ package bind import ( + "errors" "fmt" "reflect" - "github.com/keybase/go-keychain" + "github.com/mailstone/go-keychain" ) -// Test is a bind interface for the test +// Test is a bind interface for the test. type Test interface { Fail(s string) } -// AddGenericPassword adds generic password +// AddGenericPassword adds generic password. func AddGenericPassword(service string, account string, label string, password string, accessGroup string) error { item := keychain.NewGenericPassword(service, account, label, []byte(password), accessGroup) - return keychain.AddItem(item) + + return keychain.AddItem(item) // nolint: wrapcheck } -// DeleteGenericPassword deletes generic password +// DeleteGenericPassword deletes generic password. func DeleteGenericPassword(service string, account string, accessGroup string) error { item := keychain.NewItem() item.SetSecClass(keychain.SecClassGenericPassword) item.SetService(service) item.SetAccount(account) item.SetAccessGroup(accessGroup) - return keychain.DeleteItem(item) + + return keychain.DeleteItem(item) // nolint: wrapcheck } // GenericPasswordTest runs test code for generic password keychain item. // This is here so we can export using gomobile bind and run this method on iOS simulator and device. // Access groups aren't supported in iOS simulator. +// nolint: gocyclo func GenericPasswordTest(t Test, service string, accessGroup string) { var err error @@ -54,6 +58,7 @@ func GenericPasswordTest(t Test, service string, accessGroup string) { if err != nil { t.Fail(err.Error()) } + if len(accounts) != 0 { t.Fail("Should have no accounts") } @@ -66,7 +71,7 @@ func GenericPasswordTest(t Test, service string, accessGroup string) { // Test dupe err = keychain.AddItem(item) - if err != keychain.ErrorDuplicateItem { + if !errors.Is(err, keychain.ErrorDuplicateItem) { t.Fail("Should error with duplicate item") } @@ -84,6 +89,7 @@ func GenericPasswordTest(t Test, service string, accessGroup string) { query.SetAccessGroup(accessGroup) query.SetMatchLimit(keychain.MatchLimitAll) query.SetReturnAttributes(true) + results, err := keychain.QueryItem(query) if err != nil { t.Fail(err.Error()) @@ -113,6 +119,7 @@ func GenericPasswordTest(t Test, service string, accessGroup string) { queryData.SetAccessGroup(accessGroup) queryData.SetMatchLimit(keychain.MatchLimitOne) queryData.SetReturnData(true) + resultsData, err := keychain.QueryItem(queryData) if err != nil { t.Fail(err.Error()) @@ -131,6 +138,7 @@ func GenericPasswordTest(t Test, service string, accessGroup string) { if err != nil { t.Fail(err.Error()) } + if len(accounts2) != 2 { t.Fail(fmt.Sprintf("Should have 2 accounts: %v", accounts2)) } @@ -145,8 +153,8 @@ func GenericPasswordTest(t Test, service string, accessGroup string) { queryDel.SetService(service) queryDel.SetAccount(account) queryDel.SetAccessGroup(accessGroup) - err = keychain.DeleteItem(queryDel) - if err != nil { + + if err := keychain.DeleteItem(queryDel); err != nil { t.Fail(err.Error()) } @@ -158,6 +166,7 @@ func GenericPasswordTest(t Test, service string, accessGroup string) { query3.SetAccessGroup(accessGroup) query3.SetMatchLimit(keychain.MatchLimitAll) query3.SetReturnAttributes(true) + results3, err := keychain.QueryItem(query3) if err != nil { t.Fail(err.Error()) @@ -171,13 +180,14 @@ func GenericPasswordTest(t Test, service string, accessGroup string) { if err != nil { t.Fail(err.Error()) } + if len(accounts3) != 1 { t.Fail("Should have an account") } // Test remove not found err = keychain.DeleteItem(item) - if err != keychain.ErrorItemNotFound { + if !errors.Is(err, keychain.ErrorItemNotFound) { t.Fail("Error should be not found") } } diff --git a/bindtest/bind_test.go b/bindtest/bind_test.go index 8acb92c..ea4b91c 100644 --- a/bindtest/bind_test.go +++ b/bindtest/bind_test.go @@ -6,7 +6,7 @@ package bindtest import ( "testing" - "github.com/keybase/go-keychain/bind" + "github.com/mailstone/go-keychain/bind" "github.com/stretchr/testify/require" ) diff --git a/corefoundation.go b/corefoundation.go index b7ee544..9062b4a 100644 --- a/corefoundation.go +++ b/corefoundation.go @@ -1,6 +1,7 @@ //go:build darwin || ios // +build darwin ios +// nolint: nlreturn package keychain /* @@ -46,58 +47,66 @@ func Release(ref C.CFTypeRef) { // Release(ref). func BytesToCFData(b []byte) (C.CFDataRef, error) { if uint64(len(b)) > math.MaxUint32 { - return 0, errors.New("Data is too large") + return 0, errors.New("data is too large") } + var p *C.UInt8 if len(b) > 0 { p = (*C.UInt8)(&b[0]) } - cfData := C.CFDataCreate(C.kCFAllocatorDefault, p, C.CFIndex(len(b))) + + cfData := C.CFDataCreate(C.kCFAllocatorDefault, p, C.CFIndex(len(b))) // nolint: nlreturn if cfData == 0 { return 0, fmt.Errorf("CFDataCreate failed") } + return cfData, nil } // CFDataToBytes converts CFData to bytes. func CFDataToBytes(cfData C.CFDataRef) ([]byte, error) { - return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))), nil + return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))), nil // nolint: nlreturn } // MapToCFDictionary will return a CFDictionaryRef and if non-nil, must be // released with Release(ref). func MapToCFDictionary(m map[C.CFTypeRef]C.CFTypeRef) (C.CFDictionaryRef, error) { - var keys, values []C.uintptr_t + var keys, values []C.uintptr_t // nolint: prealloc + for key, value := range m { keys = append(keys, C.uintptr_t(key)) values = append(values, C.uintptr_t(value)) } + numValues := len(values) var keysPointer, valuesPointer *C.uintptr_t if numValues > 0 { keysPointer = &keys[0] valuesPointer = &values[0] } - cfDict := C.CFDictionaryCreateSafe2(C.kCFAllocatorDefault, keysPointer, valuesPointer, C.CFIndex(numValues), - &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) //nolint + + cfDict := C.CFDictionaryCreateSafe2(C.kCFAllocatorDefault, keysPointer, valuesPointer, C.CFIndex(numValues), &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) //nolint if cfDict == 0 { return 0, fmt.Errorf("CFDictionaryCreate failed") } + return cfDict, nil } // CFDictionaryToMap converts CFDictionaryRef to a map. func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) { - count := C.CFDictionaryGetCount(cfDict) + count := C.CFDictionaryGetCount(cfDict) // nolint: nlreturn if count > 0 { keys := make([]C.CFTypeRef, count) values := make([]C.CFTypeRef, count) C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(unsafe.Pointer(&keys[0])), (*unsafe.Pointer)(unsafe.Pointer(&values[0]))) m = make(map[C.CFTypeRef]C.CFTypeRef, count) + for i := C.CFIndex(0); i < count; i++ { m[keys[i]] = values[i] } } + return } @@ -105,59 +114,78 @@ func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) func Int32ToCFNumber(u int32) C.CFNumberRef { sint := C.SInt32(u) p := unsafe.Pointer(&sint) - return C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberSInt32Type, p) + + return C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberSInt32Type, p) // nolint: nlreturn } // StringToCFString will return a CFStringRef and if non-nil, must be released with // Release(ref). func StringToCFString(s string) (C.CFStringRef, error) { if !utf8.ValidString(s) { - return 0, errors.New("Invalid UTF-8 string") + return 0, errors.New("invalid UTF-8 string") } + if uint64(len(s)) > math.MaxUint32 { - return 0, errors.New("String is too large") + return 0, errors.New("string is too large") } bytes := []byte(s) var p *C.UInt8 + if len(bytes) > 0 { p = (*C.UInt8)(&bytes[0]) } - return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, p, C.CFIndex(len(s)), C.kCFStringEncodingUTF8, C.false), nil + + return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, p, C.CFIndex(len(s)), C.kCFStringEncodingUTF8, C.false), nil // nolint: nlreturn } // CFStringToString converts a CFStringRef to a string. func CFStringToString(s C.CFStringRef) string { - p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8) + p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8) // nolint: nlreturn if p != nil { return C.GoString(p) } + length := C.CFStringGetLength(s) if length == 0 { return "" } + maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8) if maxBufLen == 0 { return "" } + buf := make([]byte, maxBufLen) + var usedBufLen C.CFIndex + _ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen) + return string(buf[:usedBufLen]) } // ArrayToCFArray will return a CFArrayRef and if non-nil, must be released with // Release(ref). func ArrayToCFArray(a []C.CFTypeRef) C.CFArrayRef { - var values []C.uintptr_t - for _, value := range a { - values = append(values, C.uintptr_t(value)) + values := make([]C.uintptr_t, 0, len(a)) + + for i := range a { + if a[i] == 0 { + return 0 // Return nil if any element is nil + } + + values[i] = C.uintptr_t(a[i]) } + numValues := len(values) + var valuesPointer *C.uintptr_t + if numValues > 0 { valuesPointer = &values[0] } + return C.CFArrayCreateSafe2(C.kCFAllocatorDefault, valuesPointer, C.CFIndex(numValues), &C.kCFTypeArrayCallBacks) //nolint } @@ -168,6 +196,7 @@ func CFArrayToArray(cfArray C.CFArrayRef) (a []C.CFTypeRef) { a = make([]C.CFTypeRef, count) C.CFArrayGetValues(cfArray, C.CFRange{0, count}, (*unsafe.Pointer)(unsafe.Pointer(&a[0]))) } + return } @@ -180,11 +209,13 @@ type Convertable interface { // must be released with Release(ref). func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, error) { m := make(map[C.CFTypeRef]C.CFTypeRef) + for key, i := range attr { var valueRef C.CFTypeRef + switch val := i.(type) { default: - return 0, fmt.Errorf("Unsupported value type: %v", reflect.TypeOf(i)) + return 0, fmt.Errorf("unsupported value type: %v", reflect.TypeOf(i)) case C.CFTypeRef: valueRef = val case bool: @@ -195,34 +226,44 @@ func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, e } case int32: valueRef = C.CFTypeRef(Int32ToCFNumber(val)) + defer Release(valueRef) case []byte: bytesRef, err := BytesToCFData(val) if err != nil { - return 0, err + return 0, fmt.Errorf("failed to convert bytes to CFData: %w", err) } + valueRef = C.CFTypeRef(bytesRef) + defer Release(valueRef) case string: stringRef, err := StringToCFString(val) if err != nil { - return 0, err + return 0, fmt.Errorf("failed to convert string to CFString: %w", err) } + valueRef = C.CFTypeRef(stringRef) + defer Release(valueRef) case Convertable: convertedRef, err := val.Convert() if err != nil { - return 0, err + return 0, fmt.Errorf("failed to convert value: %w", err) } + valueRef = convertedRef + defer Release(valueRef) } + keyRef, err := StringToCFString(key) if err != nil { return 0, err } + m[C.CFTypeRef(keyRef)] = valueRef + defer Release(C.CFTypeRef(keyRef)) } @@ -244,37 +285,44 @@ func CFTypeDescription(ref C.CFTypeRef) string { // Convert converts a CFTypeRef to a go instance. func Convert(ref C.CFTypeRef) (interface{}, error) { typeID := C.CFGetTypeID(ref) - if typeID == C.CFStringGetTypeID() { + + switch typeID { + case C.CFStringGetTypeID(): return CFStringToString(C.CFStringRef(ref)), nil - } else if typeID == C.CFDictionaryGetTypeID() { + case C.CFDictionaryGetTypeID(): return ConvertCFDictionary(C.CFDictionaryRef(ref)) - } else if typeID == C.CFArrayGetTypeID() { + case C.CFArrayGetTypeID(): arr := CFArrayToArray(C.CFArrayRef(ref)) results := make([]interface{}, 0, len(arr)) + for _, ref := range arr { v, err := Convert(ref) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert CFArray element: %w", err) } + results = append(results, v) } + return results, nil - } else if typeID == C.CFDataGetTypeID() { + case C.CFDataGetTypeID(): b, err := CFDataToBytes(C.CFDataRef(ref)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert CFData: %w", err) } + return b, nil - } else if typeID == C.CFNumberGetTypeID() { + case C.CFNumberGetTypeID(): return CFNumberToInterface(C.CFNumberRef(ref)), nil - } else if typeID == C.CFBooleanGetTypeID() { + case C.CFBooleanGetTypeID(): if C.CFBooleanGetValue(C.CFBooleanRef(ref)) != 0 { return true, nil } + return false, nil + default: + return nil, fmt.Errorf("invalid type: %s", CFTypeDescription(ref)) } - - return nil, fmt.Errorf("Invalid type: %s", CFTypeDescription(ref)) } // ConvertCFDictionary converts a CFDictionary to map (deep). diff --git a/datetime.go b/datetime.go index 15be2a3..76efe0f 100644 --- a/datetime.go +++ b/datetime.go @@ -30,11 +30,13 @@ func unixToAbsoluteTime(s int64, ns int64) C.CFAbsoluteTime { // isn't much earlier than the Core Foundation absolute // reference date). abs := s - absoluteTimeIntervalSince1970() + return C.CFAbsoluteTime(abs) + C.CFTimeInterval(ns)/nsPerSec } func absoluteTimeToUnix(abs C.CFAbsoluteTime) (int64, int64) { i, frac := math.Modf(float64(abs)) + return int64(i) + absoluteTimeIntervalSince1970(), int64(frac * nsPerSec) } @@ -44,24 +46,27 @@ func TimeToCFDate(t time.Time) C.CFDateRef { s := t.Unix() ns := int64(t.Nanosecond()) abs := unixToAbsoluteTime(s, ns) - return C.CFDateCreate(C.kCFAllocatorDefault, abs) + + return C.CFDateCreate(C.kCFAllocatorDefault, abs) // nolint: nlreturn } // CFDateToTime will convert the given CFDateRef to a time.Time. func CFDateToTime(d C.CFDateRef) time.Time { - abs := C.CFDateGetAbsoluteTime(d) + abs := C.CFDateGetAbsoluteTime(d) // nolint: nlreturn + s, ns := absoluteTimeToUnix(abs) + return time.Unix(s, ns) } // Wrappers around C functions for testing. func cfDateToAbsoluteTime(d C.CFDateRef) C.CFAbsoluteTime { - return C.CFDateGetAbsoluteTime(d) + return C.CFDateGetAbsoluteTime(d) // nolint: nlreturn } func absoluteTimeToCFDate(abs C.CFAbsoluteTime) C.CFDateRef { - return C.CFDateCreate(C.kCFAllocatorDefault, abs) + return C.CFDateCreate(C.kCFAllocatorDefault, abs) // nolint: nlreturn } func releaseCFDate(d C.CFDateRef) { diff --git a/go.mod b/go.mod index cbaa2fe..2c884d0 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,13 @@ -module github.com/keybase/go-keychain +module github.com/mailstone/go-keychain -go 1.21 +go 1.24.0 + +toolchain go1.24.5 require ( - github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a + github.com/godbus/dbus/v5 v5.1.0 github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.32.0 + golang.org/x/crypto v0.40.0 ) require ( diff --git a/go.sum b/go.sum index 53611f3..9e48a2f 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a h1:K0EAzgzEQHW4Y5lxrmvPMltmlRDzlhLfGmots9EHUTI= -github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/ios/bind.framework/Versions/A/Headers/Bind.h b/ios/bind.framework/Versions/A/Headers/Bind.h index 06ee05a..cc9e7d8 100644 --- a/ios/bind.framework/Versions/A/Headers/Bind.h +++ b/ios/bind.framework/Versions/A/Headers/Bind.h @@ -1,7 +1,7 @@ // Objective-C API for talking to the following Go packages // -// github.com/keybase/go-keychain/bind +// github.com/mailstone/go-keychain/bind // // File is generated by gomobile bind. Do not edit. #ifndef __Bind_FRAMEWORK_H__ diff --git a/ios/bind.framework/Versions/A/Headers/Bind.objc.h b/ios/bind.framework/Versions/A/Headers/Bind.objc.h index b0d8d0e..c8e6480 100644 --- a/ios/bind.framework/Versions/A/Headers/Bind.objc.h +++ b/ios/bind.framework/Versions/A/Headers/Bind.objc.h @@ -1,5 +1,6 @@ -// Objective-C API for talking to github.com/keybase/go-keychain/bind Go package. -// gobind -lang=objc github.com/keybase/go-keychain/bind +// Objective-C API for talking to github.com/mailstone/go-keychain/bind Go +// package. +// gobind -lang=objc github.com/mailstone/go-keychain/bind // // File is generated by gobind. Do not edit. @@ -9,19 +10,25 @@ @import Foundation; #include "Universe.objc.h" - @protocol BindTest; @class BindTest; @protocol BindTest -- (void)fail:(NSString*)s; +- (void)fail:(NSString *)s; @end -FOUNDATION_EXPORT BOOL BindAddGenericPassword(NSString* service, NSString* account, NSString* label, NSString* password, NSString* accessGroup, NSError** error); +FOUNDATION_EXPORT BOOL BindAddGenericPassword( + NSString *service, NSString *account, NSString *label, NSString *password, + NSString *accessGroup, NSError **error); -FOUNDATION_EXPORT BOOL BindDeleteGenericPassword(NSString* service, NSString* account, NSString* accessGroup, NSError** error); +FOUNDATION_EXPORT BOOL BindDeleteGenericPassword(NSString *service, + NSString *account, + NSString *accessGroup, + NSError **error); -FOUNDATION_EXPORT void BindGenericPasswordTest(id t, NSString* service, NSString* accessGroup); +FOUNDATION_EXPORT void BindGenericPasswordTest(id t, + NSString *service, + NSString *accessGroup); @class BindTest; @@ -30,7 +37,7 @@ FOUNDATION_EXPORT void BindGenericPasswordTest(id t, NSString* service @property(strong, readonly) id _ref; - (instancetype)initWithRef:(id)ref; -- (void)fail:(NSString*)s; +- (void)fail:(NSString *)s; @end #endif diff --git a/keychain.go b/keychain.go index 7d0a1ac..5c797af 100644 --- a/keychain.go +++ b/keychain.go @@ -19,53 +19,53 @@ import ( "time" ) -// Error defines keychain errors +// Error defines keychain errors. type Error int var ( - // ErrorUnimplemented corresponds to errSecUnimplemented result code + // ErrorUnimplemented corresponds to errSecUnimplemented result code. ErrorUnimplemented = Error(C.errSecUnimplemented) - // ErrorParam corresponds to errSecParam result code + // ErrorParam corresponds to errSecParam result code. ErrorParam = Error(C.errSecParam) - // ErrorAllocate corresponds to errSecAllocate result code + // ErrorAllocate corresponds to errSecAllocate result code. ErrorAllocate = Error(C.errSecAllocate) - // ErrorNotAvailable corresponds to errSecNotAvailable result code + // ErrorNotAvailable corresponds to errSecNotAvailable result code. ErrorNotAvailable = Error(C.errSecNotAvailable) - // ErrorAuthFailed corresponds to errSecAuthFailed result code + // ErrorAuthFailed corresponds to errSecAuthFailed result code. ErrorAuthFailed = Error(C.errSecAuthFailed) - // ErrorDuplicateItem corresponds to errSecDuplicateItem result code + // ErrorDuplicateItem corresponds to errSecDuplicateItem result code. ErrorDuplicateItem = Error(C.errSecDuplicateItem) - // ErrorItemNotFound corresponds to errSecItemNotFound result code + // ErrorItemNotFound corresponds to errSecItemNotFound result code. ErrorItemNotFound = Error(C.errSecItemNotFound) - // ErrorInteractionNotAllowed corresponds to errSecInteractionNotAllowed result code + // ErrorInteractionNotAllowed corresponds to errSecInteractionNotAllowed result code. ErrorInteractionNotAllowed = Error(C.errSecInteractionNotAllowed) - // ErrorDecode corresponds to errSecDecode result code + // ErrorDecode corresponds to errSecDecode result code. ErrorDecode = Error(C.errSecDecode) - // ErrorNoSuchKeychain corresponds to errSecNoSuchKeychain result code + // ErrorNoSuchKeychain corresponds to errSecNoSuchKeychain result code. ErrorNoSuchKeychain = Error(C.errSecNoSuchKeychain) - // ErrorNoAccessForItem corresponds to errSecNoAccessForItem result code + // ErrorNoAccessForItem corresponds to errSecNoAccessForItem result code. ErrorNoAccessForItem = Error(C.errSecNoAccessForItem) - // ErrorReadOnly corresponds to errSecReadOnly result code + // ErrorReadOnly corresponds to errSecReadOnly result code. ErrorReadOnly = Error(C.errSecReadOnly) - // ErrorInvalidKeychain corresponds to errSecInvalidKeychain result code + // ErrorInvalidKeychain corresponds to errSecInvalidKeychain result code. ErrorInvalidKeychain = Error(C.errSecInvalidKeychain) - // ErrorDuplicateKeyChain corresponds to errSecDuplicateKeychain result code + // ErrorDuplicateKeyChain corresponds to errSecDuplicateKeychain result code. ErrorDuplicateKeyChain = Error(C.errSecDuplicateKeychain) - // ErrorWrongVersion corresponds to errSecWrongSecVersion result code + // ErrorWrongVersion corresponds to errSecWrongSecVersion result code. ErrorWrongVersion = Error(C.errSecWrongSecVersion) - // ErrorReadonlyAttribute corresponds to errSecReadOnlyAttr result code + // ErrorReadonlyAttribute corresponds to errSecReadOnlyAttr result code. ErrorReadonlyAttribute = Error(C.errSecReadOnlyAttr) - // ErrorInvalidSearchRef corresponds to errSecInvalidSearchRef result code + // ErrorInvalidSearchRef corresponds to errSecInvalidSearchRef result code. ErrorInvalidSearchRef = Error(C.errSecInvalidSearchRef) - // ErrorInvalidItemRef corresponds to errSecInvalidItemRef result code + // ErrorInvalidItemRef corresponds to errSecInvalidItemRef result code. ErrorInvalidItemRef = Error(C.errSecInvalidItemRef) - // ErrorDataNotAvailable corresponds to errSecDataNotAvailable result code + // ErrorDataNotAvailable corresponds to errSecDataNotAvailable result code. ErrorDataNotAvailable = Error(C.errSecDataNotAvailable) - // ErrorDataNotModifiable corresponds to errSecDataNotModifiable result code + // ErrorDataNotModifiable corresponds to errSecDataNotModifiable result code. ErrorDataNotModifiable = Error(C.errSecDataNotModifiable) - // ErrorInvalidOwnerEdit corresponds to errSecInvalidOwnerEdit result code + // ErrorInvalidOwnerEdit corresponds to errSecInvalidOwnerEdit result code. ErrorInvalidOwnerEdit = Error(C.errSecInvalidOwnerEdit) - // ErrorUserCanceled corresponds to errSecUserCanceled result code + // ErrorUserCanceled corresponds to errSecUserCanceled result code. ErrorUserCanceled = Error(C.errSecUserCanceled) ) @@ -73,9 +73,11 @@ func checkError(errCode C.OSStatus) error { if errCode == C.errSecSuccess { return nil } + return Error(errCode) } +// nolint: gocyclo func (k Error) Error() (msg string) { // SecCopyErrorMessageString is only available on OSX, so derive manually. // Messages derived from `$ security error $errcode`. @@ -127,13 +129,14 @@ func (k Error) Error() (msg string) { default: msg = "Keychain Error." } + return fmt.Sprintf("%s (%d)", msg, k) } -// SecClass is the items class code +// SecClass is the items class code. type SecClass int -// Keychain Item Classes +// Keychain Item Classes. var ( /* kSecClassGenericPassword item attributes: @@ -145,63 +148,67 @@ var ( */ SecClassGenericPassword SecClass = 1 SecClassInternetPassword SecClass = 2 + SecClassCertificate SecClass = 3 + SecClassPairKey SecClass = 4 ) -// SecClassKey is the key type for SecClass +// SecClassKey is the key type for SecClass. var SecClassKey = attrKey(C.CFTypeRef(C.kSecClass)) var secClassTypeRef = map[SecClass]C.CFTypeRef{ SecClassGenericPassword: C.CFTypeRef(C.kSecClassGenericPassword), SecClassInternetPassword: C.CFTypeRef(C.kSecClassInternetPassword), + SecClassCertificate: C.CFTypeRef(C.kSecClassCertificate), + SecClassPairKey: C.CFTypeRef(C.kSecClassKey), } var ( - // ServiceKey is for kSecAttrService + // ServiceKey is for kSecAttrService. ServiceKey = attrKey(C.CFTypeRef(C.kSecAttrService)) - // ServerKey is for kSecAttrServer + // ServerKey is for kSecAttrServer. ServerKey = attrKey(C.CFTypeRef(C.kSecAttrServer)) - // ProtocolKey is for kSecAttrProtocol + // ProtocolKey is for kSecAttrProtocol. ProtocolKey = attrKey(C.CFTypeRef(C.kSecAttrProtocol)) - // AuthenticationTypeKey is for kSecAttrAuthenticationType + // AuthenticationTypeKey is for kSecAttrAuthenticationType. AuthenticationTypeKey = attrKey(C.CFTypeRef(C.kSecAttrAuthenticationType)) - // PortKey is for kSecAttrPort + // PortKey is for kSecAttrPort. PortKey = attrKey(C.CFTypeRef(C.kSecAttrPort)) - // PathKey is for kSecAttrPath + // PathKey is for kSecAttrPath. PathKey = attrKey(C.CFTypeRef(C.kSecAttrPath)) - // LabelKey is for kSecAttrLabel + // LabelKey is for kSecAttrLabel. LabelKey = attrKey(C.CFTypeRef(C.kSecAttrLabel)) - // AccountKey is for kSecAttrAccount + // AccountKey is for kSecAttrAccount. AccountKey = attrKey(C.CFTypeRef(C.kSecAttrAccount)) - // AccessGroupKey is for kSecAttrAccessGroup + // AccessGroupKey is for kSecAttrAccessGroup. AccessGroupKey = attrKey(C.CFTypeRef(C.kSecAttrAccessGroup)) - // DataKey is for kSecValueData + // DataKey is for kSecValueData. DataKey = attrKey(C.CFTypeRef(C.kSecValueData)) - // DescriptionKey is for kSecAttrDescription + // DescriptionKey is for kSecAttrDescription. DescriptionKey = attrKey(C.CFTypeRef(C.kSecAttrDescription)) - // CommentKey is for kSecAttrComment + // CommentKey is for kSecAttrComment. CommentKey = attrKey(C.CFTypeRef(C.kSecAttrComment)) - // CreationDateKey is for kSecAttrCreationDate + // CreationDateKey is for kSecAttrCreationDate. CreationDateKey = attrKey(C.CFTypeRef(C.kSecAttrCreationDate)) - // ModificationDateKey is for kSecAttrModificationDate + // ModificationDateKey is for kSecAttrModificationDate. ModificationDateKey = attrKey(C.CFTypeRef(C.kSecAttrModificationDate)) ) -// Synchronizable is the items synchronizable status +// Synchronizable is the items synchronizable status. type Synchronizable int const ( - // SynchronizableDefault is the default setting + // SynchronizableDefault is the default setting. SynchronizableDefault Synchronizable = 0 - // SynchronizableAny is for kSecAttrSynchronizableAny + // SynchronizableAny is for kSecAttrSynchronizableAny. SynchronizableAny = 1 - // SynchronizableYes enables synchronization + // SynchronizableYes enables synchronization. SynchronizableYes = 2 - // SynchronizableNo disables synchronization + // SynchronizableNo disables synchronization. SynchronizableNo = 3 ) -// SynchronizableKey is the key type for Synchronizable +// SynchronizableKey is the key type for Synchronizable. var SynchronizableKey = attrKey(C.CFTypeRef(C.kSecAttrSynchronizable)) var syncTypeRef = map[Synchronizable]C.CFTypeRef{ SynchronizableAny: C.CFTypeRef(C.kSecAttrSynchronizableAny), @@ -209,54 +216,54 @@ var syncTypeRef = map[Synchronizable]C.CFTypeRef{ SynchronizableNo: C.CFTypeRef(C.kCFBooleanFalse), } -// Accessible is the items accessibility +// Accessible is the items accessibility. type Accessible int const ( - // AccessibleDefault is the default + // AccessibleDefault is the default. AccessibleDefault Accessible = 0 - // AccessibleWhenUnlocked is when unlocked + // AccessibleWhenUnlocked is when unlocked. AccessibleWhenUnlocked = 1 - // AccessibleAfterFirstUnlock is after first unlock + // AccessibleAfterFirstUnlock is after first unlock. AccessibleAfterFirstUnlock = 2 - // AccessibleAlways is always + // AccessibleAlways is always. AccessibleAlways = 3 - // AccessibleWhenPasscodeSetThisDeviceOnly is when passcode is set + // AccessibleWhenPasscodeSetThisDeviceOnly is when passcode is set. AccessibleWhenPasscodeSetThisDeviceOnly = 4 - // AccessibleWhenUnlockedThisDeviceOnly is when unlocked for this device only + // AccessibleWhenUnlockedThisDeviceOnly is when unlocked for this device only. AccessibleWhenUnlockedThisDeviceOnly = 5 - // AccessibleAfterFirstUnlockThisDeviceOnly is after first unlock for this device only + // AccessibleAfterFirstUnlockThisDeviceOnly is after first unlock for this device only. AccessibleAfterFirstUnlockThisDeviceOnly = 6 - // AccessibleAccessibleAlwaysThisDeviceOnly is always for this device only + // AccessibleAccessibleAlwaysThisDeviceOnly is always for this device only. AccessibleAccessibleAlwaysThisDeviceOnly = 7 ) -// MatchLimit is whether to limit results on query +// MatchLimit is whether to limit results on query. type MatchLimit int const ( - // MatchLimitDefault is the default + // MatchLimitDefault is the default. MatchLimitDefault MatchLimit = 0 - // MatchLimitOne limits to one result + // MatchLimitOne limits to one result. MatchLimitOne = 1 - // MatchLimitAll is no limit + // MatchLimitAll is no limit. MatchLimitAll = 2 ) -// MatchLimitKey is key type for MatchLimit +// MatchLimitKey is key type for MatchLimit. var MatchLimitKey = attrKey(C.CFTypeRef(C.kSecMatchLimit)) var matchTypeRef = map[MatchLimit]C.CFTypeRef{ MatchLimitOne: C.CFTypeRef(C.kSecMatchLimitOne), MatchLimitAll: C.CFTypeRef(C.kSecMatchLimitAll), } -// ReturnAttributesKey is key type for kSecReturnAttributes +// ReturnAttributesKey is key type for kSecReturnAttributes. var ReturnAttributesKey = attrKey(C.CFTypeRef(C.kSecReturnAttributes)) -// ReturnDataKey is key type for kSecReturnData +// ReturnDataKey is key type for kSecReturnData. var ReturnDataKey = attrKey(C.CFTypeRef(C.kSecReturnData)) -// ReturnRefKey is key type for kSecReturnRef +// ReturnRefKey is key type for kSecReturnRef. var ReturnRefKey = attrKey(C.CFTypeRef(C.kSecReturnRef)) // Item for adding, querying or deleting. @@ -265,12 +272,12 @@ type Item struct { attr map[string]interface{} } -// SetSecClass sets the security class +// SetSecClass sets the security class. func (k *Item) SetSecClass(sc SecClass) { k.attr[SecClassKey] = secClassTypeRef[sc] } -// SetInt32 sets an int32 attribute for a string key +// SetInt32 sets an int32 attribute for a string key. func (k *Item) SetInt32(key string, v int32) { if v != 0 { k.attr[key] = v @@ -279,7 +286,7 @@ func (k *Item) SetInt32(key string, v int32) { } } -// SetString sets a string attibute for a string key +// SetString sets a string attibute for a string key. func (k *Item) SetString(key string, s string) { if s != "" { k.attr[key] = s @@ -288,58 +295,58 @@ func (k *Item) SetString(key string, s string) { } } -// SetService sets the service attribute (for generic application items) +// SetService sets the service attribute (for generic application items). func (k *Item) SetService(s string) { k.SetString(ServiceKey, s) } -// SetServer sets the server attribute (for internet password items) +// SetServer sets the server attribute (for internet password items). func (k *Item) SetServer(s string) { k.SetString(ServerKey, s) } -// SetProtocol sets the protocol attribute (for internet password items) -// Example values are: "htps", "http", "smb " +// SetProtocol sets the protocol attribute (for internet password items). +// Example values are: "htps", "http", "smb ". func (k *Item) SetProtocol(s string) { k.SetString(ProtocolKey, s) } -// SetAuthenticationType sets the authentication type attribute (for internet password items) +// SetAuthenticationType sets the authentication type attribute (for internet password items). func (k *Item) SetAuthenticationType(s string) { k.SetString(AuthenticationTypeKey, s) } -// SetPort sets the port attribute (for internet password items) +// SetPort sets the port attribute (for internet password items). func (k *Item) SetPort(v int32) { k.SetInt32(PortKey, v) } -// SetPath sets the path attribute (for internet password items) +// SetPath sets the path attribute (for internet password items). func (k *Item) SetPath(s string) { k.SetString(PathKey, s) } -// SetAccount sets the account attribute +// SetAccount sets the account attribute. func (k *Item) SetAccount(a string) { k.SetString(AccountKey, a) } -// SetLabel sets the label attribute +// SetLabel sets the label attribute. func (k *Item) SetLabel(l string) { k.SetString(LabelKey, l) } -// SetDescription sets the description attribute +// SetDescription sets the description attribute. func (k *Item) SetDescription(s string) { k.SetString(DescriptionKey, s) } -// SetComment sets the comment attribute +// SetComment sets the comment attribute. func (k *Item) SetComment(s string) { k.SetString(CommentKey, s) } -// SetData sets the data attribute +// SetData sets the data attribute. func (k *Item) SetData(b []byte) { if b != nil { k.attr[DataKey] = b @@ -348,12 +355,12 @@ func (k *Item) SetData(b []byte) { } } -// SetAccessGroup sets the access group attribute +// SetAccessGroup sets the access group attribute. func (k *Item) SetAccessGroup(ag string) { k.SetString(AccessGroupKey, ag) } -// SetSynchronizable sets the synchronizable attribute +// SetSynchronizable sets the synchronizable attribute. func (k *Item) SetSynchronizable(sync Synchronizable) { if sync != SynchronizableDefault { k.attr[SynchronizableKey] = syncTypeRef[sync] @@ -362,7 +369,7 @@ func (k *Item) SetSynchronizable(sync Synchronizable) { } } -// SetAccessible sets the accessible attribute +// SetAccessible sets the accessible attribute. func (k *Item) SetAccessible(accessible Accessible) { if accessible != AccessibleDefault { k.attr[AccessibleKey] = accessibleTypeRef[accessible] @@ -371,7 +378,7 @@ func (k *Item) SetAccessible(accessible Accessible) { } } -// SetMatchLimit sets the match limit +// SetMatchLimit sets the match limit. func (k *Item) SetMatchLimit(matchLimit MatchLimit) { if matchLimit != MatchLimitDefault { k.attr[MatchLimitKey] = matchTypeRef[matchLimit] @@ -380,22 +387,22 @@ func (k *Item) SetMatchLimit(matchLimit MatchLimit) { } } -// SetReturnAttributes sets the return value type on query +// SetReturnAttributes sets the return value type on query. func (k *Item) SetReturnAttributes(b bool) { k.attr[ReturnAttributesKey] = b } -// SetReturnData enables returning data on query +// SetReturnData enables returning data on query. func (k *Item) SetReturnData(b bool) { k.attr[ReturnDataKey] = b } -// SetReturnRef enables returning references on query +// SetReturnRef enables returning references on query. func (k *Item) SetReturnRef(b bool) { k.attr[ReturnRefKey] = b } -// NewItem is a new empty keychain item +// NewItem is a new empty keychain item. func NewItem() Item { return Item{make(map[string]interface{})} } @@ -409,46 +416,55 @@ func NewGenericPassword(service string, account string, label string, data []byt item.SetLabel(label) item.SetData(data) item.SetAccessGroup(accessGroup) + return item } -// AddItem adds a Item to a Keychain +// AddItem adds a Item to a Keychain. func AddItem(item Item) error { cfDict, err := ConvertMapToCFDictionary(item.attr) if err != nil { - return err + return fmt.Errorf("failed to convert item attributes to CFDictionary: %w", err) } + defer Release(C.CFTypeRef(cfDict)) - errCode := C.SecItemAdd(cfDict, nil) + errCode := C.SecItemAdd(cfDict, nil) // nolint:nlreturn err = checkError(errCode) + return err } -// UpdateItem updates the queryItem with the parameters from updateItem +// UpdateItem updates the queryItem with the parameters from updateItem. func UpdateItem(queryItem Item, updateItem Item) error { cfDict, err := ConvertMapToCFDictionary(queryItem.attr) if err != nil { - return err + return fmt.Errorf("failed to convert query item attributes to CFDictionary: %w", err) } + defer Release(C.CFTypeRef(cfDict)) + cfDictUpdate, err := ConvertMapToCFDictionary(updateItem.attr) if err != nil { - return err + return fmt.Errorf("failed to convert update item attributes to CFDictionary: %w", err) } + defer Release(C.CFTypeRef(cfDictUpdate)) - errCode := C.SecItemUpdate(cfDict, cfDictUpdate) + + errCode := C.SecItemUpdate(cfDict, cfDictUpdate) // nolint:nlreturn + err = checkError(errCode) + return err } // QueryResult stores all possible results from queries. // Not all fields are applicable all the time. Results depend on query. type QueryResult struct { - // For generic application items + // For generic application items. Service string - // For internet password items + // For internet password items. Server string Protocol string AuthenticationType string @@ -474,14 +490,17 @@ func QueryItemRef(item Item) (C.CFTypeRef, error) { defer Release(C.CFTypeRef(cfDict)) var resultsRef C.CFTypeRef + errCode := C.SecItemCopyMatching(cfDict, &resultsRef) //nolint if Error(errCode) == ErrorItemNotFound { return 0, nil } + err = checkError(errCode) if err != nil { return 0, err } + return resultsRef, nil } @@ -498,36 +517,41 @@ func QueryItem(item Item) ([]QueryResult, error) { results := make([]QueryResult, 0, 1) - typeID := C.CFGetTypeID(resultsRef) - if typeID == C.CFArrayGetTypeID() { + typeID := C.CFGetTypeID(resultsRef) //nolint:nlreturn + + switch typeID { + case C.CFArrayGetTypeID(): arr := CFArrayToArray(C.CFArrayRef(resultsRef)) for _, ref := range arr { - elementTypeID := C.CFGetTypeID(ref) + elementTypeID := C.CFGetTypeID(ref) //nolint:nlreturn if elementTypeID == C.CFDictionaryGetTypeID() { item, err := convertResult(C.CFDictionaryRef(ref)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert CFDictionary to QueryResult: %w", err) } + results = append(results, *item) } else { return nil, fmt.Errorf("invalid result type (If you SetReturnRef(true) you should use QueryItemRef directly)") } } - } else if typeID == C.CFDictionaryGetTypeID() { + case C.CFDictionaryGetTypeID(): item, err := convertResult(C.CFDictionaryRef(resultsRef)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert CFDictionary to QueryResult: %w", err) } + results = append(results, *item) - } else if typeID == C.CFDataGetTypeID() { + case C.CFDataGetTypeID(): b, err := CFDataToBytes(C.CFDataRef(resultsRef)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert CFData to bytes: %w", err) } + item := QueryResult{Data: b} results = append(results, item) - } else { - return nil, fmt.Errorf("Invalid result type: %s", CFTypeDescription(resultsRef)) + default: + return nil, fmt.Errorf("invalid result type: %s", CFTypeDescription(resultsRef)) } return results, nil @@ -539,7 +563,9 @@ func attrKey(ref C.CFTypeRef) string { func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { m := CFDictionaryToMap(d) + result := QueryResult{} + for k, v := range m { switch attrKey(k) { case ServiceKey: @@ -552,7 +578,13 @@ func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { result.AuthenticationType = CFStringToString(C.CFStringRef(v)) case PortKey: val := CFNumberToInterface(C.CFNumberRef(v)) - result.Port = val.(int32) + + port, ok := val.(int32) + if !ok { + return nil, fmt.Errorf("expected int32 for PortKey, got %T", val) + } + + result.Port = port case PathKey: result.Path = CFStringToString(C.CFStringRef(v)) case AccountKey: @@ -568,8 +600,9 @@ func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { case DataKey: b, err := CFDataToBytes(C.CFDataRef(v)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert CFData to bytes: %w", err) } + result.Data = b case CreationDateKey: result.CreationDate = CFDateToTime(C.CFDateRef(v)) @@ -579,6 +612,7 @@ func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { // fmt.Printf("Unhandled key in conversion: %v = %v\n", cfTypeValue(k), cfTypeValue(v)) } } + return &result, nil } @@ -588,22 +622,25 @@ func DeleteGenericPasswordItem(service string, account string) error { item.SetSecClass(SecClassGenericPassword) item.SetService(service) item.SetAccount(account) + return DeleteItem(item) } -// DeleteItem removes a Item +// DeleteItem removes a Item. func DeleteItem(item Item) error { cfDict, err := ConvertMapToCFDictionary(item.attr) if err != nil { - return err + return fmt.Errorf("failed to convert item to CFDictionary: %w", err) } + defer Release(C.CFTypeRef(cfDict)) - errCode := C.SecItemDelete(cfDict) + errCode := C.SecItemDelete(cfDict) // nolint:nlreturn + return checkError(errCode) } -// GetAccountsForService is deprecated +// GetAccountsForService is deprecated. func GetAccountsForService(service string) ([]string, error) { return GetGenericPasswordAccounts(service) } @@ -615,6 +652,7 @@ func GetGenericPasswordAccounts(service string) ([]string, error) { query.SetService(service) query.SetMatchLimit(MatchLimitAll) query.SetReturnAttributes(true) + results, err := QueryItem(query) if err != nil { return nil, err @@ -639,15 +677,19 @@ func GetGenericPassword(service string, account string, label string, accessGrou query.SetAccessGroup(accessGroup) query.SetMatchLimit(MatchLimitOne) query.SetReturnData(true) + results, err := QueryItem(query) if err != nil { return nil, err } + if len(results) > 1 { - return nil, fmt.Errorf("Too many results") + return nil, fmt.Errorf("too many results") } + if len(results) == 1 { return results[0].Data, nil } + return nil, nil } diff --git a/macos.go b/macos.go index 366cc42..a795ccd 100644 --- a/macos.go +++ b/macos.go @@ -10,7 +10,7 @@ package keychain */ import "C" -// AccessibleKey is key for kSecAttrAccessible +// AccessibleKey is key for kSecAttrAccessible. var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) var accessibleTypeRef = map[Accessible]C.CFTypeRef{ AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), diff --git a/secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7.go b/secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7.go index 77ef893..d4ca41c 100644 --- a/secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7.go +++ b/secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7.go @@ -9,6 +9,9 @@ // and is NOT CCA2-secure. It is only meant to hide the D-Bus messages from any // system services that may be logging everything. +//go:build linux +// +build linux + package secretservice import ( @@ -34,13 +37,16 @@ var bigOne = big.NewInt(1) func (group *dhGroup) NewKeypair() (private *big.Int, public *big.Int, err error) { for { if private, err = cryptorand.Int(cryptorand.Reader, group.pMinus1); err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to generate random private key: %w", err) } + if private.Sign() > 0 { break } } + public = new(big.Int).Exp(group.g, private, group.p) + return private, public, nil } @@ -48,11 +54,13 @@ func (group *dhGroup) diffieHellman(theirPublic, myPrivate *big.Int) (*big.Int, if theirPublic.Cmp(bigOne) <= 0 || theirPublic.Cmp(group.pMinus1) >= 0 { return nil, errors.New("ssh: DH parameter out of bounds") } + return new(big.Int).Exp(theirPublic, myPrivate, group.p), nil } func rfc2409SecondOakleyGroup() *dhGroup { p, _ := new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF", 16) + return &dhGroup{ g: new(big.Int).SetInt64(2), p: p, @@ -63,16 +71,18 @@ func rfc2409SecondOakleyGroup() *dhGroup { func (group *dhGroup) keygenHKDFSHA256AES128(theirPublic *big.Int, myPrivate *big.Int) ([]byte, error) { sharedSecret, err := group.diffieHellman(theirPublic, myPrivate) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to compute shared secret: %w", err) } + sharedSecretBytes := sharedSecret.Bytes() r := hkdf.New(sha256.New, sharedSecretBytes, nil, nil) aesKey := make([]byte, 16) + _, err = io.ReadFull(r, aesKey) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read AES key from HKDF: %w", err) } return aesKey, nil @@ -80,41 +90,52 @@ func (group *dhGroup) keygenHKDFSHA256AES128(theirPublic *big.Int, myPrivate *bi func unauthenticatedAESCBCEncrypt(unpaddedPlaintext []byte, key []byte) (iv []byte, ciphertext []byte, err error) { paddedPlaintext := padPKCS7(unpaddedPlaintext, aes.BlockSize) + block, err := aes.NewCipher(key) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to create AES cipher: %w", err) } + ivSize := aes.BlockSize iv = make([]byte, ivSize) ciphertext = make([]byte, len(paddedPlaintext)) + if _, err := io.ReadFull(cryptorand.Reader, iv); err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to read random bytes for IV: %w", err) } + mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(ciphertext, paddedPlaintext) + return iv, ciphertext, nil } func unauthenticatedAESCBCDecrypt(iv []byte, ciphertext []byte, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create AES cipher: %w", err) } + if len(iv) != aes.BlockSize { return nil, fmt.Errorf("iv length not aes blocksize") } + if len(ciphertext) < aes.BlockSize { return nil, fmt.Errorf("ciphertext smaller than AES block size") } + if len(ciphertext)%aes.BlockSize != 0 { return nil, fmt.Errorf("aes ciphertext not a multiple of blocksize") } + mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(ciphertext, ciphertext) // decrypt in-place + plaintext, err := unpadPKCS7(ciphertext, aes.BlockSize) if err != nil { return nil, err } + return plaintext, nil } @@ -123,6 +144,7 @@ func padPKCS7(xs []byte, n int) []byte { if m == 0 { m = 16 } + return append(xs, bytes.Repeat([]byte{m}, int(m))...) } @@ -130,18 +152,23 @@ func unpadPKCS7(xs []byte, n int) ([]byte, error) { if len(xs) == 0 { return nil, fmt.Errorf("cannot unpad empty bytearray") } + if len(xs)%n != 0 { return nil, fmt.Errorf("length of bytearray not a multiple of blocksize") } + lastByte := xs[len(xs)-1] + padStartIdx := len(xs) - int(lastByte) if padStartIdx < 0 { return nil, fmt.Errorf("invalid pkcs7 padding; pad byte larger than number of characters") } + for i := padStartIdx; i < len(xs); i++ { if xs[i] != lastByte { return nil, fmt.Errorf("expected pad character %x, got %x", lastByte, xs[i]) } } + return xs[:padStartIdx], nil } diff --git a/secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7_test.go b/secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7_test.go index be18633..774d55e 100644 --- a/secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7_test.go +++ b/secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7_test.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package secretservice import ( diff --git a/secretservice/secretservice.go b/secretservice/secretservice.go index f7f1836..6431cbd 100644 --- a/secretservice/secretservice.go +++ b/secretservice/secretservice.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package secretservice import ( @@ -6,34 +9,26 @@ import ( "math/big" "time" - dbus "github.com/keybase/dbus" + dbus "github.com/godbus/dbus/v5" ) -// SecretServiceInterface const SecretServiceInterface = "org.freedesktop.secrets" -// SecretServiceObjectPath const SecretServiceObjectPath dbus.ObjectPath = "/org/freedesktop/secrets" // DefaultCollection need not necessarily exist in the user's keyring. const DefaultCollection dbus.ObjectPath = "/org/freedesktop/secrets/aliases/default" -// AuthenticationMode type AuthenticationMode string -// AuthenticationInsecurePlain const AuthenticationInsecurePlain AuthenticationMode = "plain" -// AuthenticationDHAES const AuthenticationDHAES AuthenticationMode = "dh-ietf1024-sha256-aes128-cbc-pkcs7" -// NilFlags const NilFlags = 0 -// Attributes type Attributes map[string]string -// Secret type Secret struct { Session dbus.ObjectPath Parameters []byte @@ -41,20 +36,17 @@ type Secret struct { ContentType string } -// PromptCompletedResult type PromptCompletedResult struct { Dismissed bool Paths dbus.Variant } -// SecretService type SecretService struct { conn *dbus.Conn signalCh <-chan *dbus.Signal sessionOpenTimeout time.Duration } -// Session type Session struct { Mode AuthenticationMode Path dbus.ObjectPath @@ -63,32 +55,30 @@ type Session struct { AESKey []byte } -// DefaultSessionOpenTimeout const DefaultSessionOpenTimeout = 10 * time.Second -// NewService func NewService() (*SecretService, error) { conn, err := dbus.ConnectSessionBus() if err != nil { return nil, fmt.Errorf("failed to open dbus connection: %w", err) } + signalCh := make(chan *dbus.Signal, 16) conn.Signal(signalCh) + _ = conn.AddMatchSignal(dbus.WithMatchOption("org.freedesktop.Secret.Prompt", "Completed")) + return &SecretService{conn: conn, signalCh: signalCh, sessionOpenTimeout: DefaultSessionOpenTimeout}, nil } -// SetSessionOpenTimeout func (s *SecretService) SetSessionOpenTimeout(d time.Duration) { s.sessionOpenTimeout = d } -// ServiceObj func (s *SecretService) ServiceObj() dbus.BusObject { return s.conn.Object(SecretServiceInterface, SecretServiceObjectPath) } -// Obj func (s *SecretService) Obj(path dbus.ObjectPath) dbus.BusObject { return s.conn.Object(SecretServiceInterface, path) } @@ -105,10 +95,10 @@ func (s *SecretService) openSessionRaw(mode AuthenticationMode, sessionAlgorithm if err != nil { return sessionOpenResponse{}, fmt.Errorf("failed to open secretservice session: %w", err) } + return resp, nil } -// OpenSession func (s *SecretService) OpenSession(mode AuthenticationMode) (session *Session, err error) { var sessionAlgorithmInput dbus.Variant @@ -121,10 +111,12 @@ func (s *SecretService) OpenSession(mode AuthenticationMode) (session *Session, sessionAlgorithmInput = dbus.MakeVariant("") case AuthenticationDHAES: group := rfc2409SecondOakleyGroup() + private, public, err := group.NewKeypair() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to generate keypair: %w", err) } + session.Private = private session.Public = public sessionAlgorithmInput = dbus.MakeVariant(public.Bytes()) // math/big.Int.Bytes is big endian @@ -134,6 +126,7 @@ func (s *SecretService) OpenSession(mode AuthenticationMode) (session *Session, sessionOpenCh := make(chan sessionOpenResponse) errCh := make(chan error) + go func() { resp, err := s.openSessionRaw(mode, sessionAlgorithmInput) if err != nil { @@ -154,7 +147,7 @@ func (s *SecretService) OpenSession(mode AuthenticationMode) (session *Session, sessionAlgorithmOutput = resp.algorithmOutput session.Path = resp.path case err := <-errCh: - return nil, err + return nil, fmt.Errorf("failed to open session: %w", err) case <-time.After(s.sessionOpenTimeout): return nil, fmt.Errorf("timed out after %s", s.sessionOpenTimeout) } @@ -166,13 +159,16 @@ func (s *SecretService) OpenSession(mode AuthenticationMode) (session *Session, if !ok { return nil, errors.New("failed to coerce algorithm output value to byteslice") } + group := rfc2409SecondOakleyGroup() theirPublic := new(big.Int) theirPublic.SetBytes(theirPublicBigEndian) + aesKey, err := group.keygenHKDFSHA256AES128(theirPublic, session.Private) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to generate AES key: %w", err) } + session.AESKey = aesKey default: return nil, fmt.Errorf("unknown authentication mode %v", mode) @@ -181,12 +177,10 @@ func (s *SecretService) OpenSession(mode AuthenticationMode) (session *Session, return session, nil } -// CloseSession func (s *SecretService) CloseSession(session *Session) { s.Obj(session.Path).Call("org.freedesktop.Secret.Session.Close", NilFlags) } -// SearchCollection func (s *SecretService) SearchCollection(collection dbus.ObjectPath, attributes Attributes) (items []dbus.ObjectPath, err error) { err = s.Obj(collection). Call("org.freedesktop.Secret.Collection.SearchItems", NilFlags, attributes). @@ -194,21 +188,19 @@ func (s *SecretService) SearchCollection(collection dbus.ObjectPath, attributes if err != nil { return nil, fmt.Errorf("failed to search collection: %w", err) } + return items, nil } -// ReplaceBehavior type ReplaceBehavior int -// ReplaceBehaviorDoNotReplace const ReplaceBehaviorDoNotReplace = 0 -// ReplaceBehaviorReplace const ReplaceBehaviorReplace = 1 -// CreateItem func (s *SecretService) CreateItem(collection dbus.ObjectPath, properties map[string]dbus.Variant, secret Secret, replaceBehavior ReplaceBehavior) (item dbus.ObjectPath, err error) { var replace bool + switch replaceBehavior { case ReplaceBehaviorDoNotReplace: replace = false @@ -219,58 +211,66 @@ func (s *SecretService) CreateItem(collection dbus.ObjectPath, properties map[st } var prompt dbus.ObjectPath + err = s.Obj(collection). Call("org.freedesktop.Secret.Collection.CreateItem", NilFlags, properties, secret, replace). Store(&item, &prompt) if err != nil { return "", fmt.Errorf("failed to create item: %w", err) } + _, err = s.PromptAndWait(prompt) if err != nil { - return "", err + return "", fmt.Errorf("failed to prompt for item creation: %w", err) } + return item, nil } -// DeleteItem func (s *SecretService) DeleteItem(item dbus.ObjectPath) (err error) { var prompt dbus.ObjectPath + err = s.Obj(item). Call("org.freedesktop.Secret.Item.Delete", NilFlags). Store(&prompt) if err != nil { return fmt.Errorf("failed to delete item: %w", err) } + _, err = s.PromptAndWait(prompt) if err != nil { - return err + return fmt.Errorf("failed to prompt for item deletion: %w", err) } + return nil } -// GetAttributes func (s *SecretService) GetAttributes(item dbus.ObjectPath) (attributes Attributes, err error) { attributesV, err := s.Obj(item).GetProperty("org.freedesktop.Secret.Item.Attributes") if err != nil { return nil, fmt.Errorf("failed to get attributes: %w", err) } + attributesMap, ok := attributesV.Value().(map[string]string) if !ok { return nil, errors.New("failed to coerce item attributes") } + return Attributes(attributesMap), nil } -// GetSecret func (s *SecretService) GetSecret(item dbus.ObjectPath, session Session) (secretPlaintext []byte, err error) { var secretI []interface{} + err = s.Obj(item). Call("org.freedesktop.Secret.Item.GetSecret", NilFlags, session.Path). Store(&secretI) if err != nil { return nil, fmt.Errorf("failed to get secret: %w", err) } + secret := new(Secret) + err = dbus.Store(secretI, &secret.Session, &secret.Parameters, &secret.Value, &secret.ContentType) if err != nil { return nil, fmt.Errorf("failed to unmarshal get secret result: %w", err) @@ -282,8 +282,9 @@ func (s *SecretService) GetSecret(item dbus.ObjectPath, session Session) (secret case AuthenticationDHAES: plaintext, err := unauthenticatedAESCBCDecrypt(secret.Parameters, secret.Value, session.AESKey) if err != nil { - return nil, nil + return nil, nil // nolint:nilerr } + secretPlaintext = plaintext default: return nil, fmt.Errorf("cannot make secret for authentication mode %v", session.Mode) @@ -292,49 +293,54 @@ func (s *SecretService) GetSecret(item dbus.ObjectPath, session Session) (secret return secretPlaintext, nil } -// NullPrompt const NullPrompt = "/" -// Unlock func (s *SecretService) Unlock(items []dbus.ObjectPath) (err error) { - var dummy []dbus.ObjectPath - var prompt dbus.ObjectPath + var ( + dummy []dbus.ObjectPath + prompt dbus.ObjectPath + ) + err = s.ServiceObj(). Call("org.freedesktop.Secret.Service.Unlock", NilFlags, items). Store(&dummy, &prompt) if err != nil { return fmt.Errorf("failed to unlock items: %w", err) } + _, err = s.PromptAndWait(prompt) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } + return nil } -// LockItems func (s *SecretService) LockItems(items []dbus.ObjectPath) (err error) { - var dummy []dbus.ObjectPath - var prompt dbus.ObjectPath + var ( + dummy []dbus.ObjectPath + prompt dbus.ObjectPath + ) + err = s.ServiceObj(). Call("org.freedesktop.Secret.Service.Lock", NilFlags, items). Store(&dummy, &prompt) if err != nil { return fmt.Errorf("failed to lock items: %w", err) } + _, err = s.PromptAndWait(prompt) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } + return nil } -// PromptDismissedError type PromptDismissedError struct { err error } -// Error func (p PromptDismissedError) Error() string { return p.err.Error() } @@ -342,32 +348,40 @@ func (p PromptDismissedError) Error() string { // PromptAndWait is NOT thread-safe. func (s *SecretService) PromptAndWait(prompt dbus.ObjectPath) (paths *dbus.Variant, err error) { if prompt == NullPrompt { - return nil, nil + return nil, nil // nolint:nilerr } + call := s.Obj(prompt).Call("org.freedesktop.Secret.Prompt.Prompt", NilFlags, "Keyring Prompt") if call.Err != nil { return nil, fmt.Errorf("failed to prompt: %w", call.Err) } + for { var result PromptCompletedResult + select { case signal, ok := <-s.signalCh: if !ok { return nil, errors.New("prompt channel closed") } + if signal == nil { continue } + if signal.Name != "org.freedesktop.Secret.Prompt.Completed" { continue } + err = dbus.Store(signal.Body, &result.Dismissed, &result.Paths) if err != nil { return nil, fmt.Errorf("failed to unmarshal prompt result: %w", err) } + if result.Dismissed { return nil, PromptDismissedError{errors.New("prompt dismissed")} } + return &result.Paths, nil case <-time.After(30 * time.Second): return nil, errors.New("prompt timed out") @@ -375,7 +389,6 @@ func (s *SecretService) PromptAndWait(prompt dbus.ObjectPath) (paths *dbus.Varia } } -// NewSecretProperties func NewSecretProperties(label string, attributes map[string]string) map[string]dbus.Variant { return map[string]dbus.Variant{ "org.freedesktop.Secret.Item.Label": dbus.MakeVariant(label), @@ -383,7 +396,6 @@ func NewSecretProperties(label string, attributes map[string]string) map[string] } } -// NewSecret func (session *Session) NewSecret(secretBytes []byte) (Secret, error) { switch session.Mode { case AuthenticationInsecurePlain: @@ -398,6 +410,7 @@ func (session *Session) NewSecret(secretBytes []byte) (Secret, error) { if err != nil { return Secret{}, err } + return Secret{ Session: session.Path, Parameters: iv, diff --git a/secretservice/secretservice_test.go b/secretservice/secretservice_test.go index 708de18..166b059 100644 --- a/secretservice/secretservice_test.go +++ b/secretservice/secretservice_test.go @@ -2,15 +2,15 @@ // keyring with a default collection created. // It should prompt you for your keyring password twice. -//go:build !skipsecretserviceintegrationtests -// +build !skipsecretserviceintegrationtests +//go:build !skipsecretserviceintegrationtests && linux +// +build !skipsecretserviceintegrationtests,linux package secretservice import ( "testing" - dbus "github.com/keybase/dbus" + dbus "github.com/godbus/dbus/v5" "github.com/stretchr/testify/require" ) diff --git a/util.go b/util.go index 8e3119d..7306e2e 100644 --- a/util.go +++ b/util.go @@ -3,6 +3,7 @@ package keychain import ( "crypto/rand" "encoding/base32" + "fmt" "strings" ) @@ -13,19 +14,22 @@ var randRead = rand.Read func RandomID(prefix string) (string, error) { buf, err := RandBytes(32) if err != nil { - return "", err + return "", fmt.Errorf("failed to generate random bytes: %w", err) } + str := base32.StdEncoding.EncodeToString(buf) str = strings.ReplaceAll(str, "=", "") str = prefix + str + return str, nil } -// RandBytes returns random bytes of length +// RandBytes returns random bytes of length. func RandBytes(length int) ([]byte, error) { buf := make([]byte, length) if _, err := randRead(buf); err != nil { - return nil, err + return nil, fmt.Errorf("failed to read random bytes: %w", err) } + return buf, nil }