├── .catwatch.yml ├── MAINTAINERS ├── .zappr.yml ├── go.mod ├── CONTRIBUTING.md ├── SECURITY.md ├── .gitignore ├── keyring_fallback.go ├── go.sum ├── LICENSE ├── internal └── shellescape │ ├── LICENSE │ ├── shellescape.go │ └── shellescape_test.go ├── keyring.go ├── keyring_mock.go ├── .github └── workflows │ └── go.yml ├── keyring_windows.go ├── keyring_mock_test.go ├── keyring_darwin.go ├── keyring_unix.go ├── keyring_test.go ├── secret_service └── secret_service.go └── README.md /.catwatch.yml: -------------------------------------------------------------------------------- 1 | title: go-keyring 2 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Mikkel Oscar Lyderik Larsen 2 | Sandor Szücs 3 | -------------------------------------------------------------------------------- /.zappr.yml: -------------------------------------------------------------------------------- 1 | approvals: 2 | groups: 3 | zalando: 4 | minimum: 2 5 | from: 6 | orgs: 7 | - "zalando" 8 | X-Zalando-Team: teapot 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zalando/go-keyring 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/danieljoos/wincred v1.2.2 7 | github.com/godbus/dbus/v5 v5.1.0 8 | ) 9 | 10 | require golang.org/x/sys v0.26.0 // indirect 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Go keyring library 2 | 3 | Please open an issue for bugs and feature requests. We are open to Pull Requests, but we would like to discuss features with you. 4 | If you want to submit a PR, always use feature branches and let people discuss changes in pull requests. 5 | 6 | Pull requests should only be merged after all discussions have been concluded and at least 2 reviewers has given their 7 | **approval**. 8 | 9 | ## Guidelines 10 | 11 | - **every code change** should have a test 12 | - keep the current code style 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | We acknowledge that every line of code that we write may potentially contain security issues. 2 | We are trying to deal with it responsibly and provide patches as quickly as possible. 3 | 4 | We host our bug bounty program on HackerOne, it is currently private, therefore if you would like to report a vulnerability and get rewarded for it, please ask to join our program by filling this form: 5 | 6 | https://corporate.zalando.com/en/services-and-contact#security-form 7 | 8 | You can also send your report via this form if you do not want to join our bug bounty program and just want to report a vulnerability or security issue. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://github.com/github/gitignore 2 | 3 | ######################### Go ################################################### 4 | # https://raw.githubusercontent.com/github/gitignore/master/Go.gitignore 5 | ################################################################################ 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | vendor/ 21 | 22 | # Go workspace file 23 | go.work 24 | -------------------------------------------------------------------------------- /keyring_fallback.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "errors" 5 | "runtime" 6 | ) 7 | 8 | // All of the following methods error out on unsupported platforms 9 | var ErrUnsupportedPlatform = errors.New("unsupported platform: " + runtime.GOOS) 10 | 11 | type fallbackServiceProvider struct{} 12 | 13 | func (fallbackServiceProvider) Set(service, user, pass string) error { 14 | return ErrUnsupportedPlatform 15 | } 16 | 17 | func (fallbackServiceProvider) Get(service, user string) (string, error) { 18 | return "", ErrUnsupportedPlatform 19 | } 20 | 21 | func (fallbackServiceProvider) Delete(service, user string) error { 22 | return ErrUnsupportedPlatform 23 | } 24 | 25 | func (fallbackServiceProvider) DeleteAll(service string) error { 26 | return ErrUnsupportedPlatform 27 | } 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= 2 | github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 5 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 8 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 9 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 10 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Zalando SE 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 | -------------------------------------------------------------------------------- /internal/shellescape/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alessio Treglia 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 | -------------------------------------------------------------------------------- /internal/shellescape/shellescape.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package shellescape provides the shellescape.Quote to escape arbitrary 3 | strings for a safe use as command line arguments in the most common 4 | POSIX shells. 5 | 6 | The original Python package which this work was inspired by can be found 7 | at https://pypi.python.org/pypi/shellescape. 8 | 9 | Portions of this file are from al.essio.dev/pkg/shellescape, © 2016 Alessio Treglia under the MIT License. 10 | See LICENSE for more information. 11 | */ 12 | package shellescape 13 | 14 | /* 15 | The functionality provided by shellescape.Quote could be helpful 16 | in those cases where it is known that the output of a Go program will 17 | be appended to/used in the context of shell programs' command line arguments. 18 | */ 19 | 20 | import ( 21 | "regexp" 22 | "strings" 23 | ) 24 | 25 | var pattern *regexp.Regexp = regexp.MustCompile(`[^\w@%+=:,./-]`) 26 | 27 | // Quote returns a shell-escaped version of the string s. The returned value 28 | // is a string that can safely be used as one token in a shell command line. 29 | func Quote(s string) string { 30 | if len(s) == 0 { 31 | return "''" 32 | } 33 | 34 | if pattern.MatchString(s) { 35 | return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" 36 | } 37 | 38 | return s 39 | } 40 | -------------------------------------------------------------------------------- /internal/shellescape/shellescape_test.go: -------------------------------------------------------------------------------- 1 | package shellescape_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zalando/go-keyring/internal/shellescape" 7 | ) 8 | 9 | func assertEqual(t *testing.T, s, expected string) { 10 | if s != expected { 11 | t.Fatalf("%q (expected: %q)", s, expected) 12 | } 13 | } 14 | 15 | func TestEmptyString(t *testing.T) { 16 | s := shellescape.Quote("") 17 | expected := "''" 18 | if s != expected { 19 | t.Errorf("Expected escaped string %s, got: %s", expected, s) 20 | } 21 | } 22 | 23 | func TestDoubleQuotedString(t *testing.T) { 24 | s := shellescape.Quote(`"double quoted"`) 25 | expected := `'"double quoted"'` 26 | if s != expected { 27 | t.Errorf("Expected escaped string %s, got: %s", expected, s) 28 | } 29 | } 30 | 31 | func TestSingleQuotedString(t *testing.T) { 32 | s := shellescape.Quote(`'single quoted'`) 33 | expected := `''"'"'single quoted'"'"''` 34 | if s != expected { 35 | t.Errorf("Expected escaped string %s, got: %s", expected, s) 36 | } 37 | } 38 | 39 | func TestUnquotedString(t *testing.T) { 40 | s := shellescape.Quote(`no quotes`) 41 | expected := `'no quotes'` 42 | if s != expected { 43 | t.Errorf("Expected escaped string %s, got: %s", expected, s) 44 | } 45 | } 46 | 47 | func TestSingleInvalid(t *testing.T) { 48 | s := shellescape.Quote(`;`) 49 | expected := `';'` 50 | if s != expected { 51 | t.Errorf("Expected escaped string %s, got: %s", expected, s) 52 | } 53 | } 54 | 55 | func TestAllInvalid(t *testing.T) { 56 | s := shellescape.Quote(`;${}`) 57 | expected := `';${}'` 58 | if s != expected { 59 | t.Errorf("Expected escaped string %s, got: %s", expected, s) 60 | } 61 | } 62 | 63 | func TestCleanString(t *testing.T) { 64 | s := shellescape.Quote("foo.example.com") 65 | expected := `foo.example.com` 66 | if s != expected { 67 | t.Errorf("Expected escaped string %s, got: %s", expected, s) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /keyring.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import "errors" 4 | 5 | // provider set in the init function by the relevant os file e.g.: 6 | // keyring_unix.go 7 | var provider Keyring = fallbackServiceProvider{} 8 | 9 | var ( 10 | // ErrNotFound is the expected error if the secret isn't found in the 11 | // keyring. 12 | ErrNotFound = errors.New("secret not found in keyring") 13 | // ErrSetDataTooBig is returned if `Set` was called with too much data. 14 | // On MacOS: The combination of service, username & password should not exceed ~3000 bytes 15 | // On Windows: The service is limited to 32KiB while the password is limited to 2560 bytes 16 | // On Linux/Unix: There is no theoretical limit but performance suffers with big values (>100KiB) 17 | ErrSetDataTooBig = errors.New("data passed to Set was too big") 18 | ) 19 | 20 | // Keyring provides a simple set/get interface for a keyring service. 21 | type Keyring interface { 22 | // Set password in keyring for user. 23 | Set(service, user, password string) error 24 | // Get password from keyring given service and user name. 25 | Get(service, user string) (string, error) 26 | // Delete secret from keyring. 27 | Delete(service, user string) error 28 | // DeleteAll deletes all secrets for a given service 29 | DeleteAll(service string) error 30 | } 31 | 32 | // Set password in keyring for user. 33 | func Set(service, user, password string) error { 34 | return provider.Set(service, user, password) 35 | } 36 | 37 | // Get password from keyring given service and user name. 38 | func Get(service, user string) (string, error) { 39 | return provider.Get(service, user) 40 | } 41 | 42 | // Delete secret from keyring. 43 | func Delete(service, user string) error { 44 | return provider.Delete(service, user) 45 | } 46 | 47 | // DeleteAll deletes all secrets for a given service 48 | func DeleteAll(service string) error { 49 | return provider.DeleteAll(service) 50 | } 51 | -------------------------------------------------------------------------------- /keyring_mock.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | type mockProvider struct { 4 | mockStore map[string]map[string]string 5 | mockError error 6 | } 7 | 8 | // Set stores user and pass in the keyring under the defined service 9 | // name. 10 | func (m *mockProvider) Set(service, user, pass string) error { 11 | if m.mockError != nil { 12 | return m.mockError 13 | } 14 | if m.mockStore == nil { 15 | m.mockStore = make(map[string]map[string]string) 16 | } 17 | if m.mockStore[service] == nil { 18 | m.mockStore[service] = make(map[string]string) 19 | } 20 | m.mockStore[service][user] = pass 21 | return nil 22 | } 23 | 24 | // Get gets a secret from the keyring given a service name and a user. 25 | func (m *mockProvider) Get(service, user string) (string, error) { 26 | if m.mockError != nil { 27 | return "", m.mockError 28 | } 29 | if b, ok := m.mockStore[service]; ok { 30 | if v, ok := b[user]; ok { 31 | return v, nil 32 | } 33 | } 34 | return "", ErrNotFound 35 | } 36 | 37 | // Delete deletes a secret, identified by service & user, from the keyring. 38 | func (m *mockProvider) Delete(service, user string) error { 39 | if m.mockError != nil { 40 | return m.mockError 41 | } 42 | if m.mockStore != nil { 43 | if _, ok := m.mockStore[service]; ok { 44 | if _, ok := m.mockStore[service][user]; ok { 45 | delete(m.mockStore[service], user) 46 | return nil 47 | } 48 | } 49 | } 50 | return ErrNotFound 51 | } 52 | 53 | // DeleteAll deletes all secrets for a given service 54 | func (m *mockProvider) DeleteAll(service string) error { 55 | if m.mockError != nil { 56 | return m.mockError 57 | } 58 | delete(m.mockStore, service) 59 | return nil 60 | } 61 | 62 | // MockInit sets the provider to a mocked memory store 63 | func MockInit() { 64 | provider = &mockProvider{} 65 | } 66 | 67 | // MockInitWithError sets the provider to a mocked memory store 68 | // that returns the given error on all operations 69 | func MockInitWithError(err error) { 70 | provider = &mockProvider{mockError: err} 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build-linux: 11 | name: Build (ubuntu-latest) 12 | runs-on: ubuntu-latest 13 | container: 14 | image: ubuntu:latest 15 | options: --privileged 16 | env: 17 | DEBIAN_FRONTEND: noninteractive 18 | 19 | steps: 20 | - name: Check out code into $GITHUB_WORKSPACE directory 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: ^1.22 27 | id: go 28 | 29 | - name: Install Dependencies 30 | run: | 31 | apt-get update 32 | apt-get install -y gnome-keyring build-essential ca-certificates 33 | mkdir -p /github/home/.cache/ 34 | mkdir -p /github/home/.local/share/keyrings/ 35 | chmod 700 -R /github/home/.local/ 36 | 37 | - name: Get dependencies 38 | run: | 39 | go get -v -t -d ./... 40 | 41 | - name: Build 42 | run: | 43 | go build -v ./... 44 | 45 | - name: Test 46 | run: | 47 | echo 'somecredstorepass' | gnome-keyring-daemon --unlock 48 | go test -v ./... 49 | shell: dbus-run-session -- bash --noprofile --norc -eo pipefail {0} 50 | 51 | build-other: 52 | name: Build 53 | runs-on: ${{ matrix.os }} 54 | strategy: 55 | matrix: 56 | os: [macOS-latest, windows-latest] 57 | 58 | steps: 59 | - name: Check out code into $GITHUB_WORKSPACE directory 60 | uses: actions/checkout@v4 61 | 62 | - name: Set up Go 63 | uses: actions/setup-go@v5 64 | with: 65 | go-version: ^1.22 66 | id: go 67 | 68 | - name: Get dependencies 69 | run: | 70 | go get -v -t -d ./... 71 | 72 | - name: Build 73 | run: | 74 | go build -v ./... 75 | 76 | - name: Test 77 | run: | 78 | go test -v ./... 79 | 80 | build-freebsd: 81 | name: Build (FreeBSD) 82 | # runs-on: macos-12 83 | runs-on: ubuntu-latest 84 | 85 | steps: 86 | - uses: actions/checkout@v4 87 | - name: Test in FreeBSD 88 | id: test 89 | uses: vmactions/freebsd-vm@v1 90 | with: 91 | usesh: true 92 | prepare: pkg install -y go gnome-keyring 93 | run: | 94 | go version 95 | go build -v 96 | dbus-run-session -- sh -c "echo 'somecredstorepass' | gnome-keyring-daemon --unlock; go test -v ./..." 97 | # verify that we can build for freebsd with cgo disabled 98 | # This will disable the functionality as it depends on cgo (via 99 | # godbus) but should still be buildable to not break backwards 100 | # compatibility 101 | CGO_ENABLED=0 go build -v 102 | -------------------------------------------------------------------------------- /keyring_windows.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "strings" 5 | "syscall" 6 | 7 | "github.com/danieljoos/wincred" 8 | ) 9 | 10 | type windowsKeychain struct{} 11 | 12 | // Get gets a secret from the keyring given a service name and a user. 13 | func (k windowsKeychain) Get(service, username string) (string, error) { 14 | cred, err := wincred.GetGenericCredential(k.credName(service, username)) 15 | if err != nil { 16 | if err == syscall.ERROR_NOT_FOUND { 17 | return "", ErrNotFound 18 | } 19 | return "", err 20 | } 21 | 22 | return string(cred.CredentialBlob), nil 23 | } 24 | 25 | // Set stores stores user and pass in the keyring under the defined service 26 | // name. 27 | func (k windowsKeychain) Set(service, username, password string) error { 28 | // password may not exceed 2560 bytes (https://github.com/jaraco/keyring/issues/540#issuecomment-968329967) 29 | if len(password) > 2560 { 30 | return ErrSetDataTooBig 31 | } 32 | 33 | // service may not exceed 512 bytes (might need more testing) 34 | if len(service) >= 512 { 35 | return ErrSetDataTooBig 36 | } 37 | 38 | // service may not exceed 32k but problems occur before that 39 | // so we limit it to 30k 40 | if len(service) > 1024*30 { 41 | return ErrSetDataTooBig 42 | } 43 | 44 | cred := wincred.NewGenericCredential(k.credName(service, username)) 45 | cred.UserName = username 46 | cred.CredentialBlob = []byte(password) 47 | return cred.Write() 48 | } 49 | 50 | // Delete deletes a secret, identified by service & user, from the keyring. 51 | func (k windowsKeychain) Delete(service, username string) error { 52 | cred, err := wincred.GetGenericCredential(k.credName(service, username)) 53 | if err != nil { 54 | if err == syscall.ERROR_NOT_FOUND { 55 | return ErrNotFound 56 | } 57 | return err 58 | } 59 | 60 | return cred.Delete() 61 | } 62 | 63 | func (k windowsKeychain) DeleteAll(service string) error { 64 | // if service is empty, do nothing otherwise it might accidentally delete all secrets 65 | if service == "" { 66 | return ErrNotFound 67 | } 68 | 69 | creds, err := wincred.List() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | prefix := k.credName(service, "") 75 | deletedCount := 0 76 | 77 | for _, cred := range creds { 78 | if strings.HasPrefix(cred.TargetName, prefix) { 79 | genericCred, err := wincred.GetGenericCredential(cred.TargetName) 80 | if err != nil { 81 | if err != syscall.ERROR_NOT_FOUND { 82 | return err 83 | } 84 | } else { 85 | err := genericCred.Delete() 86 | if err != nil { 87 | return err 88 | } 89 | deletedCount++ 90 | } 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | // credName combines service and username to a single string. 97 | func (k windowsKeychain) credName(service, username string) string { 98 | return service + ":" + username 99 | } 100 | 101 | func init() { 102 | provider = windowsKeychain{} 103 | } 104 | -------------------------------------------------------------------------------- /keyring_mock_test.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | // TestSet tests setting a user and password in the keyring. 9 | func TestMockSet(t *testing.T) { 10 | mp := mockProvider{} 11 | err := mp.Set(service, user, password) 12 | if err != nil { 13 | t.Errorf("Should not fail, got: %s", err) 14 | } 15 | } 16 | 17 | // TestGet tests getting a password from the keyring. 18 | func TestMockGet(t *testing.T) { 19 | mp := mockProvider{} 20 | err := mp.Set(service, user, password) 21 | if err != nil { 22 | t.Errorf("Should not fail, got: %s", err) 23 | } 24 | 25 | pw, err := mp.Get(service, user) 26 | if err != nil { 27 | t.Errorf("Should not fail, got: %s", err) 28 | } 29 | 30 | if password != pw { 31 | t.Errorf("Expected password %s, got %s", password, pw) 32 | } 33 | } 34 | 35 | // TestGetNonExisting tests getting a secret not in the keyring. 36 | func TestMockGetNonExisting(t *testing.T) { 37 | mp := mockProvider{} 38 | 39 | _, err := mp.Get(service, user+"fake") 40 | assertError(t, err, ErrNotFound) 41 | } 42 | 43 | // TestDelete tests deleting a secret from the keyring. 44 | func TestMockDelete(t *testing.T) { 45 | mp := mockProvider{} 46 | 47 | err := mp.Set(service, user, password) 48 | if err != nil { 49 | t.Errorf("Should not fail, got: %s", err) 50 | } 51 | 52 | err = mp.Delete(service, user) 53 | if err != nil { 54 | t.Errorf("Should not fail, got: %s", err) 55 | } 56 | } 57 | 58 | // TestDeleteNonExisting tests deleting a secret not in the keyring. 59 | func TestMockDeleteNonExisting(t *testing.T) { 60 | mp := mockProvider{} 61 | 62 | err := mp.Delete(service, user+"fake") 63 | assertError(t, err, ErrNotFound) 64 | } 65 | 66 | func TestMockWithError(t *testing.T) { 67 | mp := mockProvider{mockError: errors.New("mock error")} 68 | 69 | err := mp.Set(service, user, password) 70 | assertError(t, err, mp.mockError) 71 | 72 | _, err = mp.Get(service, user) 73 | assertError(t, err, mp.mockError) 74 | 75 | err = mp.Delete(service, user) 76 | assertError(t, err, mp.mockError) 77 | } 78 | 79 | // TestMockDeleteAll tests deleting all secrets for a given service. 80 | func TestMockDeleteAll(t *testing.T) { 81 | mp := mockProvider{} 82 | 83 | // Set up multiple secrets for the same service 84 | err := mp.Set(service, user, password) 85 | if err != nil { 86 | t.Errorf("Should not fail, got: %s", err) 87 | } 88 | 89 | err = mp.Set(service, user+"2", password+"2") 90 | if err != nil { 91 | t.Errorf("Should not fail, got: %s", err) 92 | } 93 | 94 | // Delete all secrets for the service 95 | err = mp.DeleteAll(service) 96 | if err != nil { 97 | t.Errorf("Should not fail, got: %s", err) 98 | } 99 | 100 | // Verify that all secrets for the service are deleted 101 | _, err = mp.Get(service, user) 102 | assertError(t, err, ErrNotFound) 103 | 104 | _, err = mp.Get(service, user+"2") 105 | assertError(t, err, ErrNotFound) 106 | 107 | // Verify that DeleteAll on an empty service doesn't cause an error 108 | err = mp.DeleteAll(service) 109 | if err != nil { 110 | t.Errorf("Should not fail on empty service, got: %s", err) 111 | } 112 | } 113 | 114 | func assertError(t *testing.T, err error, expected error) { 115 | if err != expected { 116 | t.Errorf("Expected error %s, got %s", expected, err) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /keyring_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package keyring 16 | 17 | import ( 18 | "encoding/base64" 19 | "encoding/hex" 20 | "fmt" 21 | "io" 22 | "os/exec" 23 | "strings" 24 | 25 | "github.com/zalando/go-keyring/internal/shellescape" 26 | ) 27 | 28 | const ( 29 | execPathKeychain = "/usr/bin/security" 30 | 31 | // encodingPrefix is a well-known prefix added to strings encoded by Set. 32 | encodingPrefix = "go-keyring-encoded:" 33 | base64EncodingPrefix = "go-keyring-base64:" 34 | ) 35 | 36 | type macOSXKeychain struct{} 37 | 38 | // func (*MacOSXKeychain) IsAvailable() bool { 39 | // return exec.Command(execPathKeychain).Run() != exec.ErrNotFound 40 | // } 41 | 42 | // Get password from macos keyring given service and user name. 43 | func (k macOSXKeychain) Get(service, username string) (string, error) { 44 | out, err := exec.Command( 45 | execPathKeychain, 46 | "find-generic-password", 47 | "-s", service, 48 | "-wa", username).CombinedOutput() 49 | if err != nil { 50 | if strings.Contains(string(out), "could not be found") { 51 | err = ErrNotFound 52 | } 53 | return "", err 54 | } 55 | 56 | trimStr := strings.TrimSpace(string(out[:])) 57 | // if the string has the well-known prefix, assume it's encoded 58 | if strings.HasPrefix(trimStr, encodingPrefix) { 59 | dec, err := hex.DecodeString(trimStr[len(encodingPrefix):]) 60 | return string(dec), err 61 | } else if strings.HasPrefix(trimStr, base64EncodingPrefix) { 62 | dec, err := base64.StdEncoding.DecodeString(trimStr[len(base64EncodingPrefix):]) 63 | return string(dec), err 64 | } 65 | 66 | return trimStr, nil 67 | } 68 | 69 | // Set stores a secret in the macos keyring given a service name and a user. 70 | func (k macOSXKeychain) Set(service, username, password string) error { 71 | // if the added secret has multiple lines or some non ascii, 72 | // osx will hex encode it on return. To avoid getting garbage, we 73 | // encode all passwords 74 | password = base64EncodingPrefix + base64.StdEncoding.EncodeToString([]byte(password)) 75 | 76 | cmd := exec.Command(execPathKeychain, "-i") 77 | stdIn, err := cmd.StdinPipe() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | if err = cmd.Start(); err != nil { 83 | return err 84 | } 85 | 86 | command := fmt.Sprintf("add-generic-password -U -s %s -a %s -w %s\n", shellescape.Quote(service), shellescape.Quote(username), shellescape.Quote(password)) 87 | if len(command) > 4096 { 88 | return ErrSetDataTooBig 89 | } 90 | 91 | if _, err := io.WriteString(stdIn, command); err != nil { 92 | return err 93 | } 94 | 95 | if err = stdIn.Close(); err != nil { 96 | return err 97 | } 98 | 99 | err = cmd.Wait() 100 | return err 101 | } 102 | 103 | // Delete deletes a secret, identified by service & user, from the keyring. 104 | func (k macOSXKeychain) Delete(service, username string) error { 105 | out, err := exec.Command( 106 | execPathKeychain, 107 | "delete-generic-password", 108 | "-s", service, 109 | "-a", username).CombinedOutput() 110 | if strings.Contains(string(out), "could not be found") { 111 | err = ErrNotFound 112 | } 113 | return err 114 | } 115 | 116 | // DeleteAll deletes all secrets for a given service 117 | func (k macOSXKeychain) DeleteAll(service string) error { 118 | // if service is empty, do nothing otherwise it might accidentally delete all secrets 119 | if service == "" { 120 | return ErrNotFound 121 | } 122 | // Delete each secret in a while loop until there is no more left 123 | // under the service 124 | for { 125 | out, err := exec.Command( 126 | execPathKeychain, 127 | "delete-generic-password", 128 | "-s", service).CombinedOutput() 129 | if strings.Contains(string(out), "could not be found") { 130 | return nil 131 | } else if err != nil { 132 | return err 133 | } 134 | } 135 | 136 | } 137 | 138 | func init() { 139 | provider = macOSXKeychain{} 140 | } 141 | -------------------------------------------------------------------------------- /keyring_unix.go: -------------------------------------------------------------------------------- 1 | //go:build (dragonfly && cgo) || (freebsd && cgo) || linux || netbsd || openbsd 2 | 3 | package keyring 4 | 5 | import ( 6 | "fmt" 7 | 8 | dbus "github.com/godbus/dbus/v5" 9 | ss "github.com/zalando/go-keyring/secret_service" 10 | ) 11 | 12 | type secretServiceProvider struct{} 13 | 14 | // Set stores user and pass in the keyring under the defined service 15 | // name. 16 | func (s secretServiceProvider) Set(service, user, pass string) error { 17 | svc, err := ss.NewSecretService() 18 | if err != nil { 19 | return err 20 | } 21 | 22 | // open a session 23 | session, err := svc.OpenSession() 24 | if err != nil { 25 | return err 26 | } 27 | defer svc.Close(session) 28 | 29 | attributes := map[string]string{ 30 | "username": user, 31 | "service": service, 32 | } 33 | 34 | secret := ss.NewSecret(session.Path(), pass) 35 | 36 | collection := svc.GetLoginCollection() 37 | 38 | err = svc.Unlock(collection.Path()) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | err = svc.CreateItem(collection, 44 | fmt.Sprintf("Password for '%s' on '%s'", user, service), 45 | attributes, secret) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // findItem looksup an item by service and user. 54 | func (s secretServiceProvider) findItem(svc *ss.SecretService, service, user string) (dbus.ObjectPath, error) { 55 | collection := svc.GetLoginCollection() 56 | 57 | search := map[string]string{ 58 | "username": user, 59 | "service": service, 60 | } 61 | 62 | err := svc.Unlock(collection.Path()) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | results, err := svc.SearchItems(collection, search) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | if len(results) == 0 { 73 | return "", ErrNotFound 74 | } 75 | 76 | return results[0], nil 77 | } 78 | 79 | // findServiceItems looksup all items by service. 80 | func (s secretServiceProvider) findServiceItems(svc *ss.SecretService, service string) ([]dbus.ObjectPath, error) { 81 | collection := svc.GetLoginCollection() 82 | 83 | search := map[string]string{ 84 | "service": service, 85 | } 86 | 87 | err := svc.Unlock(collection.Path()) 88 | if err != nil { 89 | return []dbus.ObjectPath{}, err 90 | } 91 | 92 | results, err := svc.SearchItems(collection, search) 93 | if err != nil { 94 | return []dbus.ObjectPath{}, err 95 | } 96 | 97 | if len(results) == 0 { 98 | return []dbus.ObjectPath{}, ErrNotFound 99 | } 100 | 101 | return results, nil 102 | } 103 | 104 | // Get gets a secret from the keyring given a service name and a user. 105 | func (s secretServiceProvider) Get(service, user string) (string, error) { 106 | svc, err := ss.NewSecretService() 107 | if err != nil { 108 | return "", err 109 | } 110 | 111 | item, err := s.findItem(svc, service, user) 112 | if err != nil { 113 | return "", err 114 | } 115 | 116 | // open a session 117 | session, err := svc.OpenSession() 118 | if err != nil { 119 | return "", err 120 | } 121 | defer svc.Close(session) 122 | 123 | // unlock if invdividual item is locked 124 | err = svc.Unlock(item) 125 | if err != nil { 126 | return "", err 127 | } 128 | 129 | secret, err := svc.GetSecret(item, session.Path()) 130 | if err != nil { 131 | return "", err 132 | } 133 | 134 | return string(secret.Value), nil 135 | } 136 | 137 | // Delete deletes a secret, identified by service & user, from the keyring. 138 | func (s secretServiceProvider) Delete(service, user string) error { 139 | svc, err := ss.NewSecretService() 140 | if err != nil { 141 | return err 142 | } 143 | 144 | item, err := s.findItem(svc, service, user) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | return svc.Delete(item) 150 | } 151 | 152 | // DeleteAll deletes all secrets for a given service 153 | func (s secretServiceProvider) DeleteAll(service string) error { 154 | // if service is empty, do nothing otherwise it might accidentally delete all secrets 155 | if service == "" { 156 | return ErrNotFound 157 | } 158 | 159 | svc, err := ss.NewSecretService() 160 | if err != nil { 161 | return err 162 | } 163 | // find all items for the service 164 | items, err := s.findServiceItems(svc, service) 165 | if err != nil { 166 | if err == ErrNotFound { 167 | return nil 168 | } 169 | return err 170 | } 171 | for _, item := range items { 172 | err = svc.Delete(item) 173 | if err != nil { 174 | return err 175 | } 176 | } 177 | return nil 178 | } 179 | 180 | func init() { 181 | provider = secretServiceProvider{} 182 | } 183 | -------------------------------------------------------------------------------- /keyring_test.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | service = "test-service" 11 | user = "test-user" 12 | password = "test-password" 13 | ) 14 | 15 | // TestSet tests setting a user and password in the keyring. 16 | func TestSet(t *testing.T) { 17 | err := Set(service, user, password) 18 | if err != nil { 19 | t.Errorf("Should not fail, got: %s", err) 20 | } 21 | } 22 | 23 | func TestSetTooLong(t *testing.T) { 24 | extraLongPassword := "ba" + strings.Repeat("na", 5000) 25 | err := Set(service, user, extraLongPassword) 26 | 27 | if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { 28 | // should fail on those platforms 29 | if err != ErrSetDataTooBig { 30 | t.Errorf("Should have failed, got: %s", err) 31 | } 32 | } 33 | } 34 | 35 | // TestGetMultiline tests getting a multi-line password from the keyring 36 | func TestGetMultiLine(t *testing.T) { 37 | multilinePassword := `this password 38 | has multiple 39 | lines and will be 40 | encoded by some keyring implementiations 41 | like osx` 42 | err := Set(service, user, multilinePassword) 43 | if err != nil { 44 | t.Errorf("Should not fail, got: %s", err) 45 | } 46 | 47 | pw, err := Get(service, user) 48 | if err != nil { 49 | t.Errorf("Should not fail, got: %s", err) 50 | } 51 | 52 | if multilinePassword != pw { 53 | t.Errorf("Expected password %s, got %s", multilinePassword, pw) 54 | } 55 | } 56 | 57 | // TestGetMultiline tests getting a multi-line password from the keyring 58 | func TestGetUmlaut(t *testing.T) { 59 | umlautPassword := "at least on OSX üöäÜÖÄß will be encoded" 60 | err := Set(service, user, umlautPassword) 61 | if err != nil { 62 | t.Errorf("Should not fail, got: %s", err) 63 | } 64 | 65 | pw, err := Get(service, user) 66 | if err != nil { 67 | t.Errorf("Should not fail, got: %s", err) 68 | } 69 | 70 | if umlautPassword != pw { 71 | t.Errorf("Expected password %s, got %s", umlautPassword, pw) 72 | } 73 | } 74 | 75 | // TestGetSingleLineHex tests getting a single line hex string password from the keyring. 76 | func TestGetSingleLineHex(t *testing.T) { 77 | hexPassword := "abcdef123abcdef123" 78 | err := Set(service, user, hexPassword) 79 | if err != nil { 80 | t.Errorf("Should not fail, got: %s", err) 81 | } 82 | 83 | pw, err := Get(service, user) 84 | if err != nil { 85 | t.Errorf("Should not fail, got: %s", err) 86 | } 87 | 88 | if hexPassword != pw { 89 | t.Errorf("Expected password %s, got %s", hexPassword, pw) 90 | } 91 | } 92 | 93 | // TestGet tests getting a password from the keyring. 94 | func TestGet(t *testing.T) { 95 | err := Set(service, user, password) 96 | if err != nil { 97 | t.Errorf("Should not fail, got: %s", err) 98 | } 99 | 100 | pw, err := Get(service, user) 101 | if err != nil { 102 | t.Errorf("Should not fail, got: %s", err) 103 | } 104 | 105 | if password != pw { 106 | t.Errorf("Expected password %s, got %s", password, pw) 107 | } 108 | } 109 | 110 | // TestGetNonExisting tests getting a secret not in the keyring. 111 | func TestGetNonExisting(t *testing.T) { 112 | _, err := Get(service, user+"fake") 113 | if err != ErrNotFound { 114 | t.Errorf("Expected error ErrNotFound, got %s", err) 115 | } 116 | } 117 | 118 | // TestDelete tests deleting a secret from the keyring. 119 | func TestDelete(t *testing.T) { 120 | err := Delete(service, user) 121 | if err != nil { 122 | t.Errorf("Should not fail, got: %s", err) 123 | } 124 | } 125 | 126 | // TestDeleteNonExisting tests deleting a secret not in the keyring. 127 | func TestDeleteNonExisting(t *testing.T) { 128 | err := Delete(service, user+"fake") 129 | if err != ErrNotFound { 130 | t.Errorf("Expected error ErrNotFound, got %s", err) 131 | } 132 | } 133 | 134 | // TestDeleteAll tests deleting all secrets for a given service. 135 | func TestDeleteAll(t *testing.T) { 136 | // Set up multiple secrets for the same service 137 | err := Set(service, user, password) 138 | if err != nil { 139 | t.Errorf("Should not fail, got: %s", err) 140 | } 141 | 142 | err = Set(service, user+"2", password+"2") 143 | if err != nil { 144 | t.Errorf("Should not fail, got: %s", err) 145 | } 146 | 147 | // Delete all secrets for the service 148 | err = DeleteAll(service) 149 | if err != nil { 150 | t.Errorf("Should not fail, got: %s", err) 151 | } 152 | 153 | // Verify that all secrets for the service are deleted 154 | _, err = Get(service, user) 155 | if err != ErrNotFound { 156 | t.Errorf("Expected error ErrNotFound, got %s", err) 157 | } 158 | 159 | _, err = Get(service, user+"2") 160 | if err != ErrNotFound { 161 | t.Errorf("Expected error ErrNotFound, got %s", err) 162 | } 163 | 164 | // Verify that DeleteAll on an empty service doesn't cause an error 165 | err = DeleteAll(service) 166 | if err != nil { 167 | t.Errorf("Should not fail on empty service, got: %s", err) 168 | } 169 | } 170 | 171 | // TestDeleteAll with empty service name 172 | func TestDeleteAllEmptyService(t *testing.T) { 173 | err := Set(service, user, password) 174 | 175 | if err != nil { 176 | t.Errorf("Should not fail, got: %s", err) 177 | } 178 | _ = DeleteAll("") 179 | _, err = Get(service, user) 180 | if err == ErrNotFound { 181 | t.Errorf("Should not have deleted secret from another service") 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /secret_service/secret_service.go: -------------------------------------------------------------------------------- 1 | package ss 2 | 3 | import ( 4 | "fmt" 5 | 6 | "errors" 7 | 8 | dbus "github.com/godbus/dbus/v5" 9 | ) 10 | 11 | const ( 12 | serviceName = "org.freedesktop.secrets" 13 | servicePath = "/org/freedesktop/secrets" 14 | serviceInterface = "org.freedesktop.Secret.Service" 15 | collectionInterface = "org.freedesktop.Secret.Collection" 16 | collectionsInterface = "org.freedesktop.Secret.Service.Collections" 17 | itemInterface = "org.freedesktop.Secret.Item" 18 | sessionInterface = "org.freedesktop.Secret.Session" 19 | promptInterface = "org.freedesktop.Secret.Prompt" 20 | 21 | loginCollectionAlias = "/org/freedesktop/secrets/aliases/default" 22 | collectionBasePath = "/org/freedesktop/secrets/collection/" 23 | ) 24 | 25 | // Secret defines a org.freedesk.Secret.Item secret struct. 26 | type Secret struct { 27 | Session dbus.ObjectPath 28 | Parameters []byte 29 | Value []byte 30 | ContentType string `dbus:"content_type"` 31 | } 32 | 33 | // NewSecret initializes a new Secret. 34 | func NewSecret(session dbus.ObjectPath, secret string) Secret { 35 | return Secret{ 36 | Session: session, 37 | Parameters: []byte{}, 38 | Value: []byte(secret), 39 | ContentType: "text/plain; charset=utf8", 40 | } 41 | } 42 | 43 | // SecretService is an interface for the Secret Service dbus API. 44 | type SecretService struct { 45 | *dbus.Conn 46 | object dbus.BusObject 47 | } 48 | 49 | // NewSecretService inializes a new SecretService object. 50 | func NewSecretService() (*SecretService, error) { 51 | conn, err := dbus.SessionBus() 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return &SecretService{ 57 | conn, 58 | conn.Object(serviceName, servicePath), 59 | }, nil 60 | } 61 | 62 | // OpenSession opens a secret service session. 63 | func (s *SecretService) OpenSession() (dbus.BusObject, error) { 64 | var disregard dbus.Variant 65 | var sessionPath dbus.ObjectPath 66 | err := s.object.Call(serviceInterface+".OpenSession", 0, "plain", dbus.MakeVariant("")).Store(&disregard, &sessionPath) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return s.Object(serviceName, sessionPath), nil 72 | } 73 | 74 | // CheckCollectionPath accepts dbus path and returns nil if the path is found 75 | // in the collection interface (and can be used). 76 | func (s *SecretService) CheckCollectionPath(path dbus.ObjectPath) error { 77 | obj := s.Conn.Object(serviceName, servicePath) 78 | val, err := obj.GetProperty(collectionsInterface) 79 | if err != nil { 80 | return err 81 | } 82 | paths := val.Value().([]dbus.ObjectPath) 83 | for _, p := range paths { 84 | if p == path { 85 | return nil 86 | } 87 | } 88 | return errors.New("path not found") 89 | } 90 | 91 | // GetCollection returns a collection from a name. 92 | func (s *SecretService) GetCollection(name string) dbus.BusObject { 93 | return s.Object(serviceName, dbus.ObjectPath(collectionBasePath+name)) 94 | } 95 | 96 | // GetLoginCollection decides and returns the dbus collection to be used for login. 97 | func (s *SecretService) GetLoginCollection() dbus.BusObject { 98 | path := dbus.ObjectPath(collectionBasePath + "login") 99 | if err := s.CheckCollectionPath(path); err != nil { 100 | path = dbus.ObjectPath(loginCollectionAlias) 101 | } 102 | return s.Object(serviceName, path) 103 | } 104 | 105 | // Unlock unlocks a collection. 106 | func (s *SecretService) Unlock(collection dbus.ObjectPath) error { 107 | var unlocked []dbus.ObjectPath 108 | var prompt dbus.ObjectPath 109 | err := s.object.Call(serviceInterface+".Unlock", 0, []dbus.ObjectPath{collection}).Store(&unlocked, &prompt) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | _, v, err := s.handlePrompt(prompt) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | collections := v.Value() 120 | switch c := collections.(type) { 121 | case []dbus.ObjectPath: 122 | unlocked = append(unlocked, c...) 123 | } 124 | 125 | if len(unlocked) != 1 || (collection != loginCollectionAlias && unlocked[0] != collection) { 126 | return fmt.Errorf("failed to unlock correct collection '%v'", collection) 127 | } 128 | 129 | return nil 130 | } 131 | 132 | // Close closes a secret service dbus session. 133 | func (s *SecretService) Close(session dbus.BusObject) error { 134 | return session.Call(sessionInterface+".Close", 0).Err 135 | } 136 | 137 | // CreateCollection with the supplied label. 138 | func (s *SecretService) CreateCollection(label string) (dbus.BusObject, error) { 139 | properties := map[string]dbus.Variant{ 140 | collectionInterface + ".Label": dbus.MakeVariant(label), 141 | } 142 | var collection, prompt dbus.ObjectPath 143 | err := s.object.Call(serviceInterface+".CreateCollection", 0, properties, ""). 144 | Store(&collection, &prompt) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | _, v, err := s.handlePrompt(prompt) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | if v.String() != "" { 155 | collection = dbus.ObjectPath(v.String()) 156 | } 157 | 158 | return s.Object(serviceName, collection), nil 159 | } 160 | 161 | // CreateItem creates an item in a collection, with label, attributes and a 162 | // related secret. 163 | func (s *SecretService) CreateItem(collection dbus.BusObject, label string, attributes map[string]string, secret Secret) error { 164 | properties := map[string]dbus.Variant{ 165 | itemInterface + ".Label": dbus.MakeVariant(label), 166 | itemInterface + ".Attributes": dbus.MakeVariant(attributes), 167 | } 168 | 169 | var item, prompt dbus.ObjectPath 170 | err := collection.Call(collectionInterface+".CreateItem", 0, 171 | properties, secret, true).Store(&item, &prompt) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | _, _, err = s.handlePrompt(prompt) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | return nil 182 | } 183 | 184 | // handlePrompt checks if a prompt should be handles and handles it by 185 | // triggering the prompt and waiting for the Secret service daemon to display 186 | // the prompt to the user. 187 | func (s *SecretService) handlePrompt(prompt dbus.ObjectPath) (bool, dbus.Variant, error) { 188 | if prompt != dbus.ObjectPath("/") { 189 | err := s.AddMatchSignal(dbus.WithMatchObjectPath(prompt), 190 | dbus.WithMatchInterface(promptInterface), 191 | ) 192 | if err != nil { 193 | return false, dbus.MakeVariant(""), err 194 | } 195 | 196 | defer func(s *SecretService, options ...dbus.MatchOption) { 197 | _ = s.RemoveMatchSignal(options...) 198 | }(s, dbus.WithMatchObjectPath(prompt), dbus.WithMatchInterface(promptInterface)) 199 | 200 | promptSignal := make(chan *dbus.Signal, 1) 201 | s.Signal(promptSignal) 202 | 203 | err = s.Object(serviceName, prompt).Call(promptInterface+".Prompt", 0, "").Err 204 | if err != nil { 205 | return false, dbus.MakeVariant(""), err 206 | } 207 | 208 | signal := <-promptSignal 209 | switch signal.Name { 210 | case promptInterface + ".Completed": 211 | dismissed := signal.Body[0].(bool) 212 | result := signal.Body[1].(dbus.Variant) 213 | return dismissed, result, nil 214 | } 215 | 216 | } 217 | 218 | return false, dbus.MakeVariant(""), nil 219 | } 220 | 221 | // SearchItems returns a list of items matching the search object. 222 | func (s *SecretService) SearchItems(collection dbus.BusObject, search interface{}) ([]dbus.ObjectPath, error) { 223 | var results []dbus.ObjectPath 224 | err := collection.Call(collectionInterface+".SearchItems", 0, search).Store(&results) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | return results, nil 230 | } 231 | 232 | // GetSecret gets secret from an item in a given session. 233 | func (s *SecretService) GetSecret(itemPath dbus.ObjectPath, session dbus.ObjectPath) (*Secret, error) { 234 | var secret Secret 235 | err := s.Object(serviceName, itemPath).Call(itemInterface+".GetSecret", 0, session).Store(&secret) 236 | if err != nil { 237 | return nil, err 238 | } 239 | 240 | return &secret, nil 241 | } 242 | 243 | // Delete deletes an item from the collection. 244 | func (s *SecretService) Delete(itemPath dbus.ObjectPath) error { 245 | var prompt dbus.ObjectPath 246 | err := s.Object(serviceName, itemPath).Call(itemInterface+".Delete", 0).Store(&prompt) 247 | if err != nil { 248 | return err 249 | } 250 | 251 | _, _, err = s.handlePrompt(prompt) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | return nil 257 | } 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Keyring library 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/zalando/go-keyring)](https://goreportcard.com/report/github.com/zalando/go-keyring) 4 | [![GoDoc](https://godoc.org/github.com/zalando/go-keyring?status.svg)](https://godoc.org/github.com/zalando/go-keyring) 5 | 6 | `go-keyring` is an OS-agnostic library for *setting*, *getting* and *deleting* 7 | secrets from the system keyring. It supports **OS X**, **Linux/BSD (dbus)** and 8 | **Windows**. 9 | 10 | go-keyring was created after its authors searched for, but couldn't find, a better alternative. It aims to simplify 11 | using statically linked binaries, which is cumbersome when relying on C bindings (as other keyring libraries do). 12 | 13 | #### Potential Uses 14 | 15 | If you're working with an application that needs to store user credentials 16 | locally on the user's machine, go-keyring might come in handy. For instance, if you are writing a CLI for an API 17 | that requires a username and password, you can store this information in the 18 | keyring instead of having the user type it on every invocation. 19 | 20 | ## Dependencies 21 | 22 | #### OS X 23 | 24 | The OS X implementation depends on the `/usr/bin/security` binary for 25 | interfacing with the OS X keychain. It should be available by default. 26 | 27 | #### Linux and *BSD 28 | 29 | The Linux and *BSD implementation depends on the [Secret Service][SecretService] dbus 30 | interface, which is provided by [GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring). 31 | 32 | It's expected that the default collection `login` exists in the keyring, because 33 | it's the default in most distros. If it doesn't exist, you can create it through the 34 | keyring frontend program [Seahorse](https://wiki.gnome.org/Apps/Seahorse): 35 | 36 | * Open `seahorse` 37 | * Go to **File > New > Password Keyring** 38 | * Click **Continue** 39 | * When asked for a name, use: **login** 40 | 41 | ## Example Usage 42 | 43 | How to *set* and *get* a secret from the keyring: 44 | 45 | ```go 46 | package main 47 | 48 | import ( 49 | "log" 50 | 51 | "github.com/zalando/go-keyring" 52 | ) 53 | 54 | func main() { 55 | service := "my-app" 56 | user := "anon" 57 | password := "secret" 58 | 59 | // set password 60 | err := keyring.Set(service, user, password) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | 65 | // get password 66 | secret, err := keyring.Get(service, user) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | log.Println(secret) 72 | } 73 | 74 | ``` 75 | 76 | ## Direct CLI Usage 77 | 78 | While this library provides a convenient Go API, you can also interact with the system keyring directly using OS-specific command-line tools. This can be useful for debugging, scripting, or understanding what the library does under the hood. You can use the CLI to set-up the secrets from a script and then access them from Go, or vice-versa. 79 | 80 | ### macOS 81 | 82 | macOS uses the `security` command to interact with the Keychain. 83 | 84 | **Set a password:** 85 | ```bash 86 | security add-generic-password -U -s "service" -a "user" -w "password" 87 | ``` 88 | 89 | **Get a password:** 90 | ```bash 91 | security find-generic-password -s "service" -wa "user" 92 | ``` 93 | 94 | **Delete a password:** 95 | ```bash 96 | security delete-generic-password -s "service" -a "user" 97 | ``` 98 | 99 | Where: 100 | - `-s` specifies the service name 101 | - `-a` specifies the account/username 102 | - `-w` specifies the password to store 103 | - `-U` updates the password if it already exists 104 | - The `w` option in `-wa` outputs only the password value 105 | 106 | ### Linux and *BSD 107 | 108 | Linux and *BSD systems use the Secret Service API via D-Bus. The easiest way to interact with it from the command line is using `secret-tool`, which is part of libsecret. 109 | 110 | **Install secret-tool (if not already installed):** 111 | ```bash 112 | # Debian/Ubuntu 113 | sudo apt-get install libsecret-tools 114 | 115 | # Fedora/RHEL 116 | sudo dnf install libsecret 117 | 118 | # Arch Linux 119 | sudo pacman -S libsecret 120 | ``` 121 | 122 | **Set a password:** 123 | ```bash 124 | secret-tool store --label="Password for 'user' on 'service'" service "service" username "user" 125 | # You'll be prompted to enter the password 126 | ``` 127 | 128 | Or provide the password directly: 129 | ```bash 130 | echo -n "password" | secret-tool store --label="Password for 'user' on 'service'" service "service" username "user" 131 | ``` 132 | 133 | **Get a password:** 134 | ```bash 135 | secret-tool lookup service "service" username "user" 136 | ``` 137 | 138 | **Delete a password:** 139 | ```bash 140 | secret-tool clear service "service" username "user" 141 | ``` 142 | 143 | Note: The `service` and `username` are attributes used to identify the secret. The label is a human-readable description. 144 | 145 | ### Windows 146 | 147 | Windows uses the Credential Manager, which can be accessed via `cmdkey` or PowerShell. 148 | 149 | **Using cmdkey:** 150 | 151 | **Set a password:** 152 | ```cmd 153 | cmdkey /generic:"service:user" /user:"user" /pass:"password" 154 | ``` 155 | 156 | **Get a password:** 157 | 158 | `cmdkey` doesn't support retrieving passwords directly. Use PowerShell instead: 159 | ```powershell 160 | $cred = Get-StoredCredential -Target "service:user" 161 | $cred.GetNetworkCredential().Password 162 | ``` 163 | 164 | Or using the Windows API via PowerShell: 165 | ```powershell 166 | [System.Net.NetworkCredential]::new("", (Get-StoredCredential -Target "service:user").Password).Password 167 | ``` 168 | 169 | **Delete a password:** 170 | ```cmd 171 | cmdkey /delete:"service:user" 172 | ``` 173 | 174 | **Using PowerShell with CredentialManager module:** 175 | 176 | First, install the CredentialManager module: 177 | ```powershell 178 | Install-Module -Name CredentialManager -Force 179 | ``` 180 | 181 | **Set a password:** 182 | ```powershell 183 | New-StoredCredential -Target "service:user" -UserName "user" -Password "password" -Type Generic -Persist LocalMachine 184 | ``` 185 | 186 | **Get a password:** 187 | ```powershell 188 | (Get-StoredCredential -Target "service:user").GetNetworkCredential().Password 189 | ``` 190 | 191 | **Delete a password:** 192 | ```powershell 193 | Remove-StoredCredential -Target "service:user" 194 | ``` 195 | 196 | Note: On Windows, the library combines the service and username as `service:username` for the credential target name. 197 | 198 | ## Tests 199 | 200 | ### Running tests 201 | 202 | Running the tests is simple: 203 | 204 | ``` 205 | go test 206 | ``` 207 | 208 | Which OS you use *does* matter. If you're using **Linux** or **BSD**, it will 209 | test the implementation in `keyring_unix.go`. If running the tests 210 | on **OS X**, it will test the implementation in `keyring_darwin.go`. 211 | 212 | ### Mocking 213 | 214 | If you need to mock the keyring behavior for testing on systems without a keyring implementation you can call `MockInit()` which will replace the OS defined provider with an in-memory one. 215 | 216 | ```go 217 | package implementation 218 | 219 | import ( 220 | "testing" 221 | 222 | "github.com/zalando/go-keyring" 223 | ) 224 | 225 | func TestMockedSetGet(t *testing.T) { 226 | keyring.MockInit() 227 | err := keyring.Set("service", "user", "password") 228 | if err != nil { 229 | t.Fatal(err) 230 | } 231 | 232 | p, err := keyring.Get("service", "user") 233 | if err != nil { 234 | t.Fatal(err) 235 | } 236 | 237 | if p != "password" { 238 | t.Error("password was not the expected string") 239 | } 240 | 241 | } 242 | 243 | ``` 244 | 245 | ## Contributing/TODO 246 | 247 | We welcome contributions from the community; please use [CONTRIBUTING.md](CONTRIBUTING.md) as your guidelines for getting started. Here are some items that we'd love help with: 248 | 249 | * The code base 250 | * Better test coverage 251 | 252 | Please use GitHub issues as the starting point for contributions, new ideas and/or bug reports. 253 | 254 | ## Contact 255 | 256 | * E-Mail: 257 | * Security issues: Please send an email to the [maintainers](MAINTAINERS), and we'll try to get back to you within two workdays. If you don't hear back, send an email to and someone will respond within five days max. 258 | 259 | ## Contributors 260 | 261 | Thanks to: 262 | 263 | * [your name here] 264 | 265 | ## License 266 | 267 | See [LICENSE](LICENSE) file. 268 | 269 | [SecretService]: https://specifications.freedesktop.org/secret-service-spec/latest/ 270 | --------------------------------------------------------------------------------