├── go.mod ├── appveyor.yml ├── LICENSE.md ├── .travis.yml ├── keytar_windows.go ├── keytar.go ├── keytar_test.go ├── keytar_darwin.go ├── README.md └── keytar_linux.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/havoc-io/go-keytar 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/danieljoos/wincred v1.0.2 7 | ) -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Versioning 2 | version: 1.0.0.{build} 3 | 4 | # Configuration 5 | # NOTE: Structure of MSYS2 installation outlined in this comment: 6 | # http://help.appveyor.com/discussions/suggestions/615-support-for-msys2#comment_37571757 7 | environment: 8 | GOPATH: C:\projects\go 9 | PROJECTPATH: C:\projects\go-keytar 10 | 11 | matrix: 12 | - GOROOT: C:\go-x86 13 | GOARCH: 386 14 | MINGWROOT: C:\msys64\mingw32 15 | - GOROOT: C:\go 16 | GOARCH: amd64 17 | MINGWROOT: C:\msys64\mingw64 18 | 19 | # Set up clone path 20 | clone_folder: C:\projects\go-keytar 21 | 22 | # Run build (primarily just here to stop the default Visual Studio build, since 23 | # go test will build anyway) 24 | build_script: 25 | - set PATH=%GOROOT%\bin;%MINGWROOT%\bin;%PATH% 26 | - cd %PROJECTPATH% 27 | - go build 28 | 29 | # Run tests 30 | test_script: 31 | - set PATH=%GOROOT%\bin;%MINGWROOT%\bin;%PATH% 32 | - cd %PROJECTPATH% 33 | - go test 34 | 35 | # Send notifications 36 | notifications: 37 | - provider: Email 38 | to: 39 | - jacob@havoc.io 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jacob Howard 2 | Based on code copyright (c) 2013 GitHub Inc. 3 | 4 | Both projects released under the MIT License: 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Set the project language 2 | language: go 3 | 4 | # Set test platforms 5 | os: 6 | - osx 7 | - linux 8 | 9 | # Try building both 32-bit and 64-bit architectures 10 | env: 11 | - GOARCH=386 12 | - GOARCH=amd64 13 | 14 | # Enable use of container-based infrastructure for Linux 15 | # NOTE: We'll probably have to disable this if we ever want to have a chance of 16 | # gnome-keyring-daemon running for testing, but since that doesn't seem possible 17 | # at the moment, it's better to at least enable the container-based 18 | # infrastructure so that builds can be tested faster 19 | sudo: false 20 | 21 | # Add additional required packages for Linux 22 | # NOTE: If we want to use gnome-keyring-daemon, we'll need to install it 23 | # eventually, but it's blacklisted on Travis CI at the moment anyway. We might 24 | # also need to install xvfb and wrap our scripts in xvfb-run, it's not clear 25 | # because node-keytar has yet to figure it out. 26 | addons: 27 | apt: 28 | packages: 29 | - libgnome-keyring-dev 30 | 31 | # Run build and tests 32 | # TODO: Run tests for Linux on Travis CI once we can figure out how to talk to 33 | # gnome-keyring-daemon. node-keytar hasn't figured it out either. I think that 34 | # the daemon needs to be run before login to be able to unlock our login 35 | # keychain, but I don't see how that's possible with Travis CI. For now, we can 36 | # at least check that things compile correctly. 37 | script: 38 | - if [ "$TRAVIS_OS_NAME" = "osx" ]; then go test; else go build; fi 39 | 40 | # Send notifications 41 | notifications: 42 | email: 43 | - jacob@havoc.io 44 | -------------------------------------------------------------------------------- /keytar_windows.go: -------------------------------------------------------------------------------- 1 | package keytar 2 | 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/danieljoos/wincred" 8 | ) 9 | 10 | // Utility function to format service/account into something Windows can store 11 | // AND query. Credentials actually have a username field, but you can't query 12 | // on it, so it wouldn't allow us to store multiple credentials for the same 13 | // service. 14 | func targetFormat(service, account string) string { 15 | return fmt.Sprintf("%s@%s", account, service) 16 | } 17 | 18 | // keychainWindows implements the Keychain interface on Windows by using the 19 | // Credential Vault infrastructure to store items. 20 | type keychainWindows struct{} 21 | 22 | func (*keychainWindows) AddPassword(service, account, password string) error { 23 | // Validate input 24 | serviceValid := isValidNonNullUTF8(service) 25 | accountValid := isValidNonNullUTF8(account) 26 | passwordValid := isValidNonNullUTF8(password) 27 | if !(serviceValid && accountValid && passwordValid) { 28 | return ErrInvalidValue 29 | } 30 | 31 | cred := wincred.NewGenericCredential(targetFormat(service, account)) 32 | cred.CredentialBlob = []byte(password) 33 | return cred.Write() 34 | } 35 | 36 | func (*keychainWindows) GetPassword(service, account string) (string, error) { 37 | // Validate input 38 | serviceValid := isValidNonNullUTF8(service) 39 | accountValid := isValidNonNullUTF8(account) 40 | if !(serviceValid && accountValid) { 41 | return "", ErrInvalidValue 42 | } 43 | 44 | cred, err := wincred.GetGenericCredential(targetFormat(service, account)) 45 | 46 | if err != nil { 47 | return "", err 48 | } 49 | 50 | return string(cred.CredentialBlob), nil 51 | } 52 | 53 | func (*keychainWindows) DeletePassword(service, account string) error { 54 | // Validate input 55 | serviceValid := isValidNonNullUTF8(service) 56 | accountValid := isValidNonNullUTF8(account) 57 | if !(serviceValid && accountValid) { 58 | return ErrInvalidValue 59 | } 60 | 61 | cred, err := wincred.GetGenericCredential(targetFormat(service, account)) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return cred.Delete() 67 | } 68 | 69 | func init() { 70 | // Register the Windows keychain implementation 71 | keychain = &keychainWindows{} 72 | } 73 | -------------------------------------------------------------------------------- /keytar.go: -------------------------------------------------------------------------------- 1 | // Package keytar provides an interface for manipulating credentials in a user's 2 | // keychain. 3 | package keytar 4 | 5 | import ( 6 | // System imports 7 | "errors" 8 | "unicode/utf8" 9 | ) 10 | 11 | // Error definitions 12 | var ( 13 | ErrUnsupported = errors.New("operation unsupported on this platform") 14 | ErrUnknown = errors.New("unknown keychain failure") 15 | ErrNotFound = errors.New("keychain entry not found") 16 | ErrInvalidValue = errors.New("an invalid value was provided") 17 | ) 18 | 19 | // isValidNonNullUTF8 validates a string as UTF-8 with no null bytes. 20 | func isValidNonNullUTF8(s string) bool { 21 | // Check that this is valid UTF-8 22 | if !utf8.ValidString(s) { 23 | return false 24 | } 25 | 26 | // Check that there are no null-bytes (which are allowed by UTF-8) 27 | for i := 0; i < len(s); i++ { 28 | if s[i] == 0 { 29 | return false 30 | } 31 | } 32 | 33 | // All done 34 | return true 35 | } 36 | 37 | // Keychain is the primary interface through which programs interact with the 38 | // system keychain. All strings passed to this interface must be encoded in 39 | // UTF-8. GetPassword MAY return a value which is not UTF-8 encoded if the 40 | /// original keychain entry as created by another service which stored the 41 | // password in a non-UTF-8 encoding. 42 | type Keychain interface { 43 | AddPassword(service, account, password string) error 44 | GetPassword(service, account string) (string, error) 45 | DeletePassword(service, account string) error 46 | } 47 | 48 | // Global keychain instance 49 | var keychain Keychain = nil 50 | 51 | // ReplacePassword replaces a password in a keychain by deleting the original, 52 | // if it exists, and inserting the new value. It is merely a convenience 53 | // function, built on the Keychain interface. 54 | // NOTE: It'd be nice if this common implementation could be baked into the 55 | // Keychain interface, but such is Go. We can't even add this to a common 56 | // embedded base, because it requires access to the other Keychain methods. 57 | func ReplacePassword(k Keychain, service, account, newPassword string) error { 58 | // Delete the password. We ignore errors, because the password may not 59 | // exist. Unfortunately, not every platform returns enough information via 60 | // its delete call to determine the reason for failure, so we can't check 61 | // that errors were ErrNotFound, but if there's a more serious problem, 62 | // AddPassword should pick it up. 63 | k.DeletePassword(service, account) 64 | 65 | // Add the new password 66 | return k.AddPassword(service, account, newPassword) 67 | } 68 | 69 | // GetKeychain gets the keychain instance for the platform, which might be nil 70 | // if the platform is unsupported (in which case ErrUnsupported will be 71 | // returned). 72 | func GetKeychain() (Keychain, error) { 73 | // Check if a global keychain has been registered 74 | if keychain != nil { 75 | return keychain, nil 76 | } 77 | 78 | // If not, it's not supported 79 | return nil, ErrUnsupported 80 | } 81 | -------------------------------------------------------------------------------- /keytar_test.go: -------------------------------------------------------------------------------- 1 | package keytar 2 | 3 | import "testing" 4 | 5 | // Testing constants 6 | const ( 7 | NonExistentService = "keytar-test-bad-service" 8 | NonExistentAccount = "keytar-test-bad-account" 9 | Service = "keytar-test-service-世界.example.org" 10 | Account = "keytar-世界" 11 | Password = "$uP3RSecre7世界" 12 | AlternatePassword = "GeorgeWashington" 13 | ) 14 | 15 | // Test invalid UTF-8 and null byte detection 16 | func TestUTF8Validation(t *testing.T) { 17 | // Check an invalid string 18 | if isValidNonNullUTF8("\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98") { 19 | t.Error("invalid UTF-8 string passed validation") 20 | } 21 | 22 | // Check a valid string with null bytes 23 | if isValidNonNullUTF8("世界\x00Jacob") { 24 | t.Error("UTF-8 string with null byte passed validation") 25 | } 26 | } 27 | 28 | // Test that a non-existent lookup fails 29 | func TestNonExistentGet(t *testing.T) { 30 | // Create a keychain 31 | keychain, err := GetKeychain() 32 | if err != nil { 33 | t.Fatal("unable to create keychain") 34 | } 35 | 36 | // Test that a non-existent lookup fail 37 | password, err := keychain.GetPassword( 38 | NonExistentService, 39 | NonExistentAccount, 40 | ) 41 | if password != "" || err == nil { 42 | t.Error("retrieval of non-existent service/account password succeeded ") 43 | } 44 | } 45 | 46 | func TestNonExistentReplace(t *testing.T) { 47 | // Create a keychain 48 | keychain, err := GetKeychain() 49 | if err != nil { 50 | t.Fatal("unable to create keychain") 51 | } 52 | 53 | // Replace the password 54 | err = ReplacePassword( 55 | keychain, 56 | NonExistentService, 57 | NonExistentAccount, 58 | AlternatePassword, 59 | ) 60 | if err != nil { 61 | t.Error("replacement of non-existent password failed") 62 | } 63 | 64 | // Get/verify the alternate password 65 | password, err := keychain.GetPassword( 66 | NonExistentService, 67 | NonExistentAccount, 68 | ) 69 | if err != nil { 70 | t.Error("password retrieval failed") 71 | } 72 | if password != AlternatePassword { 73 | t.Error("password mismatch") 74 | } 75 | 76 | // Delete it 77 | err = keychain.DeletePassword(NonExistentService, NonExistentAccount) 78 | if err != nil { 79 | t.Error("password deletion failed") 80 | } 81 | } 82 | 83 | // Make sure the standard password lifecycle works 84 | func TestLifecycle(t *testing.T) { 85 | // Create a keychain 86 | keychain, err := GetKeychain() 87 | if err != nil { 88 | t.Fatal("unable to create keychain") 89 | } 90 | 91 | // Add a password 92 | err = keychain.AddPassword(Service, Account, Password) 93 | if err != nil { 94 | t.Error("password addition failed") 95 | } 96 | 97 | // Get/verify the password 98 | password, err := keychain.GetPassword(Service, Account) 99 | if err != nil { 100 | t.Error("password retrieval failed") 101 | } 102 | if password != Password { 103 | t.Error("password mismatch") 104 | } 105 | 106 | // Replace the password 107 | err = ReplacePassword(keychain, Service, Account, AlternatePassword) 108 | if err != nil { 109 | t.Error("password replacement failed") 110 | } 111 | 112 | // Get/verify the alternate password 113 | password, err = keychain.GetPassword(Service, Account) 114 | if err != nil { 115 | t.Error("password retrieval failed") 116 | } 117 | if password != AlternatePassword { 118 | t.Error("password mismatch") 119 | } 120 | 121 | // Delete the password 122 | err = keychain.DeletePassword(Service, Account) 123 | if err != nil { 124 | t.Error("password deletion failed") 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /keytar_darwin.go: -------------------------------------------------------------------------------- 1 | package keytar 2 | 3 | // #cgo LDFLAGS: -framework CoreFoundation -framework Security 4 | // #include 5 | // #include 6 | // #include 7 | import "C" 8 | 9 | import ( 10 | // System imports 11 | "unsafe" 12 | ) 13 | 14 | const ( 15 | // nullSecKeychainRef is a NULL SecKeychainRef value. 16 | nullSecKeychainRef C.SecKeychainRef = 0 17 | // nullCFTypeRef is a NULL CFTypeRef value. 18 | nullCFTypeRef C.CFTypeRef = 0 19 | ) 20 | 21 | // keychainOSX implements the Keychain interface on OS X by using the Security 22 | // framework to store items in the user's login keychain. 23 | type keychainOSX struct{} 24 | 25 | func (*keychainOSX) AddPassword(service, account, password string) error { 26 | // Validate input 27 | serviceValid := isValidNonNullUTF8(service) 28 | accountValid := isValidNonNullUTF8(account) 29 | passwordValid := isValidNonNullUTF8(password) 30 | if !(serviceValid && accountValid && passwordValid) { 31 | return ErrInvalidValue 32 | } 33 | 34 | // Convert values to C strings 35 | serviceCStr := C.CString(service) 36 | defer C.free(unsafe.Pointer(serviceCStr)) 37 | accountCStr := C.CString(account) 38 | defer C.free(unsafe.Pointer(accountCStr)) 39 | passwordBlob := unsafe.Pointer(C.CString(password)) 40 | defer C.free(passwordBlob) 41 | 42 | // Try to add the password 43 | status := C.SecKeychainAddGenericPassword( 44 | nullSecKeychainRef, 45 | C.UInt32(len(service)), 46 | serviceCStr, 47 | C.UInt32(len(account)), 48 | accountCStr, 49 | C.UInt32(len(password)), 50 | passwordBlob, 51 | nil, 52 | ) 53 | 54 | // Check for errors 55 | if status != C.errSecSuccess { 56 | return ErrUnknown 57 | } 58 | 59 | // All done 60 | return nil 61 | } 62 | 63 | func (*keychainOSX) GetPassword(service, account string) (string, error) { 64 | // Validate input 65 | serviceValid := isValidNonNullUTF8(service) 66 | accountValid := isValidNonNullUTF8(account) 67 | if !(serviceValid && accountValid) { 68 | return "", ErrInvalidValue 69 | } 70 | 71 | // Convert values to C strings 72 | serviceCStr := C.CString(service) 73 | defer C.free(unsafe.Pointer(serviceCStr)) 74 | accountCStr := C.CString(account) 75 | defer C.free(unsafe.Pointer(accountCStr)) 76 | 77 | // Look for a match 78 | var passwordData unsafe.Pointer 79 | var passwordDataLength C.UInt32 80 | status := C.SecKeychainFindGenericPassword( 81 | nullCFTypeRef, 82 | C.UInt32(len(service)), 83 | serviceCStr, 84 | C.UInt32(len(account)), 85 | accountCStr, 86 | &passwordDataLength, 87 | &passwordData, 88 | nil, 89 | ) 90 | 91 | // Check for errors 92 | if status != C.errSecSuccess { 93 | return "", ErrNotFound 94 | } 95 | 96 | // Create the result 97 | result := C.GoStringN((*C.char)(passwordData), C.int(passwordDataLength)) 98 | 99 | // Cleanup the temporary buffer 100 | C.SecKeychainItemFreeContent(nil, passwordData) 101 | 102 | // All done 103 | return result, nil 104 | } 105 | 106 | func (*keychainOSX) DeletePassword(service, account string) error { 107 | // Validate input 108 | serviceValid := isValidNonNullUTF8(service) 109 | accountValid := isValidNonNullUTF8(account) 110 | if !(serviceValid && accountValid) { 111 | return ErrInvalidValue 112 | } 113 | 114 | // Convert values to C strings 115 | serviceCStr := C.CString(service) 116 | defer C.free(unsafe.Pointer(serviceCStr)) 117 | accountCStr := C.CString(account) 118 | defer C.free(unsafe.Pointer(accountCStr)) 119 | 120 | // Grab the item 121 | var item C.SecKeychainItemRef 122 | status := C.SecKeychainFindGenericPassword( 123 | nullCFTypeRef, 124 | C.UInt32(len(service)), 125 | serviceCStr, 126 | C.UInt32(len(account)), 127 | accountCStr, 128 | nil, 129 | nil, 130 | &item, 131 | ) 132 | 133 | // Check for errors 134 | if status != C.errSecSuccess { 135 | return ErrNotFound 136 | } 137 | 138 | // Delete the item 139 | status = C.SecKeychainItemDelete(item) 140 | 141 | // Free the item 142 | C.CFRelease(C.CFTypeRef(item)) 143 | 144 | // Check for errors 145 | if status != C.errSecSuccess { 146 | return ErrUnknown 147 | } 148 | 149 | // All done 150 | return nil 151 | } 152 | 153 | func init() { 154 | // Register the OS X keychain implementation 155 | keychain = &keychainOSX{} 156 | } 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-keytar 2 | 3 | Cross-platform system keychain access library for Go. 4 | 5 | This package is largely based on the 6 | [node-keytar](https://github.com/atom/node-keytar) package, though the GNOME 7 | Keyring implementation has been modified to work on older GNOME versions that 8 | don't provide the simple password storage API. 9 | 10 | This package is designed to add, get, replace, and delete passwords in the 11 | user's default keychain. On OS X, passwords are managed by the Keychain. On 12 | Linux, passwords are managed by GNOME Keyring. On Windows, passwords are 13 | managed by Credential Vault. 14 | 15 | 16 | ## Status 17 | 18 | The module is currently tested1 on the following platforms: 19 | 20 | | Windows | OS X/Linux | 21 | | :-------------------------------: | :------------------------------------: | 22 | | [![Windows][win-badge]][win-link] | [![OS X][osx-lin-badge]][osx-lin-link] | 23 | 24 | [win-badge]: https://ci.appveyor.com/api/projects/status/aqx64o6ee39ago5o/branch/master?svg=true "AppVeyor build status" 25 | [win-link]: https://ci.appveyor.com/project/havoc-io/go-keytar/branch/master "AppVeyor build status" 26 | [osx-lin-badge]: https://travis-ci.org/havoc-io/go-keytar.svg?branch=master "Travis CI build status" 27 | [osx-lin-link]: https://travis-ci.org/havoc-io/go-keytar "Travis CI build status" 28 | 29 | 30 | 1: Sadly, the gnome-keyring-daemon does not work on Travis CI, so, while the 31 | library and tests are built on Linux, the tests are not actually run. If you 32 | want to execute the tests, you'll have to build and run them locally :cry:. 33 | You'll probably have a lot better luck if you do this in a GNOME session. 34 | 35 | 36 | 37 | ## Dependencies 38 | 39 | On each platform, you'll need a Go installation that supports cgo compilation. 40 | On Windows, this means that you'll need MinGW-w64, because MinGW doesn't support 41 | the Windows Credential Vault API and, even if it did, it doesn't support 64-bit 42 | compilation. On other platforms, Go should just use the system compiler for cgo 43 | compilation. 44 | 45 | All library dependencies are met by the system on Windows and OS X. 46 | 47 | On Linux, you need to ensure that the GNOME Keyring development package is 48 | installed. On Ubuntu systems, do: 49 | 50 | sudo apt-get install libgnome-keyring-dev 51 | 52 | On Red Hat systems, do: 53 | 54 | sudo yum install gnome-keyring-devel 55 | 56 | For all other Linux distributions, consult your package manager. 57 | 58 | 59 | ## Usage 60 | 61 | The interface to the platform's default keychain is provided by the `Keychain` 62 | interface. To get the appropriate `Keychain` instance for the current platform, 63 | do: 64 | 65 | keychain, err := keytar.GetKeychain() 66 | if err != nil { 67 | // Handle error (most likely ErrUnsupported) 68 | } 69 | 70 | Then you can add a password: 71 | 72 | // NOTE: AddPassword will not overwrite a password - use 73 | // keytar.ReplacePassword for that 74 | err = keychain.AddPassword("example.org", "George", "$eCr37") 75 | if err != nil { 76 | // Handle error 77 | } 78 | 79 | Query a password: 80 | 81 | password, err := keychain.GetPassword("example.org", "George") 82 | if err != nil { 83 | // Handle error 84 | } 85 | // Use password 86 | 87 | Replace a password: 88 | 89 | // NOTE: This is a module-level function, not part of the keychain interface 90 | err = keytar.ReplacePassword( 91 | keychain, 92 | "example.org", 93 | "George", 94 | "M0r3-$eCr37", 95 | ) 96 | if err != nil { 97 | // Handle error 98 | } 99 | 100 | Or delete a password: 101 | 102 | err = keytar.DeletePassword("example.org", "George") 103 | if err != nil { 104 | // Handle error (you can probably ignore keytar.ErrNotFound) 105 | } 106 | 107 | That's it. 108 | 109 | Note that all strings passed to the interface must be UTF-8 encoded without any 110 | null bytes. The `GetPassword` method may return a non-UTF-8 string if the entry 111 | was created by another program not enforcing this constraint. 112 | 113 | 114 | ## TODO list 115 | 116 | - Create GoDoc entry. 117 | - Move Linux convenience C code out of the Go source file (it's a bit long), or, 118 | even better, switch to a more modern keychain system on Linux, like libsecret. 119 | - Make APIs try to extract more concise error information from the underlying 120 | platform APIs. At the moment, many failures are classified as `ErrUnknown`, 121 | but we could probably figure out the real error and expand our list of error 122 | codes. 123 | - Figure out if Go has a secure fallback that we could use somewhere in its 124 | crypto libraries. 125 | 126 | ## Contributors 127 | 128 | - Jacob Howard (@havoc-io) 129 | - Jeffrey Hulten (@jhulten) 130 | -------------------------------------------------------------------------------- /keytar_linux.go: -------------------------------------------------------------------------------- 1 | package keytar 2 | 3 | /* 4 | #cgo pkg-config: glib-2.0 gnome-keyring-1 5 | 6 | // Standard includes 7 | #include 8 | #include 9 | 10 | // GNOME includes 11 | #include 12 | #include 13 | 14 | // TODO: Eventually it'd be nice to switch to GNOME's simple password storage, 15 | // but manually manipulating items allows us to work with older versions of 16 | // GNOME. The better option would be to simply switch to another library, 17 | // because gnome-keyring is deprecated and unreliable. 18 | 19 | // Generates an attribute structure for creating/searching based on the account 20 | // and service 21 | GnomeKeyringAttributeList * createAttributes( 22 | const char * service, 23 | const char * account 24 | ) { 25 | // Allocate the list 26 | GnomeKeyringAttributeList * result = gnome_keyring_attribute_list_new(); 27 | 28 | // Add the attributes 29 | gnome_keyring_attribute_list_append_string(result, "service", service); 30 | gnome_keyring_attribute_list_append_string(result, "account", account); 31 | 32 | // All done 33 | return result; 34 | } 35 | 36 | // Releases the attribute structure generated by createAttributes 37 | void freeAttributes(GnomeKeyringAttributeList * list) { 38 | gnome_keyring_attribute_list_free(list); 39 | } 40 | 41 | // Adds a password to the default keychain. All arguments must be UTF-8 encoded 42 | // and null-terminated. 43 | int addPassword( 44 | const char * displayName, 45 | const char * service, 46 | const char * account, 47 | const char * password 48 | ) { 49 | // Create the item attributes 50 | GnomeKeyringAttributeList * attributes = createAttributes( 51 | service, 52 | account 53 | ); 54 | 55 | // Create the item 56 | guint32 item = 0; 57 | GnomeKeyringResult result = gnome_keyring_item_create_sync( 58 | NULL, 59 | GNOME_KEYRING_ITEM_GENERIC_SECRET, 60 | displayName, 61 | attributes, 62 | password, 63 | FALSE, 64 | &item 65 | ); 66 | 67 | // Release attributes 68 | freeAttributes(attributes); 69 | 70 | // Check the result 71 | if (result != GNOME_KEYRING_RESULT_OK) { 72 | return -1; 73 | } 74 | 75 | // All done 76 | return 0; 77 | } 78 | 79 | // Gets a password from the default keychain. All arguments must be UTF-8 80 | // encoded and null-terminated. On success, the password argument will be set 81 | // to a null-terminated string that must be released with free. 82 | int getPassword(const char * service, const char * account, char ** password) { 83 | // Create the item attributes 84 | GnomeKeyringAttributeList * attributes = createAttributes( 85 | service, 86 | account 87 | ); 88 | 89 | // Find the item 90 | GList * matches = NULL; 91 | GnomeKeyringResult result = gnome_keyring_find_items_sync( 92 | GNOME_KEYRING_ITEM_GENERIC_SECRET, 93 | attributes, 94 | &matches 95 | ); 96 | 97 | // Release attributes 98 | freeAttributes(attributes); 99 | 100 | // Check the results 101 | if (result != GNOME_KEYRING_RESULT_OK || g_list_length(matches) == 0) { 102 | gnome_keyring_found_list_free(matches); 103 | *password == NULL; 104 | return -1; 105 | } 106 | 107 | // Grab the first result and extract the password 108 | const char * secret = ((GnomeKeyringFound *)(matches->data))->secret; 109 | *password = malloc(strlen(secret) + 1); 110 | strcpy(*password, secret); 111 | 112 | // Free the results 113 | gnome_keyring_found_list_free(matches); 114 | 115 | // All done 116 | return 0; 117 | } 118 | 119 | // Deletes a password from the default keychain. All arguments must be UTF-8 120 | // encoded and null-terminated. 121 | int deletePassword(const char * service, const char * account) { 122 | // Create the item attributes 123 | GnomeKeyringAttributeList * attributes = createAttributes( 124 | service, 125 | account 126 | ); 127 | 128 | // Find the item 129 | GList * matches = NULL; 130 | GnomeKeyringResult result = gnome_keyring_find_items_sync( 131 | GNOME_KEYRING_ITEM_GENERIC_SECRET, 132 | attributes, 133 | &matches 134 | ); 135 | 136 | // Release attributes 137 | freeAttributes(attributes); 138 | 139 | // Check the results 140 | if (result != GNOME_KEYRING_RESULT_OK || g_list_length(matches) == 0) { 141 | gnome_keyring_found_list_free(matches); 142 | return -1; 143 | } 144 | 145 | // Get the id of the first result 146 | guint item = ((GnomeKeyringFound *)(matches->data))->item_id; 147 | 148 | // Free the results 149 | gnome_keyring_found_list_free(matches); 150 | 151 | // Delete the item 152 | result = gnome_keyring_item_delete_sync(NULL, item); 153 | 154 | // Check the result 155 | if (result != GNOME_KEYRING_RESULT_OK) { 156 | return -1; 157 | } 158 | 159 | // All done 160 | return 0; 161 | } 162 | */ 163 | import "C" 164 | 165 | import ( 166 | // System imports 167 | "fmt" 168 | "unsafe" 169 | ) 170 | 171 | // keychainLinux implements the Keychain interface on Linux by using the 172 | // GNOME Keyring infrastructure to store items in the user's keyring. 173 | type keychainLinux struct{} 174 | 175 | func (*keychainLinux) AddPassword(service, account, password string) error { 176 | // Validate input 177 | serviceValid := isValidNonNullUTF8(service) 178 | accountValid := isValidNonNullUTF8(account) 179 | passwordValid := isValidNonNullUTF8(password) 180 | if !(serviceValid && accountValid && passwordValid) { 181 | return ErrInvalidValue 182 | } 183 | 184 | // Compute a display name and convert it to a C string 185 | display := fmt.Sprintf("%s@%s", service, account) 186 | 187 | // Convert values to C strings 188 | displayCStr := C.CString(display) 189 | defer C.free(unsafe.Pointer(displayCStr)) 190 | serviceCStr := C.CString(service) 191 | defer C.free(unsafe.Pointer(serviceCStr)) 192 | accountCStr := C.CString(account) 193 | defer C.free(unsafe.Pointer(accountCStr)) 194 | passwordCStr := C.CString(password) 195 | C.free(unsafe.Pointer(passwordCStr)) 196 | 197 | // Do the add and check for errors 198 | if C.addPassword(displayCStr, serviceCStr, accountCStr, passwordCStr) < 0 { 199 | return ErrUnknown 200 | } 201 | 202 | // All done 203 | return nil 204 | } 205 | 206 | func (*keychainLinux) GetPassword(service, account string) (string, error) { 207 | // Validate input 208 | serviceValid := isValidNonNullUTF8(service) 209 | accountValid := isValidNonNullUTF8(account) 210 | if !(serviceValid && accountValid) { 211 | return "", ErrInvalidValue 212 | } 213 | 214 | // Convert values to C strings 215 | serviceCStr := C.CString(service) 216 | defer C.free(unsafe.Pointer(serviceCStr)) 217 | accountCStr := C.CString(account) 218 | defer C.free(unsafe.Pointer(accountCStr)) 219 | 220 | // Get the password and check for errors 221 | var passwordCStr *C.char 222 | if C.getPassword(serviceCStr, accountCStr, &passwordCStr) < 0 { 223 | return "", ErrNotFound 224 | } 225 | 226 | // If there was a match, convert it and free the underlying C string 227 | password := C.GoString(passwordCStr) 228 | C.free(unsafe.Pointer(passwordCStr)) 229 | 230 | // All done 231 | return password, nil 232 | } 233 | 234 | func (*keychainLinux) DeletePassword(service, account string) error { 235 | // Validate input 236 | serviceValid := isValidNonNullUTF8(service) 237 | accountValid := isValidNonNullUTF8(account) 238 | if !(serviceValid && accountValid) { 239 | return ErrInvalidValue 240 | } 241 | 242 | // Convert values to C strings 243 | serviceCStr := C.CString(service) 244 | defer C.free(unsafe.Pointer(serviceCStr)) 245 | accountCStr := C.CString(account) 246 | defer C.free(unsafe.Pointer(accountCStr)) 247 | 248 | // Delete the password and check for errors 249 | if C.deletePassword(serviceCStr, accountCStr) < 0 { 250 | return ErrUnknown 251 | } 252 | 253 | // All done 254 | return nil 255 | } 256 | 257 | func init() { 258 | // Register the Linux keychain implementation if keychain services are 259 | // available 260 | if C.gnome_keyring_is_available() == C.TRUE { 261 | keychain = &keychainLinux{} 262 | } 263 | } 264 | --------------------------------------------------------------------------------