├── ios ├── bind.framework │ ├── Versions │ │ ├── Current │ │ └── A │ │ │ ├── Bind │ │ │ ├── Modules │ │ │ └── module.modulemap │ │ │ ├── Resources │ │ │ └── Info.plist │ │ │ └── Headers │ │ │ ├── Bind.h │ │ │ ├── Universe.objc.h │ │ │ ├── Bind.objc.h │ │ │ └── ref.h │ ├── Bind │ ├── Headers │ ├── Modules │ └── Resources ├── Keychain │ ├── Keychain-Bridging-Header.h │ ├── ViewController.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Keychain.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── project.pbxproj ├── .gitignore └── KeychainTests │ ├── KeychainTests.swift │ └── Info.plist ├── .golangci.yml ├── .gitignore ├── go.mod ├── bindtest └── bind_test.go ├── .github └── workflows │ └── ci.yml ├── util.go ├── ios.go ├── LICENSE ├── macos.go ├── go.sum ├── datetime.go ├── datetime_test.go ├── secretservice ├── secretservice_test.go ├── dh_ietf1024_sha256_aes128_cbc_pkcs7_test.go ├── dh_ietf1024_sha256_aes128_cbc_pkcs7.go └── secretservice.go ├── README.md ├── macos_test.go ├── bind └── bind.go ├── corefoundation.go └── keychain.go /ios/bind.framework/Versions/Current: -------------------------------------------------------------------------------- 1 | A -------------------------------------------------------------------------------- /ios/bind.framework/Bind: -------------------------------------------------------------------------------- 1 | Versions/Current/Bind -------------------------------------------------------------------------------- /ios/bind.framework/Headers: -------------------------------------------------------------------------------- 1 | Versions/Current/Headers -------------------------------------------------------------------------------- /ios/bind.framework/Modules: -------------------------------------------------------------------------------- 1 | Versions/Current/Modules -------------------------------------------------------------------------------- /ios/bind.framework/Resources: -------------------------------------------------------------------------------- 1 | Versions/Current/Resources -------------------------------------------------------------------------------- /ios/bind.framework/Versions/A/Bind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbstack/go-keychain/HEAD/ios/bind.framework/Versions/A/Bind -------------------------------------------------------------------------------- /ios/Keychain/Keychain-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | -------------------------------------------------------------------------------- /ios/bind.framework/Versions/A/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module "Bind" { 2 | header "ref.h" 3 | header "Bind.objc.h" 4 | header "Universe.objc.h" 5 | header "Bind.h" 6 | 7 | export * 8 | } -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | gocritic: 3 | disabled-checks: 4 | - ifElseChain 5 | - elseif 6 | 7 | linters: 8 | enable: 9 | - gofmt 10 | - gocritic 11 | - unconvert 12 | -------------------------------------------------------------------------------- /ios/Keychain.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/bind.framework/Versions/A/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | *.xccheckout 19 | 20 | #CocoaPods 21 | Pods 22 | -------------------------------------------------------------------------------- /ios/bind.framework/Versions/A/Headers/Bind.h: -------------------------------------------------------------------------------- 1 | 2 | // Objective-C API for talking to the following Go packages 3 | // 4 | // github.com/keybase/go-keychain/bind 5 | // 6 | // File is generated by gomobile bind. Do not edit. 7 | #ifndef __Bind_FRAMEWORK_H__ 8 | #define __Bind_FRAMEWORK_H__ 9 | 10 | #include "Bind.objc.h" 11 | #include "Universe.objc.h" 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | vendor 27 | -------------------------------------------------------------------------------- /ios/KeychainTests/KeychainTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainTests.swift 3 | // KeychainTests 4 | // 5 | // Created by Gabriel on 9/25/15. 6 | // Copyright © 2015 Gabriel Handford. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Keychain 11 | 12 | // Tests don't work with bind framework for some reason. 13 | // Using the app. 14 | class KeychainTests: XCTestCase { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keybase/go-keychain 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a 7 | github.com/pkg/errors v0.9.1 8 | github.com/stretchr/testify v1.8.0 9 | golang.org/x/crypto v0.1.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /bindtest/bind_test.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || ios 2 | // +build darwin ios 3 | 4 | package bindtest 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/keybase/go-keychain/bind" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type test struct { 14 | t *testing.T 15 | } 16 | 17 | func (t test) Fail(s string) { 18 | require.Fail(t.t, s) 19 | } 20 | 21 | func TestGenericPassword(t *testing.T) { 22 | service := "Testing service as unicode テスト" 23 | accessGroup := "" 24 | bind.GenericPasswordTest(test{t}, service, accessGroup) 25 | } 26 | -------------------------------------------------------------------------------- /ios/Keychain/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Keychain 4 | // 5 | // Created by Gabriel on 9/25/15. 6 | // Copyright © 2015 Gabriel Handford. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /ios/bind.framework/Versions/A/Headers/Universe.objc.h: -------------------------------------------------------------------------------- 1 | // Objective-C API for talking to Go package. 2 | // gobind -lang=objc 3 | // 4 | // File is generated by gobind. Do not edit. 5 | 6 | #ifndef __Universe_H__ 7 | #define __Universe_H__ 8 | 9 | @import Foundation; 10 | 11 | @protocol Universeerror; 12 | @class Universeerror; 13 | 14 | @protocol Universeerror 15 | - (NSString*)error; 16 | @end 17 | 18 | @class Universeerror; 19 | 20 | @interface Universeerror : NSError { 21 | } 22 | @property(strong, readonly) id _ref; 23 | 24 | - (instancetype)initWithRef:(id)ref; 25 | - (NSString*)error; 26 | @end 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: [1.17.x, 1.18.x, 1.19.x] 14 | os: [ubuntu-latest, macos-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - uses: actions/checkout@v3 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | version: v1.50 25 | - run: go vet ./... 26 | - run: go test -tags skipsecretserviceintegrationtests ./... 27 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package keychain 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base32" 6 | "strings" 7 | ) 8 | 9 | var randRead = rand.Read 10 | 11 | // RandomID returns random ID (base32) string with prefix, using 256 bits as 12 | // recommended by tptacek: https://gist.github.com/tqbf/be58d2d39690c3b366ad 13 | func RandomID(prefix string) (string, error) { 14 | buf, err := RandBytes(32) 15 | if err != nil { 16 | return "", err 17 | } 18 | str := base32.StdEncoding.EncodeToString(buf) 19 | str = strings.ReplaceAll(str, "=", "") 20 | str = prefix + str 21 | return str, nil 22 | } 23 | 24 | // RandBytes returns random bytes of length 25 | func RandBytes(length int) ([]byte, error) { 26 | buf := make([]byte, length) 27 | if _, err := randRead(buf); err != nil { 28 | return nil, err 29 | } 30 | return buf, nil 31 | } 32 | -------------------------------------------------------------------------------- /ios/KeychainTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/Keychain/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Keychain 4 | // 5 | // Created by Gabriel on 9/25/15. 6 | // Copyright © 2015 Gabriel Handford. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | 18 | var error: NSError? 19 | 20 | BindAddGenericPassword("KeybaseTest", "gabriel", "A label", "toomanysecrets", nil, &error); 21 | if (error != nil) { 22 | print("Failed: \(String(describing: error))") 23 | } else { 24 | print("Add OK") 25 | } 26 | 27 | BindDeleteGenericPassword("KeybaseTest", "gabriel", nil, &error) 28 | if (error != nil) { 29 | print("Failed: \(String(describing: error))") 30 | } else { 31 | print("Delete OK") 32 | } 33 | 34 | return true 35 | } 36 | 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /ios.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && ios 2 | // +build darwin,ios 3 | 4 | package keychain 5 | 6 | /* 7 | #cgo LDFLAGS: -framework CoreFoundation -framework Security 8 | 9 | #include 10 | #include 11 | */ 12 | import "C" 13 | 14 | var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) 15 | var accessibleTypeRef = map[Accessible]C.CFTypeRef{ 16 | AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), 17 | AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), 18 | AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), 19 | AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), 20 | AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), 21 | AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), 22 | AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Keybase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ios/bind.framework/Versions/A/Headers/Bind.objc.h: -------------------------------------------------------------------------------- 1 | // Objective-C API for talking to github.com/keybase/go-keychain/bind Go package. 2 | // gobind -lang=objc github.com/keybase/go-keychain/bind 3 | // 4 | // File is generated by gobind. Do not edit. 5 | 6 | #ifndef __Bind_H__ 7 | #define __Bind_H__ 8 | 9 | @import Foundation; 10 | #include "Universe.objc.h" 11 | 12 | 13 | @protocol BindTest; 14 | @class BindTest; 15 | 16 | @protocol BindTest 17 | - (void)fail:(NSString*)s; 18 | @end 19 | 20 | FOUNDATION_EXPORT BOOL BindAddGenericPassword(NSString* service, NSString* account, NSString* label, NSString* password, NSString* accessGroup, NSError** error); 21 | 22 | FOUNDATION_EXPORT BOOL BindDeleteGenericPassword(NSString* service, NSString* account, NSString* accessGroup, NSError** error); 23 | 24 | FOUNDATION_EXPORT void BindGenericPasswordTest(id t, NSString* service, NSString* accessGroup); 25 | 26 | @class BindTest; 27 | 28 | @interface BindTest : NSObject { 29 | } 30 | @property(strong, readonly) id _ref; 31 | 32 | - (instancetype)initWithRef:(id)ref; 33 | - (void)fail:(NSString*)s; 34 | @end 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /macos.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !ios 2 | // +build darwin,!ios 3 | 4 | package keychain 5 | 6 | /* 7 | #cgo LDFLAGS: -framework CoreFoundation -framework Security 8 | #include 9 | #include 10 | */ 11 | import "C" 12 | 13 | // AccessibleKey is key for kSecAttrAccessible 14 | var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) 15 | var accessibleTypeRef = map[Accessible]C.CFTypeRef{ 16 | AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), 17 | AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), 18 | AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), 19 | AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), 20 | AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), 21 | AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), 22 | 23 | // Only available in 10.10 24 | //AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), 25 | } 26 | -------------------------------------------------------------------------------- /ios/bind.framework/Versions/A/Headers/ref.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | #ifndef __GO_REF_HDR__ 6 | #define __GO_REF_HDR__ 7 | 8 | #include 9 | 10 | // GoSeqRef is an object tagged with an integer for passing back and 11 | // forth across the language boundary. A GoSeqRef may represent either 12 | // an instance of a Go object, or an Objective-C object passed to Go. 13 | // The explicit allocation of a GoSeqRef is used to pin a Go object 14 | // when it is passed to Objective-C. The Go seq package maintains a 15 | // reference to the Go object in a map keyed by the refnum along with 16 | // a reference count. When the reference count reaches zero, the Go 17 | // seq package will clear the corresponding entry in the map. 18 | @interface GoSeqRef : NSObject { 19 | } 20 | @property(readonly) int32_t refnum; 21 | @property(strong) id obj; // NULL when representing a Go object. 22 | 23 | // new GoSeqRef object to proxy a Go object. The refnum must be 24 | // provided from Go side. 25 | - (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; 26 | 27 | - (int32_t)incNum; 28 | 29 | @end 30 | 31 | @protocol goSeqRefInterface 32 | -(GoSeqRef*) _ref; 33 | @end 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /ios/Keychain/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /ios/Keychain/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ios/Keychain/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ios/Keychain/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a h1:K0EAzgzEQHW4Y5lxrmvPMltmlRDzlhLfGmots9EHUTI= 5 | github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8= 6 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 7 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 12 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 14 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 15 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= 16 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /datetime.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || ios 2 | // +build darwin ios 3 | 4 | package keychain 5 | 6 | /* 7 | #cgo LDFLAGS: -framework CoreFoundation 8 | 9 | #include 10 | */ 11 | import "C" 12 | import ( 13 | "math" 14 | "time" 15 | ) 16 | 17 | const nsPerSec = 1000 * 1000 * 1000 18 | 19 | // absoluteTimeIntervalSince1970() returns the number of seconds from 20 | // the Unix epoch (1970-01-01T00:00:00+00:00) to the Core Foundation 21 | // absolute reference date (2001-01-01T00:00:00+00:00). It should be 22 | // exactly 978307200. 23 | func absoluteTimeIntervalSince1970() int64 { 24 | return int64(C.kCFAbsoluteTimeIntervalSince1970) 25 | } 26 | 27 | func unixToAbsoluteTime(s int64, ns int64) C.CFAbsoluteTime { 28 | // Subtract as int64s first before converting to floating 29 | // point to minimize precision loss (assuming the given time 30 | // isn't much earlier than the Core Foundation absolute 31 | // reference date). 32 | abs := s - absoluteTimeIntervalSince1970() 33 | return C.CFAbsoluteTime(abs) + C.CFTimeInterval(ns)/nsPerSec 34 | } 35 | 36 | func absoluteTimeToUnix(abs C.CFAbsoluteTime) (int64, int64) { 37 | int, frac := math.Modf(float64(abs)) 38 | return int64(int) + absoluteTimeIntervalSince1970(), int64(frac * nsPerSec) 39 | } 40 | 41 | // TimeToCFDate will convert the given time.Time to a CFDateRef, which 42 | // must be released with Release(ref). 43 | func TimeToCFDate(t time.Time) C.CFDateRef { 44 | s := t.Unix() 45 | ns := int64(t.Nanosecond()) 46 | abs := unixToAbsoluteTime(s, ns) 47 | return C.CFDateCreate(C.kCFAllocatorDefault, abs) 48 | } 49 | 50 | // CFDateToTime will convert the given CFDateRef to a time.Time. 51 | func CFDateToTime(d C.CFDateRef) time.Time { 52 | abs := C.CFDateGetAbsoluteTime(d) 53 | s, ns := absoluteTimeToUnix(abs) 54 | return time.Unix(s, ns) 55 | } 56 | 57 | // Wrappers around C functions for testing. 58 | 59 | func cfDateToAbsoluteTime(d C.CFDateRef) C.CFAbsoluteTime { 60 | return C.CFDateGetAbsoluteTime(d) 61 | } 62 | 63 | func absoluteTimeToCFDate(abs C.CFAbsoluteTime) C.CFDateRef { 64 | return C.CFDateCreate(C.kCFAllocatorDefault, abs) 65 | } 66 | 67 | func releaseCFDate(d C.CFDateRef) { 68 | Release(C.CFTypeRef(d)) 69 | } 70 | -------------------------------------------------------------------------------- /datetime_test.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !ios 2 | // +build darwin,!ios 3 | 4 | package keychain 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // Test time is 2018-09-13T06:08:49+00:00 12 | const ( 13 | // Number of seconds between the test time and the Unix epoch 14 | // (1970-01-01T00:00:00+00:00). 15 | testTimeUnixSeconds = 1536818929 16 | // Number of seconds between the test time and 17 | // Core Foundation's absolute reference date 18 | // (2001-01-01T00:00:00+00:00). See 19 | // https://developer.apple.com/documentation/corefoundation/cfabsolutetime?language=objc 20 | testTimeAbsoluteTimeSeconds = 558511729 21 | ) 22 | 23 | func TestUnixToAbsoluteTime(t *testing.T) { 24 | var testNano int64 = 123456789 25 | abs := unixToAbsoluteTime(testTimeUnixSeconds, testNano) 26 | const expectedAbs = testTimeAbsoluteTimeSeconds + 0.123456789 27 | if abs != expectedAbs { 28 | t.Fatalf("expected %f, got %f", expectedAbs, abs) 29 | } 30 | } 31 | 32 | func TestAbsoluteTimeToUnix(t *testing.T) { 33 | const abs = testTimeAbsoluteTimeSeconds + 0.123456789 34 | s, ns := absoluteTimeToUnix(abs) 35 | if s != testTimeUnixSeconds { 36 | t.Fatalf("expected %d, got %d", testTimeUnixSeconds, s) 37 | } 38 | // Some precision loss from floating point. 39 | const expectedNano = 123456835 40 | if ns != expectedNano { 41 | t.Fatalf("expected %d, got %d", expectedNano, ns) 42 | } 43 | } 44 | 45 | func TestTimeToCFDate(t *testing.T) { 46 | var testNano int64 = 123456789 47 | tm := time.Unix(testTimeUnixSeconds, testNano) 48 | d := TimeToCFDate(tm) 49 | defer releaseCFDate(d) 50 | 51 | abs := cfDateToAbsoluteTime(d) 52 | const expectedAbs = testTimeAbsoluteTimeSeconds + 0.123456789 53 | if abs != expectedAbs { 54 | t.Fatalf("expected %f, got %f", expectedAbs, abs) 55 | } 56 | } 57 | 58 | func TestCFDateToTime(t *testing.T) { 59 | const abs = testTimeAbsoluteTimeSeconds + 0.123456789 60 | d := absoluteTimeToCFDate(abs) 61 | defer releaseCFDate(d) 62 | 63 | tm := CFDateToTime(d) 64 | // Some precision loss from floating point. 65 | const expectedNano = testTimeUnixSeconds*nsPerSec + 123456835 66 | nano := tm.UnixNano() 67 | if nano != expectedNano { 68 | t.Fatalf("expected %d, got %d", expectedNano, nano) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /secretservice/secretservice_test.go: -------------------------------------------------------------------------------- 1 | // This test file should only be run locally and requires a Secret Service 2 | // keyring with a default collection created. 3 | // It should prompt you for your keyring password twice. 4 | 5 | //go:build !skipsecretserviceintegrationtests 6 | // +build !skipsecretserviceintegrationtests 7 | 8 | package secretservice 9 | 10 | import ( 11 | "testing" 12 | 13 | dbus "github.com/keybase/dbus" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestKeyringPlain(t *testing.T) { 18 | testKeyring(t, AuthenticationInsecurePlain) 19 | } 20 | 21 | func TestKeyringDH(t *testing.T) { 22 | testKeyring(t, AuthenticationDHAES) 23 | } 24 | 25 | func testKeyring(t *testing.T, mode AuthenticationMode) { 26 | srv, err := NewService() 27 | require.NoError(t, err) 28 | session, err := srv.OpenSession(AuthenticationDHAES) 29 | require.NoError(t, err) 30 | defer srv.CloseSession(session) 31 | 32 | collection := DefaultCollection 33 | 34 | items, err := srv.SearchCollection(collection, map[string]string{"foo": "bar"}) 35 | require.NoError(t, err) 36 | require.Equal(t, len(items), 0) 37 | 38 | secret, err := session.NewSecret([]byte("secret")) 39 | require.NoError(t, err) 40 | 41 | err = srv.Unlock([]dbus.ObjectPath{collection}) 42 | require.NoError(t, err) 43 | 44 | _, err = srv.CreateItem(collection, NewSecretProperties("testlabel", map[string]string{"foo": "bar"}), secret, ReplaceBehaviorReplace) 45 | require.NoError(t, err) 46 | 47 | items, err = srv.SearchCollection(collection, map[string]string{"foo": "bar"}) 48 | require.NoError(t, err) 49 | require.Equal(t, len(items), 1) 50 | gotItem := items[0] 51 | secretPlaintext, err := srv.GetSecret(gotItem, *session) 52 | require.NoError(t, err) 53 | require.Equal(t, secretPlaintext, []byte("secret")) 54 | 55 | err = srv.DeleteItem(gotItem) 56 | require.NoError(t, err) 57 | 58 | err = srv.LockItems([]dbus.ObjectPath{collection}) 59 | require.NoError(t, err) 60 | 61 | } 62 | 63 | func TestGetAll(t *testing.T) { 64 | srv, err := NewService() 65 | require.NoError(t, err) 66 | session, err := srv.OpenSession(AuthenticationDHAES) 67 | require.NoError(t, err) 68 | defer srv.CloseSession(session) 69 | 70 | collection := DefaultCollection 71 | 72 | secret, err := session.NewSecret([]byte("secret")) 73 | require.NoError(t, err) 74 | 75 | err = srv.Unlock([]dbus.ObjectPath{collection}) 76 | require.NoError(t, err) 77 | 78 | item, err := srv.CreateItem(collection, NewSecretProperties("testlabel", map[string]string{"username": "testuser"}), secret, ReplaceBehaviorReplace) 79 | require.NoError(t, err) 80 | 81 | attrs, err := srv.GetAttributes(item) 82 | require.NoError(t, err) 83 | require.Equal(t, attrs["username"], "testuser") 84 | 85 | err = srv.DeleteItem(item) 86 | require.NoError(t, err) 87 | } 88 | -------------------------------------------------------------------------------- /secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7_test.go: -------------------------------------------------------------------------------- 1 | package secretservice 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNewKeypair(t *testing.T) { 10 | group := rfc2409SecondOakleyGroup() 11 | private, public, err := group.NewKeypair() 12 | require.NoError(t, err) 13 | require.NotNil(t, private) 14 | require.NotNil(t, public) 15 | private2, public2, err := group.NewKeypair() 16 | require.NoError(t, err) 17 | require.NotEqual(t, private.Cmp(private2), 0, "should get different private key with every keygen") 18 | require.NotEqual(t, public.Cmp(public2), 0, "should get different public key with every keygen") 19 | } 20 | 21 | func TestKeygen(t *testing.T) { 22 | group := rfc2409SecondOakleyGroup() 23 | myPrivate, myPublic, err := group.NewKeypair() 24 | require.NoError(t, err) 25 | theirPrivate, theirPublic, err := group.NewKeypair() 26 | require.NoError(t, err) 27 | 28 | myKey, err := group.keygenHKDFSHA256AES128(theirPublic, myPrivate) 29 | require.NoError(t, err) 30 | theirKey, err := group.keygenHKDFSHA256AES128(myPublic, theirPrivate) 31 | require.NoError(t, err) 32 | require.Equal(t, myKey, theirKey) 33 | } 34 | 35 | func TestEncryption(t *testing.T) { 36 | key := []byte("YELLOW SUBMARINE") 37 | plaintext := []byte("hello world") 38 | iv, ciphertext, err := unauthenticatedAESCBCEncrypt(plaintext, key) 39 | require.NoError(t, err) 40 | gotPlaintext, err := unauthenticatedAESCBCDecrypt(iv, ciphertext, key) 41 | require.NoError(t, err) 42 | require.Equal(t, plaintext, gotPlaintext) 43 | } 44 | 45 | func TestEncryptionRng(t *testing.T) { 46 | key := []byte("YELLOW SUBMARINE") 47 | plaintext := []byte("hello world") 48 | iv1, ciphertext1, err := unauthenticatedAESCBCEncrypt(plaintext, key) 49 | require.NoError(t, err) 50 | iv2, ciphertext2, err := unauthenticatedAESCBCEncrypt(plaintext, key) 51 | require.NoError(t, err) 52 | require.NotEqual(t, iv1, iv2) 53 | require.NotEqual(t, ciphertext1, ciphertext2) 54 | } 55 | 56 | var pkcs7tests = []struct { 57 | in []byte 58 | out []byte 59 | }{ 60 | {[]byte{}, []byte{4, 4, 4, 4}}, 61 | {[]byte{1, 2}, []byte{1, 2, 2, 2}}, 62 | {[]byte{1, 2, 3}, []byte{1, 2, 3, 1}}, 63 | {[]byte{1, 2, 3, 4}, []byte{1, 2, 3, 4, 4, 4, 4, 4}}, 64 | {[]byte{1, 2, 3, 4, 5}, []byte{1, 2, 3, 4, 5, 3, 3, 3}}, 65 | {[]byte{1, 2, 3, 4, 1, 1, 1}, []byte{1, 2, 3, 4, 1, 1, 1, 1}}, 66 | } 67 | 68 | func TestPKCS7(t *testing.T) { 69 | for _, testCase := range pkcs7tests { 70 | require.Equal(t, padPKCS7(testCase.in, 4), testCase.out) 71 | preimage, err := unpadPKCS7(testCase.out, 4) 72 | require.NoError(t, err) 73 | require.Equal(t, preimage, testCase.in) 74 | } 75 | 76 | _, err := unpadPKCS7([]byte{}, 4) 77 | require.Error(t, err) 78 | _, err = unpadPKCS7([]byte{1, 2, 3, 4}, 4) 79 | require.Error(t, err) 80 | _, err = unpadPKCS7([]byte{1, 2, 3, 3}, 4) 81 | require.Error(t, err) 82 | _, err = unpadPKCS7([]byte{1, 2, 3, 4, 1, 1, 1, 2}, 4) 83 | require.Error(t, err) 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Keychain 2 | 3 | [![Build Status](https://github.com/keybase/go-keychain/actions/workflows/ci.yml/badge.svg)](https://github.com/keybase/go-keychain/actions) 4 | 5 | A library for accessing the Keychain for macOS, iOS, and Linux in Go (golang). 6 | 7 | Requires macOS 10.9 or greater and iOS 8 or greater. On Linux, communicates to 8 | a provider of the DBUS SecretService spec like gnome-keyring or ksecretservice. 9 | 10 | ```go 11 | import "github.com/keybase/go-keychain" 12 | ``` 13 | 14 | ## Mac/iOS Usage 15 | 16 | The API is meant to mirror the macOS/iOS Keychain API and is not necessarily idiomatic go. 17 | 18 | #### Add Item 19 | 20 | ```go 21 | item := keychain.NewItem() 22 | item.SetSecClass(keychain.SecClassGenericPassword) 23 | item.SetService("MyService") 24 | item.SetAccount("gabriel") 25 | item.SetLabel("A label") 26 | item.SetAccessGroup("A123456789.group.com.mycorp") 27 | item.SetData([]byte("toomanysecrets")) 28 | item.SetSynchronizable(keychain.SynchronizableNo) 29 | item.SetAccessible(keychain.AccessibleWhenUnlocked) 30 | err := keychain.AddItem(item) 31 | 32 | if err == keychain.ErrorDuplicateItem { 33 | // Duplicate 34 | } 35 | ``` 36 | 37 | #### Query Item 38 | 39 | Query for multiple results, returning attributes: 40 | 41 | ```go 42 | query := keychain.NewItem() 43 | query.SetSecClass(keychain.SecClassGenericPassword) 44 | query.SetService(service) 45 | query.SetAccount(account) 46 | query.SetAccessGroup(accessGroup) 47 | query.SetMatchLimit(keychain.MatchLimitAll) 48 | query.SetReturnAttributes(true) 49 | results, err := keychain.QueryItem(query) 50 | if err != nil { 51 | // Error 52 | } else { 53 | for _, r := range results { 54 | fmt.Printf("%#v\n", r) 55 | } 56 | } 57 | ``` 58 | 59 | Query for a single result, returning data: 60 | 61 | ```go 62 | query := keychain.NewItem() 63 | query.SetSecClass(keychain.SecClassGenericPassword) 64 | query.SetService(service) 65 | query.SetAccount(account) 66 | query.SetAccessGroup(accessGroup) 67 | query.SetMatchLimit(keychain.MatchLimitOne) 68 | query.SetReturnData(true) 69 | results, err := keychain.QueryItem(query) 70 | if err != nil { 71 | // Error 72 | } else if len(results) != 1 { 73 | // Not found 74 | } else { 75 | password := string(results[0].Data) 76 | } 77 | ``` 78 | 79 | #### Delete Item 80 | 81 | Delete a generic password item with service and account: 82 | 83 | ```go 84 | item := keychain.NewItem() 85 | item.SetSecClass(keychain.SecClassGenericPassword) 86 | item.SetService(service) 87 | item.SetAccount(account) 88 | err := keychain.DeleteItem(item) 89 | ``` 90 | 91 | ### Other 92 | 93 | There are some convenience methods for generic password: 94 | 95 | ```go 96 | // Create generic password item with service, account, label, password, access group 97 | item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp") 98 | item.SetSynchronizable(keychain.SynchronizableNo) 99 | item.SetAccessible(keychain.AccessibleWhenUnlocked) 100 | err := keychain.AddItem(item) 101 | if err == keychain.ErrorDuplicateItem { 102 | // Duplicate 103 | } 104 | 105 | password, err := keychain.GetGenericPassword("MyService", "gabriel", "A label", "A123456789.group.com.mycorp") 106 | 107 | accounts, err := keychain.GetGenericPasswordAccounts("MyService") 108 | // Should have 1 account == "gabriel" 109 | 110 | err := keychain.DeleteGenericPasswordItem("MyService", "gabriel") 111 | if err == keychain.ErrorItemNotFound { 112 | // Not found 113 | } 114 | ``` 115 | 116 | ## iOS 117 | 118 | Bindable package in `bind`. iOS project in `ios`. Run that project to test iOS. 119 | 120 | To re-generate framework: 121 | 122 | ``` 123 | (cd bind && gomobile bind -target=ios -tags=ios -o ../ios/bind.framework) 124 | ``` 125 | -------------------------------------------------------------------------------- /macos_test.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !ios 2 | // +build darwin,!ios 3 | 4 | package keychain 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestUpdateItem(t *testing.T) { 11 | var err error 12 | 13 | item := NewGenericPassword("TestAccess", "firsttest", "TestUpdateItem", []byte("toomanysecrets2"), "") 14 | defer func() { _ = DeleteItem(item) }() 15 | err = AddItem(item) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | data1, err := GetGenericPassword("TestAccess", "firsttest", "TestUpdateItem", "") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | if string(data1) != "toomanysecrets2" { 25 | t.Fatal("TestUpdateItem: new password does not match") 26 | } 27 | 28 | updateItem := NewItem() 29 | updateItem.SetSecClass(SecClassGenericPassword) 30 | updateItem.SetService("TestAccess") 31 | updateItem.SetAccount("firsttest") 32 | updateItem.SetLabel("TestUpdateItem") 33 | updateItem.SetData([]byte("toomanysecrets3")) 34 | err = UpdateItem(item, updateItem) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | data2, err := GetGenericPassword("TestAccess", "firsttest", "TestUpdateItem", "") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | if string(data2) != "toomanysecrets3" { 44 | t.Fatal("TestUpdateItem: updated password does not match") 45 | } 46 | } 47 | 48 | func TestGenericPassword(t *testing.T) { 49 | service, account, label, accessGroup, password := "TestGenericPasswordRef", "test", "", "", "toomanysecrets" 50 | 51 | item := NewGenericPassword(service, account, label, []byte(password), accessGroup) 52 | defer func() { _ = DeleteItem(item) }() 53 | err := AddItem(item) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | err = DeleteItem(item) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | passwordAfter, err := GetGenericPassword(service, account, label, accessGroup) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | if passwordAfter != nil { 68 | t.Fatal("Shouldn't have password") 69 | } 70 | } 71 | 72 | func TestInternetPassword(t *testing.T) { 73 | item := NewItem() 74 | item.SetSecClass(SecClassInternetPassword) 75 | 76 | // Internet password-specific attributes 77 | item.SetProtocol("htps") 78 | item.SetServer("8xs8h5x5dfc0AI5EzT81l.com") 79 | item.SetPort(1234) 80 | item.SetPath("/this/is/the/path") 81 | 82 | item.SetAccount("this-is-the-username") 83 | item.SetLabel("this is the label") 84 | item.SetData([]byte("this is the password")) 85 | item.SetComment("this is the comment") 86 | defer func() { _ = DeleteItem(item) }() 87 | err := AddItem(item) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | query := NewItem() 93 | query.SetSecClass(SecClassInternetPassword) 94 | query.SetServer("8xs8h5x5dfc0AI5EzT81l.com") 95 | query.SetMatchLimit(MatchLimitOne) 96 | query.SetReturnAttributes(true) 97 | results, err := QueryItem(query) 98 | if err != nil { 99 | t.Fatalf("Query Error: %v", err) 100 | } 101 | 102 | if len(results) != 1 { 103 | t.Fatalf("expected 1 result, got %d", len(results)) 104 | } 105 | 106 | r := results[0] 107 | if r.Protocol != "htps" { 108 | t.Errorf("expected protocol 'htps' but got %q", r.Protocol) 109 | } 110 | if r.Server != "8xs8h5x5dfc0AI5EzT81l.com" { 111 | t.Errorf("expected server '8xs8h5x5dfc0AI5EzT81l.com' but got %q", r.Server) 112 | } 113 | if r.Port != 1234 { 114 | t.Errorf("expected port '1234' but got %d", r.Port) 115 | } 116 | if r.Path != "/this/is/the/path" { 117 | t.Errorf("expected path '/this/is/the/path' but got %q", r.Path) 118 | } 119 | 120 | if r.Account != "this-is-the-username" { 121 | t.Errorf("expected account 'this-is-the-username' but got %q", r.Account) 122 | } 123 | if r.Label != "this is the label" { 124 | t.Errorf("expected label 'this is the label' but got %q", r.Label) 125 | } 126 | if r.Comment != "this is the comment" { 127 | t.Errorf("expected comment 'this is the comment' but got %q", r.Comment) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7.go: -------------------------------------------------------------------------------- 1 | // This file implements a basic Diffie-Hellman for groups with modular 2 | // exponentiation operators. In particular, it is used in this package 3 | // to implement the Diffie-Hellman KEX over the Second Oakley Group. 4 | // meant only for use for securing the channel to the D-Bus Secret Service. 5 | // Much of the code in this file is derived from package 6 | // golang.org/x/crypto/ssh:kex.go, and is replicated here because the relevant 7 | // variables and methods are not exported or easily accessible. 8 | // Note that this protocol is NOT authenticated, NOT secure against malleation 9 | // and is NOT CCA2-secure. It is only meant to hide the D-Bus messages from any 10 | // system services that may be logging everything. 11 | 12 | package secretservice 13 | 14 | import ( 15 | "bytes" 16 | "crypto/aes" 17 | "crypto/cipher" 18 | cryptorand "crypto/rand" 19 | "crypto/sha256" 20 | "fmt" 21 | "io" 22 | "math/big" 23 | 24 | errors "github.com/pkg/errors" 25 | "golang.org/x/crypto/hkdf" 26 | ) 27 | 28 | type dhGroup struct { 29 | g, p, pMinus1 *big.Int 30 | } 31 | 32 | var bigOne = big.NewInt(1) 33 | 34 | func (group *dhGroup) NewKeypair() (private *big.Int, public *big.Int, err error) { 35 | for { 36 | if private, err = cryptorand.Int(cryptorand.Reader, group.pMinus1); err != nil { 37 | return nil, nil, err 38 | } 39 | if private.Sign() > 0 { 40 | break 41 | } 42 | } 43 | public = new(big.Int).Exp(group.g, private, group.p) 44 | return private, public, nil 45 | } 46 | 47 | func (group *dhGroup) diffieHellman(theirPublic, myPrivate *big.Int) (*big.Int, error) { 48 | if theirPublic.Cmp(bigOne) <= 0 || theirPublic.Cmp(group.pMinus1) >= 0 { 49 | return nil, errors.New("ssh: DH parameter out of bounds") 50 | } 51 | return new(big.Int).Exp(theirPublic, myPrivate, group.p), nil 52 | } 53 | 54 | func rfc2409SecondOakleyGroup() *dhGroup { 55 | p, _ := new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF", 16) 56 | return &dhGroup{ 57 | g: new(big.Int).SetInt64(2), 58 | p: p, 59 | pMinus1: new(big.Int).Sub(p, bigOne), 60 | } 61 | } 62 | 63 | func (group *dhGroup) keygenHKDFSHA256AES128(theirPublic *big.Int, myPrivate *big.Int) ([]byte, error) { 64 | sharedSecret, err := group.diffieHellman(theirPublic, myPrivate) 65 | if err != nil { 66 | return nil, err 67 | } 68 | sharedSecretBytes := sharedSecret.Bytes() 69 | 70 | r := hkdf.New(sha256.New, sharedSecretBytes, nil, nil) 71 | 72 | aesKey := make([]byte, 16) 73 | _, err = io.ReadFull(r, aesKey) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return aesKey, nil 79 | } 80 | 81 | func unauthenticatedAESCBCEncrypt(unpaddedPlaintext []byte, key []byte) (iv []byte, ciphertext []byte, err error) { 82 | paddedPlaintext := padPKCS7(unpaddedPlaintext, aes.BlockSize) 83 | block, err := aes.NewCipher(key) 84 | if err != nil { 85 | return nil, nil, err 86 | } 87 | ivSize := aes.BlockSize 88 | iv = make([]byte, ivSize) 89 | ciphertext = make([]byte, len(paddedPlaintext)) 90 | if _, err := io.ReadFull(cryptorand.Reader, iv); err != nil { 91 | return nil, nil, err 92 | } 93 | mode := cipher.NewCBCEncrypter(block, iv) 94 | mode.CryptBlocks(ciphertext, paddedPlaintext) 95 | return iv, ciphertext, nil 96 | } 97 | 98 | func unauthenticatedAESCBCDecrypt(iv []byte, ciphertext []byte, key []byte) ([]byte, error) { 99 | block, err := aes.NewCipher(key) 100 | if err != nil { 101 | return nil, err 102 | } 103 | if len(iv) != aes.BlockSize { 104 | return nil, fmt.Errorf("iv length not aes blocksize") 105 | } 106 | if len(ciphertext) < aes.BlockSize { 107 | return nil, fmt.Errorf("ciphertext smaller than AES block size") 108 | } 109 | if len(ciphertext)%aes.BlockSize != 0 { 110 | return nil, fmt.Errorf("aes ciphertext not a multiple of blocksize") 111 | } 112 | mode := cipher.NewCBCDecrypter(block, iv) 113 | mode.CryptBlocks(ciphertext, ciphertext) // decrypt in-place 114 | plaintext, err := unpadPKCS7(ciphertext, aes.BlockSize) 115 | if err != nil { 116 | return nil, err 117 | } 118 | return plaintext, nil 119 | } 120 | 121 | func padPKCS7(xs []byte, n int) []byte { 122 | m := byte(n - (len(xs) % n)) 123 | if m == 0 { 124 | m = 16 125 | } 126 | return append(xs, bytes.Repeat([]byte{m}, int(m))...) 127 | } 128 | 129 | func unpadPKCS7(xs []byte, n int) ([]byte, error) { 130 | if len(xs) == 0 { 131 | return nil, fmt.Errorf("cannot unpad empty bytearray") 132 | } 133 | if len(xs)%n != 0 { 134 | return nil, fmt.Errorf("length of bytearray not a multiple of blocksize") 135 | } 136 | lastByte := xs[len(xs)-1] 137 | padStartIdx := len(xs) - int(lastByte) 138 | if padStartIdx < 0 { 139 | return nil, fmt.Errorf("invalid pkcs7 padding; pad byte larger than number of characters") 140 | } 141 | for i := padStartIdx; i < len(xs); i++ { 142 | if xs[i] != lastByte { 143 | return nil, fmt.Errorf("expected pad character %x, got %x", lastByte, xs[i]) 144 | } 145 | } 146 | return xs[:padStartIdx], nil 147 | } 148 | -------------------------------------------------------------------------------- /bind/bind.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || ios 2 | // +build darwin ios 3 | 4 | package bind 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | 10 | "github.com/keybase/go-keychain" 11 | ) 12 | 13 | // Test is a bind interface for the test 14 | type Test interface { 15 | Fail(s string) 16 | } 17 | 18 | // AddGenericPassword adds generic password 19 | func AddGenericPassword(service string, account string, label string, password string, accessGroup string) error { 20 | item := keychain.NewGenericPassword(service, account, label, []byte(password), accessGroup) 21 | return keychain.AddItem(item) 22 | } 23 | 24 | // DeleteGenericPassword deletes generic password 25 | func DeleteGenericPassword(service string, account string, accessGroup string) error { 26 | item := keychain.NewItem() 27 | item.SetSecClass(keychain.SecClassGenericPassword) 28 | item.SetService(service) 29 | item.SetAccount(account) 30 | item.SetAccessGroup(accessGroup) 31 | return keychain.DeleteItem(item) 32 | } 33 | 34 | // GenericPasswordTest runs test code for generic password keychain item. 35 | // This is here so we can export using gomobile bind and run this method on iOS simulator and device. 36 | // Access groups aren't supported in iOS simulator. 37 | func GenericPasswordTest(t Test, service string, accessGroup string) { 38 | var err error 39 | 40 | account := "Testing account with unicode テスト" 41 | item := keychain.NewGenericPassword(service, account, "", []byte("toomanysecrets"), accessGroup) 42 | item.SetSynchronizable(keychain.SynchronizableNo) 43 | item.SetAccessible(keychain.AccessibleWhenUnlocked) 44 | 45 | account2 := "Testing account #2" 46 | item2 := keychain.NewGenericPassword(service, account2, "", []byte("toomanysecrets2"), accessGroup) 47 | 48 | // Cleanup 49 | defer func() { _ = keychain.DeleteItem(item) }() 50 | defer func() { _ = keychain.DeleteItem(item2) }() 51 | 52 | // Test account names empty 53 | accounts, err := keychain.GetGenericPasswordAccounts(service) 54 | if err != nil { 55 | t.Fail(err.Error()) 56 | } 57 | if len(accounts) != 0 { 58 | t.Fail("Should have no accounts") 59 | } 60 | 61 | // Test add 62 | err = keychain.AddItem(item) 63 | if err != nil { 64 | t.Fail(err.Error()) 65 | } 66 | 67 | // Test dupe 68 | err = keychain.AddItem(item) 69 | if err != keychain.ErrorDuplicateItem { 70 | t.Fail("Should error with duplicate item") 71 | } 72 | 73 | // Test add another 74 | err = keychain.AddItem(item2) 75 | if err != nil { 76 | t.Fail(err.Error()) 77 | } 78 | 79 | // Test querying attributes 80 | query := keychain.NewItem() 81 | query.SetSecClass(keychain.SecClassGenericPassword) 82 | query.SetService(service) 83 | query.SetAccount(account) 84 | query.SetAccessGroup(accessGroup) 85 | query.SetMatchLimit(keychain.MatchLimitAll) 86 | query.SetReturnAttributes(true) 87 | results, err := keychain.QueryItem(query) 88 | if err != nil { 89 | t.Fail(err.Error()) 90 | } 91 | 92 | if len(results) != 1 { 93 | t.Fail(fmt.Sprintf("Invalid results count: %d", len(results))) 94 | } 95 | 96 | if results[0].Service != service { 97 | t.Fail(fmt.Sprintf("Invalid service, %v != %v, %v", results[0].Service, service, results)) 98 | } 99 | 100 | if results[0].Account != account { 101 | t.Fail(fmt.Sprintf("Invalid account, %v != %v, %v", results[0].Account, account, results)) 102 | } 103 | 104 | if len(results[0].Data) != 0 { 105 | t.Fail("Password shouldn't come back when returning attributes") 106 | } 107 | 108 | // Test querying data 109 | queryData := keychain.NewItem() 110 | queryData.SetSecClass(keychain.SecClassGenericPassword) 111 | queryData.SetService(service) 112 | queryData.SetAccount(account) 113 | queryData.SetAccessGroup(accessGroup) 114 | queryData.SetMatchLimit(keychain.MatchLimitOne) 115 | queryData.SetReturnData(true) 116 | resultsData, err := keychain.QueryItem(queryData) 117 | if err != nil { 118 | t.Fail(err.Error()) 119 | } 120 | 121 | if len(resultsData) != 1 { 122 | t.Fail("Too many results") 123 | } 124 | 125 | if string(resultsData[0].Data) != "toomanysecrets" { 126 | t.Fail("Invalid password") 127 | } 128 | 129 | // Test account names 130 | accounts2, err := keychain.GetGenericPasswordAccounts(service) 131 | if err != nil { 132 | t.Fail(err.Error()) 133 | } 134 | if len(accounts2) != 2 { 135 | t.Fail(fmt.Sprintf("Should have 2 accounts: %v", accounts2)) 136 | } 137 | 138 | if !reflect.DeepEqual(accounts2, []string{account, account2}) { 139 | t.Fail(fmt.Sprintf("Invalid accounts: %v", accounts2)) 140 | } 141 | 142 | // Remove 143 | queryDel := keychain.NewItem() 144 | queryDel.SetSecClass(keychain.SecClassGenericPassword) 145 | queryDel.SetService(service) 146 | queryDel.SetAccount(account) 147 | queryDel.SetAccessGroup(accessGroup) 148 | err = keychain.DeleteItem(queryDel) 149 | if err != nil { 150 | t.Fail(err.Error()) 151 | } 152 | 153 | // Test removed 154 | query3 := keychain.NewItem() 155 | query3.SetSecClass(keychain.SecClassGenericPassword) 156 | query3.SetService(service) 157 | query3.SetAccount(account) 158 | query3.SetAccessGroup(accessGroup) 159 | query3.SetMatchLimit(keychain.MatchLimitAll) 160 | query3.SetReturnAttributes(true) 161 | results3, err := keychain.QueryItem(query3) 162 | if err != nil { 163 | t.Fail(err.Error()) 164 | } 165 | 166 | if len(results3) != 0 { 167 | t.Fail("Results should have been empty") 168 | } 169 | 170 | accounts3, err := keychain.GetGenericPasswordAccounts(service) 171 | if err != nil { 172 | t.Fail(err.Error()) 173 | } 174 | if len(accounts3) != 1 { 175 | t.Fail("Should have an account") 176 | } 177 | 178 | // Test remove not found 179 | err = keychain.DeleteItem(item) 180 | if err != keychain.ErrorItemNotFound { 181 | t.Fail("Error should be not found") 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /corefoundation.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || ios 2 | // +build darwin ios 3 | 4 | package keychain 5 | 6 | /* 7 | #cgo LDFLAGS: -framework CoreFoundation 8 | 9 | #include 10 | 11 | // Can't cast a *uintptr to *unsafe.Pointer in Go, and casting 12 | // C.CFTypeRef to unsafe.Pointer is unsafe in Go, so have shim functions to 13 | // do the casting in C (where it's safe). 14 | 15 | // We add a suffix to the C functions below, because we copied this 16 | // file from go-kext, which means that any project that depends on this 17 | // package and go-kext would run into duplicate symbol errors otherwise. 18 | // 19 | // TODO: Move this file into its own package depended on by go-kext 20 | // and this package. 21 | 22 | CFDictionaryRef CFDictionaryCreateSafe2(CFAllocatorRef allocator, const uintptr_t *keys, const uintptr_t *values, CFIndex numValues, const CFDictionaryKeyCallBacks *keyCallBacks, const CFDictionaryValueCallBacks *valueCallBacks) { 23 | return CFDictionaryCreate(allocator, (const void **)keys, (const void **)values, numValues, keyCallBacks, valueCallBacks); 24 | } 25 | 26 | CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values, CFIndex numValues, const CFArrayCallBacks *callBacks) { 27 | return CFArrayCreate(allocator, (const void **)values, numValues, callBacks); 28 | } 29 | */ 30 | import "C" 31 | import ( 32 | "errors" 33 | "fmt" 34 | "math" 35 | "reflect" 36 | "unicode/utf8" 37 | "unsafe" 38 | ) 39 | 40 | // Release releases memory pointed to by a CFTypeRef. 41 | func Release(ref C.CFTypeRef) { 42 | C.CFRelease(ref) 43 | } 44 | 45 | // BytesToCFData will return a CFDataRef and if non-nil, must be released with 46 | // Release(ref). 47 | func BytesToCFData(b []byte) (C.CFDataRef, error) { 48 | if uint64(len(b)) > math.MaxUint32 { 49 | return 0, errors.New("Data is too large") 50 | } 51 | var p *C.UInt8 52 | if len(b) > 0 { 53 | p = (*C.UInt8)(&b[0]) 54 | } 55 | cfData := C.CFDataCreate(C.kCFAllocatorDefault, p, C.CFIndex(len(b))) 56 | if cfData == 0 { 57 | return 0, fmt.Errorf("CFDataCreate failed") 58 | } 59 | return cfData, nil 60 | } 61 | 62 | // CFDataToBytes converts CFData to bytes. 63 | func CFDataToBytes(cfData C.CFDataRef) ([]byte, error) { 64 | return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))), nil 65 | } 66 | 67 | // MapToCFDictionary will return a CFDictionaryRef and if non-nil, must be 68 | // released with Release(ref). 69 | func MapToCFDictionary(m map[C.CFTypeRef]C.CFTypeRef) (C.CFDictionaryRef, error) { 70 | var keys, values []C.uintptr_t 71 | for key, value := range m { 72 | keys = append(keys, C.uintptr_t(key)) 73 | values = append(values, C.uintptr_t(value)) 74 | } 75 | numValues := len(values) 76 | var keysPointer, valuesPointer *C.uintptr_t 77 | if numValues > 0 { 78 | keysPointer = &keys[0] 79 | valuesPointer = &values[0] 80 | } 81 | cfDict := C.CFDictionaryCreateSafe2(C.kCFAllocatorDefault, keysPointer, valuesPointer, C.CFIndex(numValues), 82 | &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) //nolint 83 | if cfDict == 0 { 84 | return 0, fmt.Errorf("CFDictionaryCreate failed") 85 | } 86 | return cfDict, nil 87 | } 88 | 89 | // CFDictionaryToMap converts CFDictionaryRef to a map. 90 | func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) { 91 | count := C.CFDictionaryGetCount(cfDict) 92 | if count > 0 { 93 | keys := make([]C.CFTypeRef, count) 94 | values := make([]C.CFTypeRef, count) 95 | C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(unsafe.Pointer(&keys[0])), (*unsafe.Pointer)(unsafe.Pointer(&values[0]))) 96 | m = make(map[C.CFTypeRef]C.CFTypeRef, count) 97 | for i := C.CFIndex(0); i < count; i++ { 98 | m[keys[i]] = values[i] 99 | } 100 | } 101 | return 102 | } 103 | 104 | // Int32ToCFNumber will return a CFNumberRef, must be released with Release(ref). 105 | func Int32ToCFNumber(u int32) C.CFNumberRef { 106 | sint := C.SInt32(u) 107 | p := unsafe.Pointer(&sint) 108 | return C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberSInt32Type, p) 109 | } 110 | 111 | // StringToCFString will return a CFStringRef and if non-nil, must be released with 112 | // Release(ref). 113 | func StringToCFString(s string) (C.CFStringRef, error) { 114 | if !utf8.ValidString(s) { 115 | return 0, errors.New("Invalid UTF-8 string") 116 | } 117 | if uint64(len(s)) > math.MaxUint32 { 118 | return 0, errors.New("String is too large") 119 | } 120 | 121 | bytes := []byte(s) 122 | var p *C.UInt8 123 | if len(bytes) > 0 { 124 | p = (*C.UInt8)(&bytes[0]) 125 | } 126 | return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, p, C.CFIndex(len(s)), C.kCFStringEncodingUTF8, C.false), nil 127 | } 128 | 129 | // CFStringToString converts a CFStringRef to a string. 130 | func CFStringToString(s C.CFStringRef) string { 131 | p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8) 132 | if p != nil { 133 | return C.GoString(p) 134 | } 135 | length := C.CFStringGetLength(s) 136 | if length == 0 { 137 | return "" 138 | } 139 | maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8) 140 | if maxBufLen == 0 { 141 | return "" 142 | } 143 | buf := make([]byte, maxBufLen) 144 | var usedBufLen C.CFIndex 145 | _ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen) 146 | return string(buf[:usedBufLen]) 147 | } 148 | 149 | // ArrayToCFArray will return a CFArrayRef and if non-nil, must be released with 150 | // Release(ref). 151 | func ArrayToCFArray(a []C.CFTypeRef) C.CFArrayRef { 152 | var values []C.uintptr_t 153 | for _, value := range a { 154 | values = append(values, C.uintptr_t(value)) 155 | } 156 | numValues := len(values) 157 | var valuesPointer *C.uintptr_t 158 | if numValues > 0 { 159 | valuesPointer = &values[0] 160 | } 161 | return C.CFArrayCreateSafe2(C.kCFAllocatorDefault, valuesPointer, C.CFIndex(numValues), &C.kCFTypeArrayCallBacks) //nolint 162 | } 163 | 164 | // CFArrayToArray converts a CFArrayRef to an array of CFTypes. 165 | func CFArrayToArray(cfArray C.CFArrayRef) (a []C.CFTypeRef) { 166 | count := C.CFArrayGetCount(cfArray) 167 | if count > 0 { 168 | a = make([]C.CFTypeRef, count) 169 | C.CFArrayGetValues(cfArray, C.CFRange{0, count}, (*unsafe.Pointer)(unsafe.Pointer(&a[0]))) 170 | } 171 | return 172 | } 173 | 174 | // Convertable knows how to convert an instance to a CFTypeRef. 175 | type Convertable interface { 176 | Convert() (C.CFTypeRef, error) 177 | } 178 | 179 | // ConvertMapToCFDictionary converts a map to a CFDictionary and if non-nil, 180 | // must be released with Release(ref). 181 | func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, error) { 182 | m := make(map[C.CFTypeRef]C.CFTypeRef) 183 | for key, i := range attr { 184 | var valueRef C.CFTypeRef 185 | switch val := i.(type) { 186 | default: 187 | return 0, fmt.Errorf("Unsupported value type: %v", reflect.TypeOf(i)) 188 | case C.CFTypeRef: 189 | valueRef = val 190 | case bool: 191 | if val { 192 | valueRef = C.CFTypeRef(C.kCFBooleanTrue) 193 | } else { 194 | valueRef = C.CFTypeRef(C.kCFBooleanFalse) 195 | } 196 | case int32: 197 | valueRef = C.CFTypeRef(Int32ToCFNumber(val)) 198 | defer Release(valueRef) 199 | case []byte: 200 | bytesRef, err := BytesToCFData(val) 201 | if err != nil { 202 | return 0, err 203 | } 204 | valueRef = C.CFTypeRef(bytesRef) 205 | defer Release(valueRef) 206 | case string: 207 | stringRef, err := StringToCFString(val) 208 | if err != nil { 209 | return 0, err 210 | } 211 | valueRef = C.CFTypeRef(stringRef) 212 | defer Release(valueRef) 213 | case Convertable: 214 | convertedRef, err := val.Convert() 215 | if err != nil { 216 | return 0, err 217 | } 218 | valueRef = convertedRef 219 | defer Release(valueRef) 220 | } 221 | keyRef, err := StringToCFString(key) 222 | if err != nil { 223 | return 0, err 224 | } 225 | m[C.CFTypeRef(keyRef)] = valueRef 226 | defer Release(C.CFTypeRef(keyRef)) 227 | } 228 | 229 | cfDict, err := MapToCFDictionary(m) 230 | if err != nil { 231 | return 0, err 232 | } 233 | return cfDict, nil 234 | } 235 | 236 | // CFTypeDescription returns type string for CFTypeRef. 237 | func CFTypeDescription(ref C.CFTypeRef) string { 238 | typeID := C.CFGetTypeID(ref) 239 | typeDesc := C.CFCopyTypeIDDescription(typeID) 240 | defer Release(C.CFTypeRef(typeDesc)) 241 | return CFStringToString(typeDesc) 242 | } 243 | 244 | // Convert converts a CFTypeRef to a go instance. 245 | func Convert(ref C.CFTypeRef) (interface{}, error) { 246 | typeID := C.CFGetTypeID(ref) 247 | if typeID == C.CFStringGetTypeID() { 248 | return CFStringToString(C.CFStringRef(ref)), nil 249 | } else if typeID == C.CFDictionaryGetTypeID() { 250 | return ConvertCFDictionary(C.CFDictionaryRef(ref)) 251 | } else if typeID == C.CFArrayGetTypeID() { 252 | arr := CFArrayToArray(C.CFArrayRef(ref)) 253 | results := make([]interface{}, 0, len(arr)) 254 | for _, ref := range arr { 255 | v, err := Convert(ref) 256 | if err != nil { 257 | return nil, err 258 | } 259 | results = append(results, v) 260 | } 261 | return results, nil 262 | } else if typeID == C.CFDataGetTypeID() { 263 | b, err := CFDataToBytes(C.CFDataRef(ref)) 264 | if err != nil { 265 | return nil, err 266 | } 267 | return b, nil 268 | } else if typeID == C.CFNumberGetTypeID() { 269 | return CFNumberToInterface(C.CFNumberRef(ref)), nil 270 | } else if typeID == C.CFBooleanGetTypeID() { 271 | if C.CFBooleanGetValue(C.CFBooleanRef(ref)) != 0 { 272 | return true, nil 273 | } 274 | return false, nil 275 | } 276 | 277 | return nil, fmt.Errorf("Invalid type: %s", CFTypeDescription(ref)) 278 | } 279 | 280 | // ConvertCFDictionary converts a CFDictionary to map (deep). 281 | func ConvertCFDictionary(d C.CFDictionaryRef) (map[interface{}]interface{}, error) { 282 | m := CFDictionaryToMap(d) 283 | result := make(map[interface{}]interface{}) 284 | 285 | for k, v := range m { 286 | gk, err := Convert(k) 287 | if err != nil { 288 | return nil, err 289 | } 290 | gv, err := Convert(v) 291 | if err != nil { 292 | return nil, err 293 | } 294 | result[gk] = gv 295 | } 296 | return result, nil 297 | } 298 | 299 | // CFNumberToInterface converts the CFNumberRef to the most appropriate numeric 300 | // type. 301 | // This code is from github.com/kballard/go-osx-plist. 302 | func CFNumberToInterface(cfNumber C.CFNumberRef) interface{} { 303 | typ := C.CFNumberGetType(cfNumber) 304 | switch typ { 305 | case C.kCFNumberSInt8Type: 306 | var sint C.SInt8 307 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint 308 | return int8(sint) 309 | case C.kCFNumberSInt16Type: 310 | var sint C.SInt16 311 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint 312 | return int16(sint) 313 | case C.kCFNumberSInt32Type: 314 | var sint C.SInt32 315 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint 316 | return int32(sint) 317 | case C.kCFNumberSInt64Type: 318 | var sint C.SInt64 319 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint 320 | return int64(sint) 321 | case C.kCFNumberFloat32Type: 322 | var float C.Float32 323 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint 324 | return float32(float) 325 | case C.kCFNumberFloat64Type: 326 | var float C.Float64 327 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint 328 | return float64(float) 329 | case C.kCFNumberCharType: 330 | var char C.char 331 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&char)) //nolint 332 | return byte(char) 333 | case C.kCFNumberShortType: 334 | var short C.short 335 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&short)) //nolint 336 | return int16(short) 337 | case C.kCFNumberIntType: 338 | var i C.int 339 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&i)) //nolint 340 | return int32(i) 341 | case C.kCFNumberLongType: 342 | var long C.long 343 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&long)) //nolint 344 | return int(long) 345 | case C.kCFNumberLongLongType: 346 | // This is the only type that may actually overflow us 347 | var longlong C.longlong 348 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&longlong)) //nolint 349 | return int64(longlong) 350 | case C.kCFNumberFloatType: 351 | var float C.float 352 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint 353 | return float32(float) 354 | case C.kCFNumberDoubleType: 355 | var double C.double 356 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&double)) //nolint 357 | return float64(double) 358 | case C.kCFNumberCFIndexType: 359 | // CFIndex is a long 360 | var index C.CFIndex 361 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&index)) //nolint 362 | return int(index) 363 | case C.kCFNumberNSIntegerType: 364 | // We don't have a definition of NSInteger, but we know it's either an int or a long 365 | var nsInt C.long 366 | C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&nsInt)) //nolint 367 | return int(nsInt) 368 | } 369 | panic("Unknown CFNumber type") 370 | } 371 | -------------------------------------------------------------------------------- /secretservice/secretservice.go: -------------------------------------------------------------------------------- 1 | package secretservice 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | dbus "github.com/keybase/dbus" 8 | errors "github.com/pkg/errors" 9 | ) 10 | 11 | // SecretServiceInterface 12 | const SecretServiceInterface = "org.freedesktop.secrets" 13 | 14 | // SecretServiceObjectPath 15 | const SecretServiceObjectPath dbus.ObjectPath = "/org/freedesktop/secrets" 16 | 17 | // DefaultCollection need not necessarily exist in the user's keyring. 18 | const DefaultCollection dbus.ObjectPath = "/org/freedesktop/secrets/aliases/default" 19 | 20 | // AuthenticationMode 21 | type AuthenticationMode string 22 | 23 | // AuthenticationInsecurePlain 24 | const AuthenticationInsecurePlain AuthenticationMode = "plain" 25 | 26 | // AuthenticationDHAES 27 | const AuthenticationDHAES AuthenticationMode = "dh-ietf1024-sha256-aes128-cbc-pkcs7" 28 | 29 | // NilFlags 30 | const NilFlags = 0 31 | 32 | // Attributes 33 | type Attributes map[string]string 34 | 35 | // Secret 36 | type Secret struct { 37 | Session dbus.ObjectPath 38 | Parameters []byte 39 | Value []byte 40 | ContentType string 41 | } 42 | 43 | // PromptCompletedResult 44 | type PromptCompletedResult struct { 45 | Dismissed bool 46 | Paths dbus.Variant 47 | } 48 | 49 | // SecretService 50 | type SecretService struct { 51 | conn *dbus.Conn 52 | signalCh <-chan *dbus.Signal 53 | sessionOpenTimeout time.Duration 54 | } 55 | 56 | // Session 57 | type Session struct { 58 | Mode AuthenticationMode 59 | Path dbus.ObjectPath 60 | Public *big.Int 61 | Private *big.Int 62 | AESKey []byte 63 | } 64 | 65 | // DefaultSessionOpenTimeout 66 | const DefaultSessionOpenTimeout = 10 * time.Second 67 | 68 | // NewService 69 | func NewService() (*SecretService, error) { 70 | conn, err := dbus.ConnectSessionBus() 71 | if err != nil { 72 | return nil, errors.Wrap(err, "failed to open dbus connection") 73 | } 74 | signalCh := make(chan *dbus.Signal, 16) 75 | conn.Signal(signalCh) 76 | _ = conn.AddMatchSignal(dbus.WithMatchOption("org.freedesktop.Secret.Prompt", "Completed")) 77 | return &SecretService{conn: conn, signalCh: signalCh, sessionOpenTimeout: DefaultSessionOpenTimeout}, nil 78 | } 79 | 80 | // SetSessionOpenTimeout 81 | func (s *SecretService) SetSessionOpenTimeout(d time.Duration) { 82 | s.sessionOpenTimeout = d 83 | } 84 | 85 | // ServiceObj 86 | func (s *SecretService) ServiceObj() dbus.BusObject { 87 | return s.conn.Object(SecretServiceInterface, SecretServiceObjectPath) 88 | } 89 | 90 | // Obj 91 | func (s *SecretService) Obj(path dbus.ObjectPath) dbus.BusObject { 92 | return s.conn.Object(SecretServiceInterface, path) 93 | } 94 | 95 | type sessionOpenResponse struct { 96 | algorithmOutput dbus.Variant 97 | path dbus.ObjectPath 98 | } 99 | 100 | func (s *SecretService) openSessionRaw(mode AuthenticationMode, sessionAlgorithmInput dbus.Variant) (resp sessionOpenResponse, err error) { 101 | err = s.ServiceObj(). 102 | Call("org.freedesktop.Secret.Service.OpenSession", NilFlags, mode, sessionAlgorithmInput). 103 | Store(&resp.algorithmOutput, &resp.path) 104 | return resp, errors.Wrap(err, "failed to open secretservice session") 105 | } 106 | 107 | // OpenSession 108 | func (s *SecretService) OpenSession(mode AuthenticationMode) (session *Session, err error) { 109 | var sessionAlgorithmInput dbus.Variant 110 | 111 | session = new(Session) 112 | 113 | session.Mode = mode 114 | 115 | switch mode { 116 | case AuthenticationInsecurePlain: 117 | sessionAlgorithmInput = dbus.MakeVariant("") 118 | case AuthenticationDHAES: 119 | group := rfc2409SecondOakleyGroup() 120 | private, public, err := group.NewKeypair() 121 | if err != nil { 122 | return nil, err 123 | } 124 | session.Private = private 125 | session.Public = public 126 | sessionAlgorithmInput = dbus.MakeVariant(public.Bytes()) // math/big.Int.Bytes is big endian 127 | default: 128 | return nil, errors.Errorf("unknown authentication mode %v", mode) 129 | } 130 | 131 | sessionOpenCh := make(chan sessionOpenResponse) 132 | errCh := make(chan error) 133 | go func() { 134 | sessionOpenResponse, err := s.openSessionRaw(mode, sessionAlgorithmInput) 135 | if err != nil { 136 | errCh <- err 137 | } else { 138 | sessionOpenCh <- sessionOpenResponse 139 | } 140 | }() 141 | 142 | var sessionAlgorithmOutput dbus.Variant 143 | // NOTE: If the timeout case is reached, the above goroutine is leaked. 144 | // This is not terrible because D-Bus calls have an internal 2-mintue 145 | // timeout, so the goroutine will finish eventually. If two OpenSessions 146 | // are called at the saime time, they'll be on different channels so 147 | // they won't interfere with each other. 148 | select { 149 | case resp := <-sessionOpenCh: 150 | sessionAlgorithmOutput = resp.algorithmOutput 151 | session.Path = resp.path 152 | case err := <-errCh: 153 | return nil, err 154 | case <-time.After(s.sessionOpenTimeout): 155 | return nil, errors.Errorf("timed out after %s", s.sessionOpenTimeout) 156 | } 157 | 158 | switch mode { 159 | case AuthenticationInsecurePlain: 160 | case AuthenticationDHAES: 161 | theirPublicBigEndian, ok := sessionAlgorithmOutput.Value().([]byte) 162 | if !ok { 163 | return nil, errors.Errorf("failed to coerce algorithm output value to byteslice") 164 | } 165 | group := rfc2409SecondOakleyGroup() 166 | theirPublic := new(big.Int) 167 | theirPublic.SetBytes(theirPublicBigEndian) 168 | aesKey, err := group.keygenHKDFSHA256AES128(theirPublic, session.Private) 169 | if err != nil { 170 | return nil, err 171 | } 172 | session.AESKey = aesKey 173 | default: 174 | return nil, errors.Errorf("unknown authentication mode %v", mode) 175 | } 176 | 177 | return session, nil 178 | } 179 | 180 | // CloseSession 181 | func (s *SecretService) CloseSession(session *Session) { 182 | s.Obj(session.Path).Call("org.freedesktop.Secret.Session.Close", NilFlags) 183 | } 184 | 185 | // SearchColleciton 186 | func (s *SecretService) SearchCollection(collection dbus.ObjectPath, attributes Attributes) (items []dbus.ObjectPath, err error) { 187 | err = s.Obj(collection). 188 | Call("org.freedesktop.Secret.Collection.SearchItems", NilFlags, attributes). 189 | Store(&items) 190 | if err != nil { 191 | return nil, errors.Wrap(err, "failed to search collection") 192 | } 193 | return items, nil 194 | } 195 | 196 | // ReplaceBehavior 197 | type ReplaceBehavior int 198 | 199 | // ReplaceBehaviorDoNotReplace 200 | const ReplaceBehaviorDoNotReplace = 0 201 | 202 | // ReplaceBehaviorReplace 203 | const ReplaceBehaviorReplace = 1 204 | 205 | // CreateItem 206 | func (s *SecretService) CreateItem(collection dbus.ObjectPath, properties map[string]dbus.Variant, secret Secret, replaceBehavior ReplaceBehavior) (item dbus.ObjectPath, err error) { 207 | var replace bool 208 | switch replaceBehavior { 209 | case ReplaceBehaviorDoNotReplace: 210 | replace = false 211 | case ReplaceBehaviorReplace: 212 | replace = true 213 | default: 214 | return "", errors.Errorf("unknown replace behavior %v", replaceBehavior) 215 | } 216 | 217 | var prompt dbus.ObjectPath 218 | err = s.Obj(collection). 219 | Call("org.freedesktop.Secret.Collection.CreateItem", NilFlags, properties, secret, replace). 220 | Store(&item, &prompt) 221 | if err != nil { 222 | return "", errors.Wrap(err, "failed to create item") 223 | } 224 | _, err = s.PromptAndWait(prompt) 225 | if err != nil { 226 | return "", err 227 | } 228 | return item, nil 229 | } 230 | 231 | // DeleteItem 232 | func (s *SecretService) DeleteItem(item dbus.ObjectPath) (err error) { 233 | var prompt dbus.ObjectPath 234 | err = s.Obj(item). 235 | Call("org.freedesktop.Secret.Item.Delete", NilFlags). 236 | Store(&prompt) 237 | if err != nil { 238 | return errors.Wrap(err, "failed to delete item") 239 | } 240 | _, err = s.PromptAndWait(prompt) 241 | if err != nil { 242 | return err 243 | } 244 | return nil 245 | } 246 | 247 | // GetAttributes 248 | func (s *SecretService) GetAttributes(item dbus.ObjectPath) (attributes Attributes, err error) { 249 | attributesV, err := s.Obj(item).GetProperty("org.freedesktop.Secret.Item.Attributes") 250 | if err != nil { 251 | return nil, errors.Wrap(err, "failed to get attributes") 252 | } 253 | attributesMap, ok := attributesV.Value().(map[string]string) 254 | if !ok { 255 | return nil, errors.Errorf("failed to coerce item attributes") 256 | } 257 | return Attributes(attributesMap), nil 258 | } 259 | 260 | // GetSecret 261 | func (s *SecretService) GetSecret(item dbus.ObjectPath, session Session) (secretPlaintext []byte, err error) { 262 | var secretI []interface{} 263 | err = s.Obj(item). 264 | Call("org.freedesktop.Secret.Item.GetSecret", NilFlags, session.Path). 265 | Store(&secretI) 266 | if err != nil { 267 | return nil, errors.Wrap(err, "failed to get secret") 268 | } 269 | secret := new(Secret) 270 | err = dbus.Store(secretI, &secret.Session, &secret.Parameters, &secret.Value, &secret.ContentType) 271 | if err != nil { 272 | return nil, errors.Wrap(err, "failed to unmarshal get secret result") 273 | } 274 | 275 | switch session.Mode { 276 | case AuthenticationInsecurePlain: 277 | secretPlaintext = secret.Value 278 | case AuthenticationDHAES: 279 | plaintext, err := unauthenticatedAESCBCDecrypt(secret.Parameters, secret.Value, session.AESKey) 280 | if err != nil { 281 | return nil, nil 282 | } 283 | secretPlaintext = plaintext 284 | default: 285 | return nil, errors.Errorf("cannot make secret for authentication mode %v", session.Mode) 286 | } 287 | 288 | return secretPlaintext, nil 289 | } 290 | 291 | // NullPrompt 292 | const NullPrompt = "/" 293 | 294 | // Unlock 295 | func (s *SecretService) Unlock(items []dbus.ObjectPath) (err error) { 296 | var dummy []dbus.ObjectPath 297 | var prompt dbus.ObjectPath 298 | err = s.ServiceObj(). 299 | Call("org.freedesktop.Secret.Service.Unlock", NilFlags, items). 300 | Store(&dummy, &prompt) 301 | if err != nil { 302 | return errors.Wrap(err, "failed to unlock items") 303 | } 304 | _, err = s.PromptAndWait(prompt) 305 | if err != nil { 306 | return errors.Wrap(err, "failed to prompt") 307 | } 308 | return nil 309 | } 310 | 311 | // LockItems 312 | func (s *SecretService) LockItems(items []dbus.ObjectPath) (err error) { 313 | var dummy []dbus.ObjectPath 314 | var prompt dbus.ObjectPath 315 | err = s.ServiceObj(). 316 | Call("org.freedesktop.Secret.Service.Lock", NilFlags, items). 317 | Store(&dummy, &prompt) 318 | if err != nil { 319 | return errors.Wrap(err, "failed to lock items") 320 | } 321 | _, err = s.PromptAndWait(prompt) 322 | if err != nil { 323 | return errors.Wrap(err, "failed to prompt") 324 | } 325 | return nil 326 | } 327 | 328 | // PromptDismissedError 329 | type PromptDismissedError struct { 330 | err error 331 | } 332 | 333 | // Error 334 | func (p PromptDismissedError) Error() string { 335 | return p.err.Error() 336 | } 337 | 338 | // PromptAndWait is NOT thread-safe. 339 | func (s *SecretService) PromptAndWait(prompt dbus.ObjectPath) (paths *dbus.Variant, err error) { 340 | if prompt == NullPrompt { 341 | return nil, nil 342 | } 343 | call := s.Obj(prompt).Call("org.freedesktop.Secret.Prompt.Prompt", NilFlags, "Keyring Prompt") 344 | if call.Err != nil { 345 | return nil, errors.Wrap(err, "failed to prompt") 346 | } 347 | for { 348 | var result PromptCompletedResult 349 | select { 350 | case signal, ok := <-s.signalCh: 351 | if !ok { 352 | return nil, errors.New("prompt channel closed") 353 | } 354 | if signal == nil { 355 | continue 356 | } 357 | if signal.Name != "org.freedesktop.Secret.Prompt.Completed" { 358 | continue 359 | } 360 | err = dbus.Store(signal.Body, &result.Dismissed, &result.Paths) 361 | if err != nil { 362 | return nil, errors.Wrap(err, "failed to unmarshal prompt result") 363 | } 364 | if result.Dismissed { 365 | return nil, PromptDismissedError{errors.New("prompt dismissed")} 366 | } 367 | return &result.Paths, nil 368 | case <-time.After(30 * time.Second): 369 | return nil, errors.New("prompt timed out") 370 | } 371 | } 372 | } 373 | 374 | // NewSecretProperties 375 | func NewSecretProperties(label string, attributes map[string]string) map[string]dbus.Variant { 376 | return map[string]dbus.Variant{ 377 | "org.freedesktop.Secret.Item.Label": dbus.MakeVariant(label), 378 | "org.freedesktop.Secret.Item.Attributes": dbus.MakeVariant(attributes), 379 | } 380 | } 381 | 382 | // NewSecret 383 | func (session *Session) NewSecret(secretBytes []byte) (Secret, error) { 384 | switch session.Mode { 385 | case AuthenticationInsecurePlain: 386 | return Secret{ 387 | Session: session.Path, 388 | Parameters: nil, 389 | Value: secretBytes, 390 | ContentType: "application/octet-stream", 391 | }, nil 392 | case AuthenticationDHAES: 393 | iv, ciphertext, err := unauthenticatedAESCBCEncrypt(secretBytes, session.AESKey) 394 | if err != nil { 395 | return Secret{}, err 396 | } 397 | return Secret{ 398 | Session: session.Path, 399 | Parameters: iv, 400 | Value: ciphertext, 401 | ContentType: "application/octet-stream", 402 | }, nil 403 | default: 404 | return Secret{}, errors.Errorf("cannot make secret for authentication mode %v", session.Mode) 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /ios/Keychain.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 00FB9F181BB5DE2E003FBCBD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00FB9F171BB5DE2E003FBCBD /* AppDelegate.swift */; }; 11 | 00FB9F1A1BB5DE2E003FBCBD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00FB9F191BB5DE2E003FBCBD /* ViewController.swift */; }; 12 | 00FB9F1D1BB5DE2E003FBCBD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00FB9F1B1BB5DE2E003FBCBD /* Main.storyboard */; }; 13 | 00FB9F1F1BB5DE2E003FBCBD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00FB9F1E1BB5DE2E003FBCBD /* Assets.xcassets */; }; 14 | 00FB9F221BB5DE2E003FBCBD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00FB9F201BB5DE2E003FBCBD /* LaunchScreen.storyboard */; }; 15 | 00FB9F2D1BB5DE2E003FBCBD /* KeychainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00FB9F2C1BB5DE2E003FBCBD /* KeychainTests.swift */; }; 16 | 00FB9F3E1BB5E147003FBCBD /* bind.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00FB9F3D1BB5E147003FBCBD /* bind.framework */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXContainerItemProxy section */ 20 | 00FB9F291BB5DE2E003FBCBD /* PBXContainerItemProxy */ = { 21 | isa = PBXContainerItemProxy; 22 | containerPortal = 00FB9F0C1BB5DE2D003FBCBD /* Project object */; 23 | proxyType = 1; 24 | remoteGlobalIDString = 00FB9F131BB5DE2D003FBCBD; 25 | remoteInfo = Keychain; 26 | }; 27 | /* End PBXContainerItemProxy section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 00FB9F141BB5DE2D003FBCBD /* Keychain.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Keychain.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 00FB9F171BB5DE2E003FBCBD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 32 | 00FB9F191BB5DE2E003FBCBD /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 33 | 00FB9F1C1BB5DE2E003FBCBD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 34 | 00FB9F1E1BB5DE2E003FBCBD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 35 | 00FB9F211BB5DE2E003FBCBD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 36 | 00FB9F231BB5DE2E003FBCBD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37 | 00FB9F281BB5DE2E003FBCBD /* KeychainTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KeychainTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | 00FB9F2C1BB5DE2E003FBCBD /* KeychainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainTests.swift; sourceTree = ""; }; 39 | 00FB9F2E1BB5DE2E003FBCBD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 00FB9F381BB5DEFB003FBCBD /* Keychain-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Keychain-Bridging-Header.h"; sourceTree = ""; }; 41 | 00FB9F3D1BB5E147003FBCBD /* bind.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = bind.framework; sourceTree = ""; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | 00FB9F111BB5DE2D003FBCBD /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | 00FB9F3E1BB5E147003FBCBD /* bind.framework in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | 00FB9F251BB5DE2E003FBCBD /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXFrameworksBuildPhase section */ 61 | 62 | /* Begin PBXGroup section */ 63 | 00FB9F0B1BB5DE2D003FBCBD = { 64 | isa = PBXGroup; 65 | children = ( 66 | 00FB9F161BB5DE2D003FBCBD /* Keychain */, 67 | 00FB9F3D1BB5E147003FBCBD /* bind.framework */, 68 | 00FB9F2B1BB5DE2E003FBCBD /* KeychainTests */, 69 | 00FB9F151BB5DE2D003FBCBD /* Products */, 70 | ); 71 | sourceTree = ""; 72 | }; 73 | 00FB9F151BB5DE2D003FBCBD /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 00FB9F141BB5DE2D003FBCBD /* Keychain.app */, 77 | 00FB9F281BB5DE2E003FBCBD /* KeychainTests.xctest */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | 00FB9F161BB5DE2D003FBCBD /* Keychain */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 00FB9F171BB5DE2E003FBCBD /* AppDelegate.swift */, 86 | 00FB9F191BB5DE2E003FBCBD /* ViewController.swift */, 87 | 00FB9F1B1BB5DE2E003FBCBD /* Main.storyboard */, 88 | 00FB9F1E1BB5DE2E003FBCBD /* Assets.xcassets */, 89 | 00FB9F201BB5DE2E003FBCBD /* LaunchScreen.storyboard */, 90 | 00FB9F231BB5DE2E003FBCBD /* Info.plist */, 91 | 00FB9F381BB5DEFB003FBCBD /* Keychain-Bridging-Header.h */, 92 | ); 93 | path = Keychain; 94 | sourceTree = ""; 95 | }; 96 | 00FB9F2B1BB5DE2E003FBCBD /* KeychainTests */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 00FB9F2C1BB5DE2E003FBCBD /* KeychainTests.swift */, 100 | 00FB9F2E1BB5DE2E003FBCBD /* Info.plist */, 101 | ); 102 | path = KeychainTests; 103 | sourceTree = ""; 104 | }; 105 | /* End PBXGroup section */ 106 | 107 | /* Begin PBXNativeTarget section */ 108 | 00FB9F131BB5DE2D003FBCBD /* Keychain */ = { 109 | isa = PBXNativeTarget; 110 | buildConfigurationList = 00FB9F311BB5DE2E003FBCBD /* Build configuration list for PBXNativeTarget "Keychain" */; 111 | buildPhases = ( 112 | 00FB9F101BB5DE2D003FBCBD /* Sources */, 113 | 00FB9F111BB5DE2D003FBCBD /* Frameworks */, 114 | 00FB9F121BB5DE2D003FBCBD /* Resources */, 115 | ); 116 | buildRules = ( 117 | ); 118 | dependencies = ( 119 | ); 120 | name = Keychain; 121 | productName = Keychain; 122 | productReference = 00FB9F141BB5DE2D003FBCBD /* Keychain.app */; 123 | productType = "com.apple.product-type.application"; 124 | }; 125 | 00FB9F271BB5DE2E003FBCBD /* KeychainTests */ = { 126 | isa = PBXNativeTarget; 127 | buildConfigurationList = 00FB9F341BB5DE2E003FBCBD /* Build configuration list for PBXNativeTarget "KeychainTests" */; 128 | buildPhases = ( 129 | 00FB9F241BB5DE2E003FBCBD /* Sources */, 130 | 00FB9F251BB5DE2E003FBCBD /* Frameworks */, 131 | 00FB9F261BB5DE2E003FBCBD /* Resources */, 132 | ); 133 | buildRules = ( 134 | ); 135 | dependencies = ( 136 | 00FB9F2A1BB5DE2E003FBCBD /* PBXTargetDependency */, 137 | ); 138 | name = KeychainTests; 139 | productName = KeychainTests; 140 | productReference = 00FB9F281BB5DE2E003FBCBD /* KeychainTests.xctest */; 141 | productType = "com.apple.product-type.bundle.unit-test"; 142 | }; 143 | /* End PBXNativeTarget section */ 144 | 145 | /* Begin PBXProject section */ 146 | 00FB9F0C1BB5DE2D003FBCBD /* Project object */ = { 147 | isa = PBXProject; 148 | attributes = { 149 | LastSwiftUpdateCheck = 0700; 150 | LastUpgradeCheck = 0830; 151 | ORGANIZATIONNAME = "Gabriel Handford"; 152 | TargetAttributes = { 153 | 00FB9F131BB5DE2D003FBCBD = { 154 | CreatedOnToolsVersion = 7.0; 155 | LastSwiftMigration = 0820; 156 | }; 157 | 00FB9F271BB5DE2E003FBCBD = { 158 | CreatedOnToolsVersion = 7.0; 159 | LastSwiftMigration = 0820; 160 | TestTargetID = 00FB9F131BB5DE2D003FBCBD; 161 | }; 162 | }; 163 | }; 164 | buildConfigurationList = 00FB9F0F1BB5DE2D003FBCBD /* Build configuration list for PBXProject "Keychain" */; 165 | compatibilityVersion = "Xcode 3.2"; 166 | developmentRegion = English; 167 | hasScannedForEncodings = 0; 168 | knownRegions = ( 169 | en, 170 | Base, 171 | ); 172 | mainGroup = 00FB9F0B1BB5DE2D003FBCBD; 173 | productRefGroup = 00FB9F151BB5DE2D003FBCBD /* Products */; 174 | projectDirPath = ""; 175 | projectRoot = ""; 176 | targets = ( 177 | 00FB9F131BB5DE2D003FBCBD /* Keychain */, 178 | 00FB9F271BB5DE2E003FBCBD /* KeychainTests */, 179 | ); 180 | }; 181 | /* End PBXProject section */ 182 | 183 | /* Begin PBXResourcesBuildPhase section */ 184 | 00FB9F121BB5DE2D003FBCBD /* Resources */ = { 185 | isa = PBXResourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | 00FB9F221BB5DE2E003FBCBD /* LaunchScreen.storyboard in Resources */, 189 | 00FB9F1F1BB5DE2E003FBCBD /* Assets.xcassets in Resources */, 190 | 00FB9F1D1BB5DE2E003FBCBD /* Main.storyboard in Resources */, 191 | ); 192 | runOnlyForDeploymentPostprocessing = 0; 193 | }; 194 | 00FB9F261BB5DE2E003FBCBD /* Resources */ = { 195 | isa = PBXResourcesBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | ); 199 | runOnlyForDeploymentPostprocessing = 0; 200 | }; 201 | /* End PBXResourcesBuildPhase section */ 202 | 203 | /* Begin PBXSourcesBuildPhase section */ 204 | 00FB9F101BB5DE2D003FBCBD /* Sources */ = { 205 | isa = PBXSourcesBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | 00FB9F1A1BB5DE2E003FBCBD /* ViewController.swift in Sources */, 209 | 00FB9F181BB5DE2E003FBCBD /* AppDelegate.swift in Sources */, 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | }; 213 | 00FB9F241BB5DE2E003FBCBD /* Sources */ = { 214 | isa = PBXSourcesBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | 00FB9F2D1BB5DE2E003FBCBD /* KeychainTests.swift in Sources */, 218 | ); 219 | runOnlyForDeploymentPostprocessing = 0; 220 | }; 221 | /* End PBXSourcesBuildPhase section */ 222 | 223 | /* Begin PBXTargetDependency section */ 224 | 00FB9F2A1BB5DE2E003FBCBD /* PBXTargetDependency */ = { 225 | isa = PBXTargetDependency; 226 | target = 00FB9F131BB5DE2D003FBCBD /* Keychain */; 227 | targetProxy = 00FB9F291BB5DE2E003FBCBD /* PBXContainerItemProxy */; 228 | }; 229 | /* End PBXTargetDependency section */ 230 | 231 | /* Begin PBXVariantGroup section */ 232 | 00FB9F1B1BB5DE2E003FBCBD /* Main.storyboard */ = { 233 | isa = PBXVariantGroup; 234 | children = ( 235 | 00FB9F1C1BB5DE2E003FBCBD /* Base */, 236 | ); 237 | name = Main.storyboard; 238 | sourceTree = ""; 239 | }; 240 | 00FB9F201BB5DE2E003FBCBD /* LaunchScreen.storyboard */ = { 241 | isa = PBXVariantGroup; 242 | children = ( 243 | 00FB9F211BB5DE2E003FBCBD /* Base */, 244 | ); 245 | name = LaunchScreen.storyboard; 246 | sourceTree = ""; 247 | }; 248 | /* End PBXVariantGroup section */ 249 | 250 | /* Begin XCBuildConfiguration section */ 251 | 00FB9F2F1BB5DE2E003FBCBD /* Debug */ = { 252 | isa = XCBuildConfiguration; 253 | buildSettings = { 254 | ALWAYS_SEARCH_USER_PATHS = NO; 255 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 256 | CLANG_CXX_LIBRARY = "libc++"; 257 | CLANG_ENABLE_MODULES = YES; 258 | CLANG_ENABLE_OBJC_ARC = YES; 259 | CLANG_WARN_BOOL_CONVERSION = YES; 260 | CLANG_WARN_CONSTANT_CONVERSION = YES; 261 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 262 | CLANG_WARN_EMPTY_BODY = YES; 263 | CLANG_WARN_ENUM_CONVERSION = YES; 264 | CLANG_WARN_INFINITE_RECURSION = YES; 265 | CLANG_WARN_INT_CONVERSION = YES; 266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 267 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 268 | CLANG_WARN_UNREACHABLE_CODE = YES; 269 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 270 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 271 | COPY_PHASE_STRIP = NO; 272 | DEBUG_INFORMATION_FORMAT = dwarf; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | ENABLE_TESTABILITY = YES; 275 | GCC_C_LANGUAGE_STANDARD = gnu99; 276 | GCC_DYNAMIC_NO_PIC = NO; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_OPTIMIZATION_LEVEL = 0; 279 | GCC_PREPROCESSOR_DEFINITIONS = ( 280 | "DEBUG=1", 281 | "$(inherited)", 282 | ); 283 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 284 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 285 | GCC_WARN_UNDECLARED_SELECTOR = YES; 286 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 287 | GCC_WARN_UNUSED_FUNCTION = YES; 288 | GCC_WARN_UNUSED_VARIABLE = YES; 289 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 290 | MTL_ENABLE_DEBUG_INFO = YES; 291 | ONLY_ACTIVE_ARCH = YES; 292 | SDKROOT = iphoneos; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | TARGETED_DEVICE_FAMILY = "1,2"; 295 | }; 296 | name = Debug; 297 | }; 298 | 00FB9F301BB5DE2E003FBCBD /* Release */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 303 | CLANG_CXX_LIBRARY = "libc++"; 304 | CLANG_ENABLE_MODULES = YES; 305 | CLANG_ENABLE_OBJC_ARC = YES; 306 | CLANG_WARN_BOOL_CONVERSION = YES; 307 | CLANG_WARN_CONSTANT_CONVERSION = YES; 308 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 309 | CLANG_WARN_EMPTY_BODY = YES; 310 | CLANG_WARN_ENUM_CONVERSION = YES; 311 | CLANG_WARN_INFINITE_RECURSION = YES; 312 | CLANG_WARN_INT_CONVERSION = YES; 313 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 314 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 315 | CLANG_WARN_UNREACHABLE_CODE = YES; 316 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 317 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 318 | COPY_PHASE_STRIP = NO; 319 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 320 | ENABLE_NS_ASSERTIONS = NO; 321 | ENABLE_STRICT_OBJC_MSGSEND = YES; 322 | GCC_C_LANGUAGE_STANDARD = gnu99; 323 | GCC_NO_COMMON_BLOCKS = YES; 324 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 325 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 326 | GCC_WARN_UNDECLARED_SELECTOR = YES; 327 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 328 | GCC_WARN_UNUSED_FUNCTION = YES; 329 | GCC_WARN_UNUSED_VARIABLE = YES; 330 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 331 | MTL_ENABLE_DEBUG_INFO = NO; 332 | SDKROOT = iphoneos; 333 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 334 | TARGETED_DEVICE_FAMILY = "1,2"; 335 | VALIDATE_PRODUCT = YES; 336 | }; 337 | name = Release; 338 | }; 339 | 00FB9F321BB5DE2E003FBCBD /* Debug */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 343 | CLANG_ENABLE_MODULES = YES; 344 | FRAMEWORK_SEARCH_PATHS = ( 345 | "$(inherited)", 346 | "$(PROJECT_DIR)/Keychain", 347 | "$(PROJECT_DIR)", 348 | ); 349 | INFOPLIST_FILE = Keychain/Info.plist; 350 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 351 | PRODUCT_BUNDLE_IDENTIFIER = me.rel.Keychain; 352 | PRODUCT_NAME = "$(TARGET_NAME)"; 353 | SWIFT_OBJC_BRIDGING_HEADER = "Keychain/Keychain-Bridging-Header.h"; 354 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 355 | SWIFT_VERSION = 3.0; 356 | }; 357 | name = Debug; 358 | }; 359 | 00FB9F331BB5DE2E003FBCBD /* Release */ = { 360 | isa = XCBuildConfiguration; 361 | buildSettings = { 362 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 363 | CLANG_ENABLE_MODULES = YES; 364 | FRAMEWORK_SEARCH_PATHS = ( 365 | "$(inherited)", 366 | "$(PROJECT_DIR)/Keychain", 367 | "$(PROJECT_DIR)", 368 | ); 369 | INFOPLIST_FILE = Keychain/Info.plist; 370 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 371 | PRODUCT_BUNDLE_IDENTIFIER = me.rel.Keychain; 372 | PRODUCT_NAME = "$(TARGET_NAME)"; 373 | SWIFT_OBJC_BRIDGING_HEADER = "Keychain/Keychain-Bridging-Header.h"; 374 | SWIFT_VERSION = 3.0; 375 | }; 376 | name = Release; 377 | }; 378 | 00FB9F351BB5DE2E003FBCBD /* Debug */ = { 379 | isa = XCBuildConfiguration; 380 | buildSettings = { 381 | BUNDLE_LOADER = "$(TEST_HOST)"; 382 | FRAMEWORK_SEARCH_PATHS = ( 383 | "$(inherited)", 384 | "$(PROJECT_DIR)", 385 | ); 386 | INFOPLIST_FILE = KeychainTests/Info.plist; 387 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 388 | PRODUCT_BUNDLE_IDENTIFIER = me.rel.KeychainTests; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | SWIFT_VERSION = 3.0; 391 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Keychain.app/Keychain"; 392 | }; 393 | name = Debug; 394 | }; 395 | 00FB9F361BB5DE2E003FBCBD /* Release */ = { 396 | isa = XCBuildConfiguration; 397 | buildSettings = { 398 | BUNDLE_LOADER = "$(TEST_HOST)"; 399 | FRAMEWORK_SEARCH_PATHS = ( 400 | "$(inherited)", 401 | "$(PROJECT_DIR)", 402 | ); 403 | INFOPLIST_FILE = KeychainTests/Info.plist; 404 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 405 | PRODUCT_BUNDLE_IDENTIFIER = me.rel.KeychainTests; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | SWIFT_VERSION = 3.0; 408 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Keychain.app/Keychain"; 409 | }; 410 | name = Release; 411 | }; 412 | /* End XCBuildConfiguration section */ 413 | 414 | /* Begin XCConfigurationList section */ 415 | 00FB9F0F1BB5DE2D003FBCBD /* Build configuration list for PBXProject "Keychain" */ = { 416 | isa = XCConfigurationList; 417 | buildConfigurations = ( 418 | 00FB9F2F1BB5DE2E003FBCBD /* Debug */, 419 | 00FB9F301BB5DE2E003FBCBD /* Release */, 420 | ); 421 | defaultConfigurationIsVisible = 0; 422 | defaultConfigurationName = Release; 423 | }; 424 | 00FB9F311BB5DE2E003FBCBD /* Build configuration list for PBXNativeTarget "Keychain" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | 00FB9F321BB5DE2E003FBCBD /* Debug */, 428 | 00FB9F331BB5DE2E003FBCBD /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | 00FB9F341BB5DE2E003FBCBD /* Build configuration list for PBXNativeTarget "KeychainTests" */ = { 434 | isa = XCConfigurationList; 435 | buildConfigurations = ( 436 | 00FB9F351BB5DE2E003FBCBD /* Debug */, 437 | 00FB9F361BB5DE2E003FBCBD /* Release */, 438 | ); 439 | defaultConfigurationIsVisible = 0; 440 | defaultConfigurationName = Release; 441 | }; 442 | /* End XCConfigurationList section */ 443 | }; 444 | rootObject = 00FB9F0C1BB5DE2D003FBCBD /* Project object */; 445 | } 446 | -------------------------------------------------------------------------------- /keychain.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package keychain 5 | 6 | // See https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/index.html for the APIs used below. 7 | 8 | // Also see https://developer.apple.com/library/ios/documentation/Security/Conceptual/keychainServConcepts/01introduction/introduction.html . 9 | 10 | /* 11 | #cgo LDFLAGS: -framework CoreFoundation -framework Security 12 | 13 | #include 14 | #include 15 | */ 16 | import "C" 17 | import ( 18 | "fmt" 19 | "time" 20 | ) 21 | 22 | // Error defines keychain errors 23 | type Error int 24 | 25 | var ( 26 | // ErrorUnimplemented corresponds to errSecUnimplemented result code 27 | ErrorUnimplemented = Error(C.errSecUnimplemented) 28 | // ErrorParam corresponds to errSecParam result code 29 | ErrorParam = Error(C.errSecParam) 30 | // ErrorAllocate corresponds to errSecAllocate result code 31 | ErrorAllocate = Error(C.errSecAllocate) 32 | // ErrorNotAvailable corresponds to errSecNotAvailable result code 33 | ErrorNotAvailable = Error(C.errSecNotAvailable) 34 | // ErrorAuthFailed corresponds to errSecAuthFailed result code 35 | ErrorAuthFailed = Error(C.errSecAuthFailed) 36 | // ErrorDuplicateItem corresponds to errSecDuplicateItem result code 37 | ErrorDuplicateItem = Error(C.errSecDuplicateItem) 38 | // ErrorItemNotFound corresponds to errSecItemNotFound result code 39 | ErrorItemNotFound = Error(C.errSecItemNotFound) 40 | // ErrorInteractionNotAllowed corresponds to errSecInteractionNotAllowed result code 41 | ErrorInteractionNotAllowed = Error(C.errSecInteractionNotAllowed) 42 | // ErrorDecode corresponds to errSecDecode result code 43 | ErrorDecode = Error(C.errSecDecode) 44 | // ErrorNoSuchKeychain corresponds to errSecNoSuchKeychain result code 45 | ErrorNoSuchKeychain = Error(C.errSecNoSuchKeychain) 46 | // ErrorNoAccessForItem corresponds to errSecNoAccessForItem result code 47 | ErrorNoAccessForItem = Error(C.errSecNoAccessForItem) 48 | // ErrorReadOnly corresponds to errSecReadOnly result code 49 | ErrorReadOnly = Error(C.errSecReadOnly) 50 | // ErrorInvalidKeychain corresponds to errSecInvalidKeychain result code 51 | ErrorInvalidKeychain = Error(C.errSecInvalidKeychain) 52 | // ErrorDuplicateKeyChain corresponds to errSecDuplicateKeychain result code 53 | ErrorDuplicateKeyChain = Error(C.errSecDuplicateKeychain) 54 | // ErrorWrongVersion corresponds to errSecWrongSecVersion result code 55 | ErrorWrongVersion = Error(C.errSecWrongSecVersion) 56 | // ErrorReadonlyAttribute corresponds to errSecReadOnlyAttr result code 57 | ErrorReadonlyAttribute = Error(C.errSecReadOnlyAttr) 58 | // ErrorInvalidSearchRef corresponds to errSecInvalidSearchRef result code 59 | ErrorInvalidSearchRef = Error(C.errSecInvalidSearchRef) 60 | // ErrorInvalidItemRef corresponds to errSecInvalidItemRef result code 61 | ErrorInvalidItemRef = Error(C.errSecInvalidItemRef) 62 | // ErrorDataNotAvailable corresponds to errSecDataNotAvailable result code 63 | ErrorDataNotAvailable = Error(C.errSecDataNotAvailable) 64 | // ErrorDataNotModifiable corresponds to errSecDataNotModifiable result code 65 | ErrorDataNotModifiable = Error(C.errSecDataNotModifiable) 66 | // ErrorInvalidOwnerEdit corresponds to errSecInvalidOwnerEdit result code 67 | ErrorInvalidOwnerEdit = Error(C.errSecInvalidOwnerEdit) 68 | // ErrorUserCanceled corresponds to errSecUserCanceled result code 69 | ErrorUserCanceled = Error(C.errSecUserCanceled) 70 | ) 71 | 72 | func checkError(errCode C.OSStatus) error { 73 | if errCode == C.errSecSuccess { 74 | return nil 75 | } 76 | return Error(errCode) 77 | } 78 | 79 | func (k Error) Error() (msg string) { 80 | // SecCopyErrorMessageString is only available on OSX, so derive manually. 81 | // Messages derived from `$ security error $errcode`. 82 | switch k { 83 | case ErrorUnimplemented: 84 | msg = "Function or operation not implemented." 85 | case ErrorParam: 86 | msg = "One or more parameters passed to the function were not valid." 87 | case ErrorAllocate: 88 | msg = "Failed to allocate memory." 89 | case ErrorNotAvailable: 90 | msg = "No keychain is available. You may need to restart your computer." 91 | case ErrorAuthFailed: 92 | msg = "The user name or passphrase you entered is not correct." 93 | case ErrorDuplicateItem: 94 | msg = "The specified item already exists in the keychain." 95 | case ErrorItemNotFound: 96 | msg = "The specified item could not be found in the keychain." 97 | case ErrorInteractionNotAllowed: 98 | msg = "User interaction is not allowed." 99 | case ErrorDecode: 100 | msg = "Unable to decode the provided data." 101 | case ErrorNoSuchKeychain: 102 | msg = "The specified keychain could not be found." 103 | case ErrorNoAccessForItem: 104 | msg = "The specified item has no access control." 105 | case ErrorReadOnly: 106 | msg = "Read-only error." 107 | case ErrorReadonlyAttribute: 108 | msg = "The attribute is read-only." 109 | case ErrorInvalidKeychain: 110 | msg = "The keychain is not valid." 111 | case ErrorDuplicateKeyChain: 112 | msg = "A keychain with the same name already exists." 113 | case ErrorWrongVersion: 114 | msg = "The version is incorrect." 115 | case ErrorInvalidItemRef: 116 | msg = "The item reference is invalid." 117 | case ErrorInvalidSearchRef: 118 | msg = "The search reference is invalid." 119 | case ErrorDataNotAvailable: 120 | msg = "The data is not available." 121 | case ErrorDataNotModifiable: 122 | msg = "The data is not modifiable." 123 | case ErrorInvalidOwnerEdit: 124 | msg = "An invalid attempt to change the owner of an item." 125 | case ErrorUserCanceled: 126 | msg = "User canceled the operation." 127 | default: 128 | msg = "Keychain Error." 129 | } 130 | return fmt.Sprintf("%s (%d)", msg, k) 131 | } 132 | 133 | // SecClass is the items class code 134 | type SecClass int 135 | 136 | // Keychain Item Classes 137 | var ( 138 | /* 139 | kSecClassGenericPassword item attributes: 140 | kSecAttrAccess (OS X only) 141 | kSecAttrAccessGroup (iOS; also OS X if kSecAttrSynchronizable specified) 142 | kSecAttrAccessible (iOS; also OS X if kSecAttrSynchronizable specified) 143 | kSecAttrAccount 144 | kSecAttrService 145 | */ 146 | SecClassGenericPassword SecClass = 1 147 | SecClassInternetPassword SecClass = 2 148 | ) 149 | 150 | // SecClassKey is the key type for SecClass 151 | var SecClassKey = attrKey(C.CFTypeRef(C.kSecClass)) 152 | var secClassTypeRef = map[SecClass]C.CFTypeRef{ 153 | SecClassGenericPassword: C.CFTypeRef(C.kSecClassGenericPassword), 154 | SecClassInternetPassword: C.CFTypeRef(C.kSecClassInternetPassword), 155 | } 156 | 157 | var ( 158 | // ServiceKey is for kSecAttrService 159 | ServiceKey = attrKey(C.CFTypeRef(C.kSecAttrService)) 160 | 161 | // ServerKey is for kSecAttrServer 162 | ServerKey = attrKey(C.CFTypeRef(C.kSecAttrServer)) 163 | // ProtocolKey is for kSecAttrProtocol 164 | ProtocolKey = attrKey(C.CFTypeRef(C.kSecAttrProtocol)) 165 | // AuthenticationTypeKey is for kSecAttrAuthenticationType 166 | AuthenticationTypeKey = attrKey(C.CFTypeRef(C.kSecAttrAuthenticationType)) 167 | // PortKey is for kSecAttrPort 168 | PortKey = attrKey(C.CFTypeRef(C.kSecAttrPort)) 169 | // PathKey is for kSecAttrPath 170 | PathKey = attrKey(C.CFTypeRef(C.kSecAttrPath)) 171 | 172 | // LabelKey is for kSecAttrLabel 173 | LabelKey = attrKey(C.CFTypeRef(C.kSecAttrLabel)) 174 | // AccountKey is for kSecAttrAccount 175 | AccountKey = attrKey(C.CFTypeRef(C.kSecAttrAccount)) 176 | // AccessGroupKey is for kSecAttrAccessGroup 177 | AccessGroupKey = attrKey(C.CFTypeRef(C.kSecAttrAccessGroup)) 178 | // DataKey is for kSecValueData 179 | DataKey = attrKey(C.CFTypeRef(C.kSecValueData)) 180 | // DescriptionKey is for kSecAttrDescription 181 | DescriptionKey = attrKey(C.CFTypeRef(C.kSecAttrDescription)) 182 | // CommentKey is for kSecAttrComment 183 | CommentKey = attrKey(C.CFTypeRef(C.kSecAttrComment)) 184 | // CreationDateKey is for kSecAttrCreationDate 185 | CreationDateKey = attrKey(C.CFTypeRef(C.kSecAttrCreationDate)) 186 | // ModificationDateKey is for kSecAttrModificationDate 187 | ModificationDateKey = attrKey(C.CFTypeRef(C.kSecAttrModificationDate)) 188 | ) 189 | 190 | // Synchronizable is the items synchronizable status 191 | type Synchronizable int 192 | 193 | const ( 194 | // SynchronizableDefault is the default setting 195 | SynchronizableDefault Synchronizable = 0 196 | // SynchronizableAny is for kSecAttrSynchronizableAny 197 | SynchronizableAny = 1 198 | // SynchronizableYes enables synchronization 199 | SynchronizableYes = 2 200 | // SynchronizableNo disables synchronization 201 | SynchronizableNo = 3 202 | ) 203 | 204 | // SynchronizableKey is the key type for Synchronizable 205 | var SynchronizableKey = attrKey(C.CFTypeRef(C.kSecAttrSynchronizable)) 206 | var syncTypeRef = map[Synchronizable]C.CFTypeRef{ 207 | SynchronizableAny: C.CFTypeRef(C.kSecAttrSynchronizableAny), 208 | SynchronizableYes: C.CFTypeRef(C.kCFBooleanTrue), 209 | SynchronizableNo: C.CFTypeRef(C.kCFBooleanFalse), 210 | } 211 | 212 | var DataProtectionKey = attrKey(C.CFTypeRef(C.kSecUseDataProtectionKeychain)) 213 | var dataProtectionYes = C.CFTypeRef(C.kCFBooleanTrue) 214 | 215 | // Accessible is the items accessibility 216 | type Accessible int 217 | 218 | const ( 219 | // AccessibleDefault is the default 220 | AccessibleDefault Accessible = 0 221 | // AccessibleWhenUnlocked is when unlocked 222 | AccessibleWhenUnlocked = 1 223 | // AccessibleAfterFirstUnlock is after first unlock 224 | AccessibleAfterFirstUnlock = 2 225 | // AccessibleAlways is always 226 | AccessibleAlways = 3 227 | // AccessibleWhenPasscodeSetThisDeviceOnly is when passcode is set 228 | AccessibleWhenPasscodeSetThisDeviceOnly = 4 229 | // AccessibleWhenUnlockedThisDeviceOnly is when unlocked for this device only 230 | AccessibleWhenUnlockedThisDeviceOnly = 5 231 | // AccessibleAfterFirstUnlockThisDeviceOnly is after first unlock for this device only 232 | AccessibleAfterFirstUnlockThisDeviceOnly = 6 233 | // AccessibleAccessibleAlwaysThisDeviceOnly is always for this device only 234 | AccessibleAccessibleAlwaysThisDeviceOnly = 7 235 | ) 236 | 237 | // MatchLimit is whether to limit results on query 238 | type MatchLimit int 239 | 240 | const ( 241 | // MatchLimitDefault is the default 242 | MatchLimitDefault MatchLimit = 0 243 | // MatchLimitOne limits to one result 244 | MatchLimitOne = 1 245 | // MatchLimitAll is no limit 246 | MatchLimitAll = 2 247 | ) 248 | 249 | // MatchLimitKey is key type for MatchLimit 250 | var MatchLimitKey = attrKey(C.CFTypeRef(C.kSecMatchLimit)) 251 | var matchTypeRef = map[MatchLimit]C.CFTypeRef{ 252 | MatchLimitOne: C.CFTypeRef(C.kSecMatchLimitOne), 253 | MatchLimitAll: C.CFTypeRef(C.kSecMatchLimitAll), 254 | } 255 | 256 | // ReturnAttributesKey is key type for kSecReturnAttributes 257 | var ReturnAttributesKey = attrKey(C.CFTypeRef(C.kSecReturnAttributes)) 258 | 259 | // ReturnDataKey is key type for kSecReturnData 260 | var ReturnDataKey = attrKey(C.CFTypeRef(C.kSecReturnData)) 261 | 262 | // ReturnRefKey is key type for kSecReturnRef 263 | var ReturnRefKey = attrKey(C.CFTypeRef(C.kSecReturnRef)) 264 | 265 | // Item for adding, querying or deleting. 266 | type Item struct { 267 | // Values can be string, []byte, Convertable or CFTypeRef (constant). 268 | attr map[string]interface{} 269 | } 270 | 271 | // SetSecClass sets the security class 272 | func (k *Item) SetSecClass(sc SecClass) { 273 | k.attr[SecClassKey] = secClassTypeRef[sc] 274 | } 275 | 276 | // SetInt32 sets an int32 attribute for a string key 277 | func (k *Item) SetInt32(key string, v int32) { 278 | if v != 0 { 279 | k.attr[key] = v 280 | } else { 281 | delete(k.attr, key) 282 | } 283 | } 284 | 285 | // SetString sets a string attibute for a string key 286 | func (k *Item) SetString(key string, s string) { 287 | if s != "" { 288 | k.attr[key] = s 289 | } else { 290 | delete(k.attr, key) 291 | } 292 | } 293 | 294 | // SetService sets the service attribute (for generic application items) 295 | func (k *Item) SetService(s string) { 296 | k.SetString(ServiceKey, s) 297 | } 298 | 299 | // SetServer sets the server attribute (for internet password items) 300 | func (k *Item) SetServer(s string) { 301 | k.SetString(ServerKey, s) 302 | } 303 | 304 | // SetProtocol sets the protocol attribute (for internet password items) 305 | // Example values are: "htps", "http", "smb " 306 | func (k *Item) SetProtocol(s string) { 307 | k.SetString(ProtocolKey, s) 308 | } 309 | 310 | // SetAuthenticationType sets the authentication type attribute (for internet password items) 311 | func (k *Item) SetAuthenticationType(s string) { 312 | k.SetString(AuthenticationTypeKey, s) 313 | } 314 | 315 | // SetPort sets the port attribute (for internet password items) 316 | func (k *Item) SetPort(v int32) { 317 | k.SetInt32(PortKey, v) 318 | } 319 | 320 | // SetPath sets the path attribute (for internet password items) 321 | func (k *Item) SetPath(s string) { 322 | k.SetString(PathKey, s) 323 | } 324 | 325 | // SetAccount sets the account attribute 326 | func (k *Item) SetAccount(a string) { 327 | k.SetString(AccountKey, a) 328 | } 329 | 330 | // SetLabel sets the label attribute 331 | func (k *Item) SetLabel(l string) { 332 | k.SetString(LabelKey, l) 333 | } 334 | 335 | // SetDescription sets the description attribute 336 | func (k *Item) SetDescription(s string) { 337 | k.SetString(DescriptionKey, s) 338 | } 339 | 340 | // SetComment sets the comment attribute 341 | func (k *Item) SetComment(s string) { 342 | k.SetString(CommentKey, s) 343 | } 344 | 345 | // SetData sets the data attribute 346 | func (k *Item) SetData(b []byte) { 347 | if b != nil { 348 | k.attr[DataKey] = b 349 | } else { 350 | delete(k.attr, DataKey) 351 | } 352 | } 353 | 354 | // SetAccessGroup sets the access group attribute 355 | func (k *Item) SetAccessGroup(ag string) { 356 | k.SetString(AccessGroupKey, ag) 357 | } 358 | 359 | // SetSynchronizable sets the synchronizable attribute 360 | func (k *Item) SetSynchronizable(sync Synchronizable) { 361 | if sync != SynchronizableDefault { 362 | k.attr[SynchronizableKey] = syncTypeRef[sync] 363 | } else { 364 | delete(k.attr, SynchronizableKey) 365 | } 366 | } 367 | 368 | func (key *Item) SetDataProtection() { 369 | key.attr[DataProtectionKey] = dataProtectionYes 370 | } 371 | 372 | // SetAccessible sets the accessible attribute 373 | func (k *Item) SetAccessible(accessible Accessible) { 374 | if accessible != AccessibleDefault { 375 | k.attr[AccessibleKey] = accessibleTypeRef[accessible] 376 | } else { 377 | delete(k.attr, AccessibleKey) 378 | } 379 | } 380 | 381 | // SetMatchLimit sets the match limit 382 | func (k *Item) SetMatchLimit(matchLimit MatchLimit) { 383 | if matchLimit != MatchLimitDefault { 384 | k.attr[MatchLimitKey] = matchTypeRef[matchLimit] 385 | } else { 386 | delete(k.attr, MatchLimitKey) 387 | } 388 | } 389 | 390 | // SetReturnAttributes sets the return value type on query 391 | func (k *Item) SetReturnAttributes(b bool) { 392 | k.attr[ReturnAttributesKey] = b 393 | } 394 | 395 | // SetReturnData enables returning data on query 396 | func (k *Item) SetReturnData(b bool) { 397 | k.attr[ReturnDataKey] = b 398 | } 399 | 400 | // SetReturnRef enables returning references on query 401 | func (k *Item) SetReturnRef(b bool) { 402 | k.attr[ReturnRefKey] = b 403 | } 404 | 405 | // NewItem is a new empty keychain item 406 | func NewItem() Item { 407 | return Item{make(map[string]interface{})} 408 | } 409 | 410 | // NewGenericPassword creates a generic password item with the default keychain. This is a convenience method. 411 | func NewGenericPassword(service string, account string, label string, data []byte, accessGroup string) Item { 412 | item := NewItem() 413 | item.SetSecClass(SecClassGenericPassword) 414 | item.SetService(service) 415 | item.SetAccount(account) 416 | item.SetLabel(label) 417 | item.SetData(data) 418 | item.SetAccessGroup(accessGroup) 419 | return item 420 | } 421 | 422 | // AddItem adds a Item to a Keychain 423 | func AddItem(item Item) error { 424 | cfDict, err := ConvertMapToCFDictionary(item.attr) 425 | if err != nil { 426 | return err 427 | } 428 | defer Release(C.CFTypeRef(cfDict)) 429 | 430 | errCode := C.SecItemAdd(cfDict, nil) 431 | err = checkError(errCode) 432 | return err 433 | } 434 | 435 | // UpdateItem updates the queryItem with the parameters from updateItem 436 | func UpdateItem(queryItem Item, updateItem Item) error { 437 | cfDict, err := ConvertMapToCFDictionary(queryItem.attr) 438 | if err != nil { 439 | return err 440 | } 441 | defer Release(C.CFTypeRef(cfDict)) 442 | cfDictUpdate, err := ConvertMapToCFDictionary(updateItem.attr) 443 | if err != nil { 444 | return err 445 | } 446 | defer Release(C.CFTypeRef(cfDictUpdate)) 447 | errCode := C.SecItemUpdate(cfDict, cfDictUpdate) 448 | err = checkError(errCode) 449 | return err 450 | } 451 | 452 | // QueryResult stores all possible results from queries. 453 | // Not all fields are applicable all the time. Results depend on query. 454 | type QueryResult struct { 455 | // For generic application items 456 | Service string 457 | 458 | // For internet password items 459 | Server string 460 | Protocol string 461 | AuthenticationType string 462 | Port int32 463 | Path string 464 | 465 | Account string 466 | AccessGroup string 467 | Label string 468 | Description string 469 | Comment string 470 | Data []byte 471 | CreationDate time.Time 472 | ModificationDate time.Time 473 | } 474 | 475 | // QueryItemRef returns query result as CFTypeRef. You must release it when you are done. 476 | func QueryItemRef(item Item) (C.CFTypeRef, error) { 477 | cfDict, err := ConvertMapToCFDictionary(item.attr) 478 | if err != nil { 479 | return 0, err 480 | } 481 | defer Release(C.CFTypeRef(cfDict)) 482 | 483 | var resultsRef C.CFTypeRef 484 | errCode := C.SecItemCopyMatching(cfDict, &resultsRef) //nolint 485 | if Error(errCode) == ErrorItemNotFound { 486 | return 0, nil 487 | } 488 | err = checkError(errCode) 489 | if err != nil { 490 | return 0, err 491 | } 492 | return resultsRef, nil 493 | } 494 | 495 | // QueryItem returns a list of query results. 496 | func QueryItem(item Item) ([]QueryResult, error) { 497 | resultsRef, err := QueryItemRef(item) 498 | if err != nil { 499 | return nil, err 500 | } 501 | if resultsRef == 0 { 502 | return nil, nil 503 | } 504 | defer Release(resultsRef) 505 | 506 | results := make([]QueryResult, 0, 1) 507 | 508 | typeID := C.CFGetTypeID(resultsRef) 509 | if typeID == C.CFArrayGetTypeID() { 510 | arr := CFArrayToArray(C.CFArrayRef(resultsRef)) 511 | for _, ref := range arr { 512 | elementTypeID := C.CFGetTypeID(ref) 513 | if elementTypeID == C.CFDictionaryGetTypeID() { 514 | item, err := convertResult(C.CFDictionaryRef(ref)) 515 | if err != nil { 516 | return nil, err 517 | } 518 | results = append(results, *item) 519 | } else { 520 | return nil, fmt.Errorf("invalid result type (If you SetReturnRef(true) you should use QueryItemRef directly)") 521 | } 522 | } 523 | } else if typeID == C.CFDictionaryGetTypeID() { 524 | item, err := convertResult(C.CFDictionaryRef(resultsRef)) 525 | if err != nil { 526 | return nil, err 527 | } 528 | results = append(results, *item) 529 | } else if typeID == C.CFDataGetTypeID() { 530 | b, err := CFDataToBytes(C.CFDataRef(resultsRef)) 531 | if err != nil { 532 | return nil, err 533 | } 534 | item := QueryResult{Data: b} 535 | results = append(results, item) 536 | } else { 537 | return nil, fmt.Errorf("Invalid result type: %s", CFTypeDescription(resultsRef)) 538 | } 539 | 540 | return results, nil 541 | } 542 | 543 | func attrKey(ref C.CFTypeRef) string { 544 | return CFStringToString(C.CFStringRef(ref)) 545 | } 546 | 547 | func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { 548 | m := CFDictionaryToMap(d) 549 | result := QueryResult{} 550 | for k, v := range m { 551 | switch attrKey(k) { 552 | case ServiceKey: 553 | result.Service = CFStringToString(C.CFStringRef(v)) 554 | case ServerKey: 555 | result.Server = CFStringToString(C.CFStringRef(v)) 556 | case ProtocolKey: 557 | result.Protocol = CFStringToString(C.CFStringRef(v)) 558 | case AuthenticationTypeKey: 559 | result.AuthenticationType = CFStringToString(C.CFStringRef(v)) 560 | case PortKey: 561 | val := CFNumberToInterface(C.CFNumberRef(v)) 562 | result.Port = val.(int32) 563 | case PathKey: 564 | result.Path = CFStringToString(C.CFStringRef(v)) 565 | case AccountKey: 566 | result.Account = CFStringToString(C.CFStringRef(v)) 567 | case AccessGroupKey: 568 | result.AccessGroup = CFStringToString(C.CFStringRef(v)) 569 | case LabelKey: 570 | result.Label = CFStringToString(C.CFStringRef(v)) 571 | case DescriptionKey: 572 | result.Description = CFStringToString(C.CFStringRef(v)) 573 | case CommentKey: 574 | result.Comment = CFStringToString(C.CFStringRef(v)) 575 | case DataKey: 576 | b, err := CFDataToBytes(C.CFDataRef(v)) 577 | if err != nil { 578 | return nil, err 579 | } 580 | result.Data = b 581 | case CreationDateKey: 582 | result.CreationDate = CFDateToTime(C.CFDateRef(v)) 583 | case ModificationDateKey: 584 | result.ModificationDate = CFDateToTime(C.CFDateRef(v)) 585 | // default: 586 | // fmt.Printf("Unhandled key in conversion: %v = %v\n", cfTypeValue(k), cfTypeValue(v)) 587 | } 588 | } 589 | return &result, nil 590 | } 591 | 592 | // DeleteGenericPasswordItem removes a generic password item. 593 | func DeleteGenericPasswordItem(service string, account string) error { 594 | item := NewItem() 595 | item.SetSecClass(SecClassGenericPassword) 596 | item.SetService(service) 597 | item.SetAccount(account) 598 | return DeleteItem(item) 599 | } 600 | 601 | // DeleteItem removes a Item 602 | func DeleteItem(item Item) error { 603 | cfDict, err := ConvertMapToCFDictionary(item.attr) 604 | if err != nil { 605 | return err 606 | } 607 | defer Release(C.CFTypeRef(cfDict)) 608 | 609 | errCode := C.SecItemDelete(cfDict) 610 | return checkError(errCode) 611 | } 612 | 613 | // GetAccountsForService is deprecated 614 | func GetAccountsForService(service string) ([]string, error) { 615 | return GetGenericPasswordAccounts(service) 616 | } 617 | 618 | // GetGenericPasswordAccounts returns generic password accounts for service. This is a convenience method. 619 | func GetGenericPasswordAccounts(service string) ([]string, error) { 620 | query := NewItem() 621 | query.SetSecClass(SecClassGenericPassword) 622 | query.SetService(service) 623 | query.SetMatchLimit(MatchLimitAll) 624 | query.SetReturnAttributes(true) 625 | results, err := QueryItem(query) 626 | if err != nil { 627 | return nil, err 628 | } 629 | 630 | accounts := make([]string, 0, len(results)) 631 | for _, r := range results { 632 | accounts = append(accounts, r.Account) 633 | } 634 | 635 | return accounts, nil 636 | } 637 | 638 | // GetGenericPassword returns password data for service and account. This is a convenience method. 639 | // If item is not found returns nil, nil. 640 | func GetGenericPassword(service string, account string, label string, accessGroup string) ([]byte, error) { 641 | query := NewItem() 642 | query.SetSecClass(SecClassGenericPassword) 643 | query.SetService(service) 644 | query.SetAccount(account) 645 | query.SetLabel(label) 646 | query.SetAccessGroup(accessGroup) 647 | query.SetMatchLimit(MatchLimitOne) 648 | query.SetReturnData(true) 649 | results, err := QueryItem(query) 650 | if err != nil { 651 | return nil, err 652 | } 653 | if len(results) > 1 { 654 | return nil, fmt.Errorf("Too many results") 655 | } 656 | if len(results) == 1 { 657 | return results[0].Data, nil 658 | } 659 | return nil, nil 660 | } 661 | --------------------------------------------------------------------------------