diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
| commit | 20ef0d92694465ac86b550df139e8366a0a2b4fa (patch) | |
| tree | 3f14589e1ce6eb9306a3af31c3a1f9e1af5ed637 /vendor/github.com/99designs/keyring | |
| parent | 44e0d272c040cdc53a98b9f1dc58ae7da67752e6 (diff) | |
feat: connect to spicedb
Diffstat (limited to 'vendor/github.com/99designs/keyring')
19 files changed, 2109 insertions, 0 deletions
diff --git a/vendor/github.com/99designs/keyring/.gitattributes b/vendor/github.com/99designs/keyring/.gitattributes new file mode 100644 index 0000000..d207b18 --- /dev/null +++ b/vendor/github.com/99designs/keyring/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf diff --git a/vendor/github.com/99designs/keyring/.gitignore b/vendor/github.com/99designs/keyring/.gitignore new file mode 100644 index 0000000..8000dd9 --- /dev/null +++ b/vendor/github.com/99designs/keyring/.gitignore @@ -0,0 +1 @@ +.vagrant diff --git a/vendor/github.com/99designs/keyring/.golangci.yml b/vendor/github.com/99designs/keyring/.golangci.yml new file mode 100644 index 0000000..f83428e --- /dev/null +++ b/vendor/github.com/99designs/keyring/.golangci.yml @@ -0,0 +1,29 @@ +linters: + enable: + - bodyclose + - contextcheck + - depguard + - durationcheck + - dupl + - errchkjson + - errname + - exhaustive + - exportloopref + - gocritic + - gofmt + - goimports + - makezero + - misspell + - nakedret + - nilerr + - nilnil + - noctx + - prealloc + - revive + # - rowserrcheck + - thelper + - tparallel + - unconvert + - unparam + # - wastedassign + - whitespace diff --git a/vendor/github.com/99designs/keyring/LICENSE b/vendor/github.com/99designs/keyring/LICENSE new file mode 100644 index 0000000..0fe9e46 --- /dev/null +++ b/vendor/github.com/99designs/keyring/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 99designs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/99designs/keyring/README.md b/vendor/github.com/99designs/keyring/README.md new file mode 100644 index 0000000..629a6aa --- /dev/null +++ b/vendor/github.com/99designs/keyring/README.md @@ -0,0 +1,67 @@ +Keyring +======= +[](https://github.com/99designs/keyring/actions) +[](https://godoc.org/github.com/99designs/keyring) + +Keyring provides a common interface to a range of secure credential storage services. Originally developed as part of [AWS Vault](https://github.com/99designs/aws-vault), a command line tool for securely managing AWS access from developer workstations. + +Currently Keyring supports the following backends + * [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac) + * [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager) + * Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)) + * [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5) + * [Pass](https://www.passwordstore.org/) + * [Encrypted file (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) + * [KeyCtl](https://linux.die.net/man/1/keyctl) + + +## Usage + +The short version of how to use keyring is shown below. + +```go +ring, _ := keyring.Open(keyring.Config{ + ServiceName: "example", +}) + +_ = ring.Set(keyring.Item{ + Key: "foo", + Data: []byte("secret-bar"), +}) + +i, _ := ring.Get("foo") + +fmt.Printf("%s", i.Data) +``` + +For more detail on the API please check [the keyring godocs](https://godoc.org/github.com/99designs/keyring) + + +## Testing + +[Vagrant](https://www.vagrantup.com/) is used to create linux and windows test environments. + +```bash +# Start vagrant +vagrant up + +# Run go tests on all platforms +./bin/go-test +``` + + +## Contributing + +Contributions to the keyring package are most welcome from engineers of all backgrounds and skill levels. In particular the addition of extra backends across popular operating systems would be appreciated. + +This project will adhere to the [Go Community Code of Conduct](https://golang.org/conduct) in the github provided discussion spaces, with the moderators being the 99designs engineering team. + +To make a contribution: + + * Fork the repository + * Make your changes on the fork + * Submit a pull request back to this repo with a clear description of the problem you're solving + * Ensure your PR passes all current (and new) tests + * Ideally verify that [aws-vault](https://github.com/99designs/aws-vault) works with your changes (optional) + +...and we'll do our best to get your work merged in diff --git a/vendor/github.com/99designs/keyring/Vagrantfile b/vendor/github.com/99designs/keyring/Vagrantfile new file mode 100644 index 0000000..7d30d2a --- /dev/null +++ b/vendor/github.com/99designs/keyring/Vagrantfile @@ -0,0 +1,85 @@ +Vagrant.configure("2") do |config| + + config.vm.define "linux" do |linux| + linux.vm.box = "generic/fedora32" + + linux.vm.provider "virtualbox" do |vb| + vb.gui = true + vb.memory = 2048 + vb.cpus = 2 + + # VBoxVGA flickers constantly, use vmsvga instead which doesn't have that problem + vb.customize ["modifyvm", :id, "--graphicscontroller", "vmsvga"] + end + + # mount the project into /keyring + linux.vm.synced_folder ".", "/keyring" + + # install gnome desktop and auto login + linux.vm.provision "shell", inline: "sudo dnf install -y --exclude='gnome-initial-setup' @gnome-desktop langpacks-en" + linux.vm.provision "shell", inline: <<-SHELL + sudo sed -i -e 's/\\[daemon\\]/\\[daemon\\]\\nAutomaticLoginEnable=True\\nAutomaticLogin=vagrant\\n/' \ + /etc/gdm/custom.conf + SHELL + linux.vm.provision "shell", inline: "sudo systemctl set-default graphical.target" + linux.vm.provision "shell", inline: "sudo systemctl isolate graphical.target" + + # set the root password - sometimes prompts show up in gnome needing to install software + linux.vm.provision "shell", inline: "echo 'vagrant' | sudo passwd root --stdin" + + # install gnome keyring + linux.vm.provision "shell", inline: "sudo dnf install -y gnome-keyring seahorse" + + # install kwallet + linux.vm.provision "shell", inline: "sudo dnf install -y kwalletmanager5" + + # install pass + linux.vm.provision "shell", inline: "sudo dnf install -y pass" + + # install golang + linux.vm.provision "shell", inline: "sudo dnf install -y go" + end + + + config.vm.define "windows" do |windows| + windows.vm.box = "StefanScherer/windows_10" + + windows.vm.provider "virtualbox" do |vb| + vb.gui = true + vb.memory = 2048 + vb.cpus = 2 + end + + # mount the project into c:\keyring + windows.vm.synced_folder ".", "/keyring" + + # install chocolately + windows.vm.provision "shell", privileged: true, inline: <<-SHELL + Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + choco feature disable -n=showDownloadProgress + SHELL + + # install golang + windows.vm.provision "shell", privileged: true, inline: "choco install -y git golang" + end + + config.vm.post_up_message = <<-MESSAGE + There are 2 vagrant boxes: + - linux + - OS: Fedora 32 with Gnome Desktop + - The keyring directory is mounted at /keyring + - Get a shell with 'vagrant ssh linux' + - When running go test, you'll need to use the GUI to click "Continue" on the prompts + - After provisioning, adjusting the virtualbox GUI window size doesn't cause the resolution to update. A 'vagrant reload linux' solves the problem + - windows + - OS: Windows 10 + - The keyring directory is mounted at C:\keyring + - Get a shell by starting PowerShell in the GUI + - You can run commands remotely using 'vagrant winrm -e windows CMD'. You'll need the -e (elevated privileges) if you want to interact with wincred + + Automated scripts for running go test on vagrant boxes (run these locally): + - ./bin/go-test-linux - Run tests on Linux + - ./bin/go-test-windows - Run tests on Windows + - ./bin/go-test - Run all tests - locally, linux and windows + MESSAGE +end diff --git a/vendor/github.com/99designs/keyring/array.go b/vendor/github.com/99designs/keyring/array.go new file mode 100644 index 0000000..3179cb5 --- /dev/null +++ b/vendor/github.com/99designs/keyring/array.go @@ -0,0 +1,54 @@ +package keyring + +// ArrayKeyring is a mock/non-secure backend that meets the Keyring interface. +// It is intended to be used to aid unit testing of code that relies on the package. +// NOTE: Do not use in production code. +type ArrayKeyring struct { + items map[string]Item +} + +// NewArrayKeyring returns an ArrayKeyring, optionally constructed with an initial slice +// of items. +func NewArrayKeyring(initial []Item) *ArrayKeyring { + kr := &ArrayKeyring{} + for _, i := range initial { + _ = kr.Set(i) + } + return kr +} + +// Get returns an Item matching Key. +func (k *ArrayKeyring) Get(key string) (Item, error) { + if i, ok := k.items[key]; ok { + return i, nil + } + return Item{}, ErrKeyNotFound +} + +// Set will store an item on the mock Keyring. +func (k *ArrayKeyring) Set(i Item) error { + if k.items == nil { + k.items = map[string]Item{} + } + k.items[i.Key] = i + return nil +} + +// Remove will delete an Item from the Keyring. +func (k *ArrayKeyring) Remove(key string) error { + delete(k.items, key) + return nil +} + +// Keys provides a slice of all Item keys on the Keyring. +func (k *ArrayKeyring) Keys() ([]string, error) { + var keys = []string{} + for key := range k.items { + keys = append(keys, key) + } + return keys, nil +} + +func (k *ArrayKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNeedsCredentials +} diff --git a/vendor/github.com/99designs/keyring/config.go b/vendor/github.com/99designs/keyring/config.go new file mode 100644 index 0000000..590af7c --- /dev/null +++ b/vendor/github.com/99designs/keyring/config.go @@ -0,0 +1,58 @@ +package keyring + +// Config contains configuration for keyring. +type Config struct { + // AllowedBackends is a whitelist of backend providers that can be used. Nil means all available. + AllowedBackends []BackendType + + // ServiceName is a generic service name that is used by backends that support the concept + ServiceName string + + // MacOSKeychainNameKeychainName is the name of the macOS keychain that is used + KeychainName string + + // KeychainTrustApplication is whether the calling application should be trusted by default by items + KeychainTrustApplication bool + + // KeychainSynchronizable is whether the item can be synchronized to iCloud + KeychainSynchronizable bool + + // KeychainAccessibleWhenUnlocked is whether the item is accessible when the device is locked + KeychainAccessibleWhenUnlocked bool + + // KeychainPasswordFunc is an optional function used to prompt the user for a password + KeychainPasswordFunc PromptFunc + + // FilePasswordFunc is a required function used to prompt the user for a password + FilePasswordFunc PromptFunc + + // FileDir is the directory that keyring files are stored in, ~/ is resolved to the users' home dir + FileDir string + + // KeyCtlScope is the scope of the kernel keyring (either "user", "session", "process" or "thread") + KeyCtlScope string + + // KeyCtlPerm is the permission mask to use for new keys + KeyCtlPerm uint32 + + // KWalletAppID is the application id for KWallet + KWalletAppID string + + // KWalletFolder is the folder for KWallet + KWalletFolder string + + // LibSecretCollectionName is the name collection in secret-service + LibSecretCollectionName string + + // PassDir is the pass password-store directory, ~/ is resolved to the users' home dir + PassDir string + + // PassCmd is the name of the pass executable + PassCmd string + + // PassPrefix is a string prefix to prepend to the item path stored in pass + PassPrefix string + + // WinCredPrefix is a string prefix to prepend to the key name + WinCredPrefix string +} diff --git a/vendor/github.com/99designs/keyring/docker-compose.yml b/vendor/github.com/99designs/keyring/docker-compose.yml new file mode 100644 index 0000000..9020220 --- /dev/null +++ b/vendor/github.com/99designs/keyring/docker-compose.yml @@ -0,0 +1,7 @@ +version: "3.9" +services: + keyring: + image: golang:1.19 + volumes: + - .:/usr/local/src/keyring + working_dir: /usr/local/src/keyring diff --git a/vendor/github.com/99designs/keyring/file.go b/vendor/github.com/99designs/keyring/file.go new file mode 100644 index 0000000..0d25b57 --- /dev/null +++ b/vendor/github.com/99designs/keyring/file.go @@ -0,0 +1,180 @@ +package keyring + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + jose "github.com/dvsekhvalnov/jose2go" + "github.com/mtibben/percent" +) + +func init() { + supportedBackends[FileBackend] = opener(func(cfg Config) (Keyring, error) { + return &fileKeyring{ + dir: cfg.FileDir, + passwordFunc: cfg.FilePasswordFunc, + }, nil + }) +} + +var filenameEscape = func(s string) string { + return percent.Encode(s, "/") +} +var filenameUnescape = percent.Decode + +type fileKeyring struct { + dir string + passwordFunc PromptFunc + password string +} + +func (k *fileKeyring) resolveDir() (string, error) { + if k.dir == "" { + return "", fmt.Errorf("No directory provided for file keyring") + } + + dir, err := ExpandTilde(k.dir) + if err != nil { + return "", err + } + + stat, err := os.Stat(dir) + if os.IsNotExist(err) { + err = os.MkdirAll(dir, 0700) + } else if err != nil && stat != nil && !stat.IsDir() { + err = fmt.Errorf("%s is a file, not a directory", dir) + } + + return dir, err +} + +func (k *fileKeyring) unlock() error { + dir, err := k.resolveDir() + if err != nil { + return err + } + + if k.password == "" { + pwd, err := k.passwordFunc(fmt.Sprintf("Enter passphrase to unlock %q", dir)) + if err != nil { + return err + } + k.password = pwd + } + + return nil +} + +func (k *fileKeyring) Get(key string) (Item, error) { + filename, err := k.filename(key) + if err != nil { + return Item{}, err + } + + bytes, err := os.ReadFile(filename) + if os.IsNotExist(err) { + return Item{}, ErrKeyNotFound + } else if err != nil { + return Item{}, err + } + + if err = k.unlock(); err != nil { + return Item{}, err + } + + payload, _, err := jose.Decode(string(bytes), k.password) + if err != nil { + return Item{}, err + } + + var decoded Item + err = json.Unmarshal([]byte(payload), &decoded) + + return decoded, err +} + +func (k *fileKeyring) GetMetadata(key string) (Metadata, error) { + filename, err := k.filename(key) + if err != nil { + return Metadata{}, err + } + + stat, err := os.Stat(filename) + if os.IsNotExist(err) { + return Metadata{}, ErrKeyNotFound + } else if err != nil { + return Metadata{}, err + } + + // For the File provider, all internal data is encrypted, not just the + // credentials. Thus we only have the timestamps. Return a nil *Item. + // + // If we want to change this ... how portable are extended file attributes + // these days? Would it break user expectations of the security model to + // leak data into those? I'm hesitant to do so. + + return Metadata{ + ModificationTime: stat.ModTime(), + }, nil +} + +func (k *fileKeyring) Set(i Item) error { + bytes, err := json.Marshal(i) + if err != nil { + return err + } + + if err = k.unlock(); err != nil { + return err + } + + token, err := jose.Encrypt(string(bytes), jose.PBES2_HS256_A128KW, jose.A256GCM, k.password, + jose.Headers(map[string]interface{}{ + "created": time.Now().String(), + })) + if err != nil { + return err + } + + filename, err := k.filename(i.Key) + if err != nil { + return err + } + return os.WriteFile(filename, []byte(token), 0600) +} + +func (k *fileKeyring) filename(key string) (string, error) { + dir, err := k.resolveDir() + if err != nil { + return "", err + } + + return filepath.Join(dir, filenameEscape(key)), nil +} + +func (k *fileKeyring) Remove(key string) error { + filename, err := k.filename(key) + if err != nil { + return err + } + + return os.Remove(filename) +} + +func (k *fileKeyring) Keys() ([]string, error) { + dir, err := k.resolveDir() + if err != nil { + return nil, err + } + + var keys = []string{} + files, _ := os.ReadDir(dir) + for _, f := range files { + keys = append(keys, filenameUnescape(f.Name())) + } + + return keys, nil +} diff --git a/vendor/github.com/99designs/keyring/keychain.go b/vendor/github.com/99designs/keyring/keychain.go new file mode 100644 index 0000000..d4e634e --- /dev/null +++ b/vendor/github.com/99designs/keyring/keychain.go @@ -0,0 +1,301 @@ +//go:build darwin && cgo +// +build darwin,cgo + +package keyring + +import ( + "errors" + "fmt" + + gokeychain "github.com/99designs/go-keychain" +) + +type keychain struct { + path string + service string + + passwordFunc PromptFunc + + isSynchronizable bool + isAccessibleWhenUnlocked bool + isTrusted bool +} + +func init() { + supportedBackends[KeychainBackend] = opener(func(cfg Config) (Keyring, error) { + kc := &keychain{ + service: cfg.ServiceName, + passwordFunc: cfg.KeychainPasswordFunc, + + // Set the isAccessibleWhenUnlocked to the boolean value of + // KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value. + // See: https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked + isAccessibleWhenUnlocked: cfg.KeychainAccessibleWhenUnlocked, + } + if cfg.KeychainName != "" { + kc.path = cfg.KeychainName + ".keychain" + } + if cfg.KeychainTrustApplication { + kc.isTrusted = true + } + return kc, nil + }) +} + +func (k *keychain) Get(key string) (Item, error) { + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(k.service) + query.SetAccount(key) + query.SetMatchLimit(gokeychain.MatchLimitOne) + query.SetReturnAttributes(true) + query.SetReturnData(true) + + if k.path != "" { + // When we are querying, we don't create by default + query.SetMatchSearchList(gokeychain.NewWithPath(k.path)) + } + + debugf("Querying keychain for service=%q, account=%q, keychain=%q", k.service, key, k.path) + results, err := gokeychain.QueryItem(query) + if err == gokeychain.ErrorItemNotFound || len(results) == 0 { + debugf("No results found") + return Item{}, ErrKeyNotFound + } + + if err != nil { + debugf("Error: %#v", err) + return Item{}, err + } + + item := Item{ + Key: key, + Data: results[0].Data, + Label: results[0].Label, + Description: results[0].Description, + } + + debugf("Found item %q", results[0].Label) + return item, nil +} + +func (k *keychain) GetMetadata(key string) (Metadata, error) { + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(k.service) + query.SetAccount(key) + query.SetMatchLimit(gokeychain.MatchLimitOne) + query.SetReturnAttributes(true) + query.SetReturnData(false) + query.SetReturnRef(true) + + debugf("Querying keychain for metadata of service=%q, account=%q, keychain=%q", k.service, key, k.path) + results, err := gokeychain.QueryItem(query) + if err == gokeychain.ErrorItemNotFound || len(results) == 0 { + debugf("No results found") + return Metadata{}, ErrKeyNotFound + } else if err != nil { + debugf("Error: %#v", err) + return Metadata{}, err + } + + md := Metadata{ + Item: &Item{ + Key: key, + Label: results[0].Label, + Description: results[0].Description, + }, + ModificationTime: results[0].ModificationDate, + } + + debugf("Found metadata for %q", md.Item.Label) + + return md, nil +} + +func (k *keychain) updateItem(kc gokeychain.Keychain, kcItem gokeychain.Item, account string) error { + queryItem := gokeychain.NewItem() + queryItem.SetSecClass(gokeychain.SecClassGenericPassword) + queryItem.SetService(k.service) + queryItem.SetAccount(account) + queryItem.SetMatchLimit(gokeychain.MatchLimitOne) + queryItem.SetReturnAttributes(true) + + if k.path != "" { + queryItem.SetMatchSearchList(kc) + } + + results, err := gokeychain.QueryItem(queryItem) + if err != nil { + return fmt.Errorf("Failed to query keychain: %v", err) + } + if len(results) == 0 { + return errors.New("no results") + } + + // Don't call SetAccess() as this will cause multiple prompts on update, even when we are not updating the AccessList + kcItem.SetAccess(nil) + + if err := gokeychain.UpdateItem(queryItem, kcItem); err != nil { + return fmt.Errorf("Failed to update item in keychain: %v", err) + } + + return nil +} + +func (k *keychain) Set(item Item) error { + var kc gokeychain.Keychain + + // when we are setting a value, we create or open + if k.path != "" { + var err error + kc, err = k.createOrOpen() + if err != nil { + return err + } + } + + kcItem := gokeychain.NewItem() + kcItem.SetSecClass(gokeychain.SecClassGenericPassword) + kcItem.SetService(k.service) + kcItem.SetAccount(item.Key) + kcItem.SetLabel(item.Label) + kcItem.SetDescription(item.Description) + kcItem.SetData(item.Data) + + if k.path != "" { + kcItem.UseKeychain(kc) + } + + if k.isSynchronizable && !item.KeychainNotSynchronizable { + kcItem.SetSynchronizable(gokeychain.SynchronizableYes) + } + + if k.isAccessibleWhenUnlocked { + kcItem.SetAccessible(gokeychain.AccessibleWhenUnlocked) + } + + isTrusted := k.isTrusted && !item.KeychainNotTrustApplication + + if isTrusted { + debugf("Keychain item trusts keyring") + kcItem.SetAccess(&gokeychain.Access{ + Label: item.Label, + TrustedApplications: nil, + }) + } else { + debugf("Keychain item doesn't trust keyring") + kcItem.SetAccess(&gokeychain.Access{ + Label: item.Label, + TrustedApplications: []string{}, + }) + } + + debugf("Adding service=%q, label=%q, account=%q, trusted=%v to osx keychain %q", k.service, item.Label, item.Key, isTrusted, k.path) + + err := gokeychain.AddItem(kcItem) + + if err == gokeychain.ErrorDuplicateItem { + debugf("Item already exists, updating") + err = k.updateItem(kc, kcItem, item.Key) + } + + if err != nil { + return err + } + + return nil +} + +func (k *keychain) Remove(key string) error { + item := gokeychain.NewItem() + item.SetSecClass(gokeychain.SecClassGenericPassword) + item.SetService(k.service) + item.SetAccount(key) + + if k.path != "" { + kc := gokeychain.NewWithPath(k.path) + + if err := kc.Status(); err != nil { + if err == gokeychain.ErrorNoSuchKeychain { + return ErrKeyNotFound + } + return err + } + + item.SetMatchSearchList(kc) + } + + debugf("Removing keychain item service=%q, account=%q, keychain %q", k.service, key, k.path) + err := gokeychain.DeleteItem(item) + if err == gokeychain.ErrorItemNotFound { + return ErrKeyNotFound + } + + return err +} + +func (k *keychain) Keys() ([]string, error) { + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(k.service) + query.SetMatchLimit(gokeychain.MatchLimitAll) + query.SetReturnAttributes(true) + + if k.path != "" { + kc := gokeychain.NewWithPath(k.path) + + if err := kc.Status(); err != nil { + if err == gokeychain.ErrorNoSuchKeychain { + return []string{}, nil + } + return nil, err + } + + query.SetMatchSearchList(kc) + } + + debugf("Querying keychain for service=%q, keychain=%q", k.service, k.path) + results, err := gokeychain.QueryItem(query) + if err != nil { + return nil, err + } + + debugf("Found %d results", len(results)) + accountNames := make([]string, len(results)) + for idx, r := range results { + accountNames[idx] = r.Account + } + + return accountNames, nil +} + +func (k *keychain) createOrOpen() (gokeychain.Keychain, error) { + kc := gokeychain.NewWithPath(k.path) + + debugf("Checking keychain status") + err := kc.Status() + if err == nil { + debugf("Keychain status returned nil, keychain exists") + return kc, nil + } + + debugf("Keychain status returned error: %v", err) + + if err != gokeychain.ErrorNoSuchKeychain { + return gokeychain.Keychain{}, err + } + + if k.passwordFunc == nil { + debugf("Creating keychain %s with prompt", k.path) + return gokeychain.NewKeychainWithPrompt(k.path) + } + + passphrase, err := k.passwordFunc("Enter passphrase for keychain") + if err != nil { + return gokeychain.Keychain{}, err + } + + debugf("Creating keychain %s with provided password", k.path) + return gokeychain.NewKeychain(k.path, passphrase) +} diff --git a/vendor/github.com/99designs/keyring/keyctl.go b/vendor/github.com/99designs/keyring/keyctl.go new file mode 100644 index 0000000..bd8018a --- /dev/null +++ b/vendor/github.com/99designs/keyring/keyctl.go @@ -0,0 +1,327 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "errors" + "fmt" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +//nolint:revive +const ( + KEYCTL_PERM_VIEW = uint32(1 << 0) + KEYCTL_PERM_READ = uint32(1 << 1) + KEYCTL_PERM_WRITE = uint32(1 << 2) + KEYCTL_PERM_SEARCH = uint32(1 << 3) + KEYCTL_PERM_LINK = uint32(1 << 4) + KEYCTL_PERM_SETATTR = uint32(1 << 5) + KEYCTL_PERM_ALL = uint32((1 << 6) - 1) + + KEYCTL_PERM_OTHERS = 0 + KEYCTL_PERM_GROUP = 8 + KEYCTL_PERM_USER = 16 + KEYCTL_PERM_PROCESS = 24 +) + +// GetPermissions constructs the permission mask from the elements. +func GetPermissions(process, user, group, others uint32) uint32 { + perm := others << KEYCTL_PERM_OTHERS + perm |= group << KEYCTL_PERM_GROUP + perm |= user << KEYCTL_PERM_USER + perm |= process << KEYCTL_PERM_PROCESS + + return perm +} + +// GetKeyringIDForScope get the keyring ID for a given scope. +func GetKeyringIDForScope(scope string) (int32, error) { + ringRef, err := getKeyringForScope(scope) + if err != nil { + return 0, err + } + id, err := unix.KeyctlGetKeyringID(int(ringRef), false) + return int32(id), err +} + +type keyctlKeyring struct { + keyring int32 + perm uint32 +} + +func init() { + supportedBackends[KeyCtlBackend] = opener(func(cfg Config) (Keyring, error) { + keyring := keyctlKeyring{} + if cfg.KeyCtlPerm > 0 { + keyring.perm = cfg.KeyCtlPerm + } + + parent, err := getKeyringForScope(cfg.KeyCtlScope) + if err != nil { + return nil, fmt.Errorf("accessing %q keyring failed: %v", cfg.KeyCtlScope, err) + } + + // Check for named keyrings + keyring.keyring = parent + if cfg.ServiceName != "" { + namedKeyring, err := keyctlSearch(parent, "keyring", cfg.ServiceName) + if err != nil { + if !errors.Is(err, syscall.ENOKEY) { + return nil, fmt.Errorf("opening named %q keyring failed: %v", cfg.KeyCtlScope, err) + } + + // Keyring does not yet exist, create it + namedKeyring, err = keyring.createNamedKeyring(parent, cfg.ServiceName) + if err != nil { + return nil, fmt.Errorf("creating named %q keyring failed: %v", cfg.KeyCtlScope, err) + } + } + keyring.keyring = namedKeyring + } + + return &keyring, nil + }) +} + +func (k *keyctlKeyring) Get(name string) (Item, error) { + key, err := keyctlSearch(k.keyring, "user", name) + if err != nil { + if errors.Is(err, syscall.ENOKEY) { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + // data, err := key.Get() + data, err := keyctlRead(key) + if err != nil { + return Item{}, err + } + + item := Item{ + Key: name, + Data: data, + } + + return item, nil +} + +// GetMetadata for pass returns an error indicating that it's unsupported for this backend. +// TODO: We can deliver metadata different from the defined ones (e.g. permissions, expire-time, etc). +func (k *keyctlKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNotSupported +} + +func (k *keyctlKeyring) Set(item Item) error { + if k.perm == 0 { + // Keep the default permissions (alswrv-----v------------) + _, err := keyctlAdd(k.keyring, "user", item.Key, item.Data) + return err + } + + // By default we loose possession of the key in anything above the session keyring. + // Together with the default permissions (which cannot be changed during creation) we + // cannot change the permissions without possessing the key. Therefore, create the + // key in the session keyring, change permissions and then link to the target + // keyring and unlink from the intermediate keyring again. + key, err := keyctlAdd(unix.KEY_SPEC_SESSION_KEYRING, "user", item.Key, item.Data) + if err != nil { + return fmt.Errorf("adding key to session failed: %v", err) + } + + if err := keyctlSetperm(key, k.perm); err != nil { + return fmt.Errorf("setting permission 0x%x failed: %v", k.perm, err) + } + + if err := keyctlLink(k.keyring, key); err != nil { + return fmt.Errorf("linking key to keyring failed: %v", err) + } + + if err := keyctlUnlink(unix.KEY_SPEC_SESSION_KEYRING, key); err != nil { + return fmt.Errorf("unlinking key from session failed: %v", err) + } + + return nil +} + +func (k *keyctlKeyring) Remove(name string) error { + key, err := keyctlSearch(k.keyring, "user", name) + if err != nil { + return ErrKeyNotFound + } + + return keyctlUnlink(k.keyring, key) +} + +func (k *keyctlKeyring) Keys() ([]string, error) { + results := []string{} + + data, err := keyctlRead(k.keyring) + if err != nil { + return nil, fmt.Errorf("reading keyring failed: %v", err) + } + ids, err := keyctlConvertKeyBuffer(data) + if err != nil { + return nil, fmt.Errorf("converting raw keylist failed: %v", err) + } + + for _, id := range ids { + info, err := keyctlDescribe(id) + if err != nil { + return nil, err + } + if info["type"] == "user" { + results = append(results, info["description"]) + } + } + + return results, nil +} + +func (k *keyctlKeyring) createNamedKeyring(parent int32, name string) (int32, error) { + if k.perm == 0 { + // Keep the default permissions (alswrv-----v------------) + return keyctlAdd(parent, "keyring", name, nil) + } + + // By default we loose possession of the keyring in anything above the session keyring. + // Together with the default permissions (which cannot be changed during creation) we + // cannot change the permissions without possessing the keyring. Therefore, create the + // keyring linked to the session keyring, change permissions and then link to the target + // keyring and unlink from the intermediate keyring again. + keyring, err := keyctlAdd(unix.KEY_SPEC_SESSION_KEYRING, "keyring", name, nil) + if err != nil { + return 0, fmt.Errorf("creating keyring failed: %v", err) + } + + if err := keyctlSetperm(keyring, k.perm); err != nil { + return 0, fmt.Errorf("setting permission 0x%x failed: %v", k.perm, err) + } + + if err := keyctlLink(k.keyring, keyring); err != nil { + return 0, fmt.Errorf("linking keyring failed: %v", err) + } + + if err := keyctlUnlink(unix.KEY_SPEC_SESSION_KEYRING, keyring); err != nil { + return 0, fmt.Errorf("unlinking keyring from session failed: %v", err) + } + + return keyring, nil +} + +func getKeyringForScope(scope string) (int32, error) { + switch scope { + case "user": + return int32(unix.KEY_SPEC_USER_KEYRING), nil + case "usersession": + return int32(unix.KEY_SPEC_USER_SESSION_KEYRING), nil + case "group": + // Not yet implemented in the kernel + // return int32(unix.KEY_SPEC_GROUP_KEYRING) + return 0, fmt.Errorf("scope %q not yet implemented", scope) + case "session": + return int32(unix.KEY_SPEC_SESSION_KEYRING), nil + case "process": + return int32(unix.KEY_SPEC_PROCESS_KEYRING), nil + case "thread": + return int32(unix.KEY_SPEC_THREAD_KEYRING), nil + } + return 0, fmt.Errorf("unknown scope %q", scope) +} + +func keyctlAdd(parent int32, keytype, key string, data []byte) (int32, error) { + id, err := unix.AddKey(keytype, key, data, int(parent)) + if err != nil { + return 0, err + } + return int32(id), nil +} + +func keyctlSearch(id int32, idtype, name string) (int32, error) { + key, err := unix.KeyctlSearch(int(id), idtype, name, 0) + if err != nil { + return 0, err + } + return int32(key), nil +} + +func keyctlRead(id int32) ([]byte, error) { + var buffer []byte + + for { + length, err := unix.KeyctlBuffer(unix.KEYCTL_READ, int(id), buffer, 0) + if err != nil { + return nil, err + } + + // Return the buffer if it was large enough + if length <= len(buffer) { + return buffer[:length], nil + } + + // Next try with a larger buffer + buffer = make([]byte, length) + } +} + +func keyctlDescribe(id int32) (map[string]string, error) { + description, err := unix.KeyctlString(unix.KEYCTL_DESCRIBE, int(id)) + if err != nil { + return nil, err + } + fields := strings.Split(description, ";") + if len(fields) < 1 { + return nil, fmt.Errorf("no data") + } + + data := make(map[string]string) + names := []string{"type", "uid", "gid", "perm"} // according to keyctlDescribe(3) new fields are added at the end + data["description"] = fields[len(fields)-1] // according to keyctlDescribe(3) description is always last + for i, f := range fields[:len(fields)-1] { + if i >= len(names) { + // Do not stumble upon unknown fields + break + } + data[names[i]] = f + } + + return data, nil +} + +func keyctlLink(parent, child int32) error { + _, _, errno := syscall.Syscall(syscall.SYS_KEYCTL, uintptr(unix.KEYCTL_LINK), uintptr(child), uintptr(parent)) + if errno != 0 { + return errno + } + return nil +} + +func keyctlUnlink(parent, child int32) error { + _, _, errno := syscall.Syscall(syscall.SYS_KEYCTL, uintptr(unix.KEYCTL_UNLINK), uintptr(child), uintptr(parent)) + if errno != 0 { + return errno + } + return nil +} + +func keyctlSetperm(id int32, perm uint32) error { + return unix.KeyctlSetperm(int(id), perm) +} + +func keyctlConvertKeyBuffer(buffer []byte) ([]int32, error) { + if len(buffer)%4 != 0 { + return nil, fmt.Errorf("buffer size %d not a multiple of 4", len(buffer)) + } + + results := make([]int32, 0, len(buffer)/4) + for i := 0; i < len(buffer); i += 4 { + // We need to case in host-native endianess here as this is what we get from the kernel. + r := *((*int32)(unsafe.Pointer(&buffer[i]))) + results = append(results, r) + } + return results, nil +} diff --git a/vendor/github.com/99designs/keyring/keyring.go b/vendor/github.com/99designs/keyring/keyring.go new file mode 100644 index 0000000..12161b7 --- /dev/null +++ b/vendor/github.com/99designs/keyring/keyring.go @@ -0,0 +1,134 @@ +// Package keyring provides a uniform API over a range of desktop credential storage engines. +package keyring + +import ( + "errors" + "log" + "time" +) + +// BackendType is an identifier for a credential storage service. +type BackendType string + +// All currently supported secure storage backends. +const ( + InvalidBackend BackendType = "" + SecretServiceBackend BackendType = "secret-service" + KeychainBackend BackendType = "keychain" + KeyCtlBackend BackendType = "keyctl" + KWalletBackend BackendType = "kwallet" + WinCredBackend BackendType = "wincred" + FileBackend BackendType = "file" + PassBackend BackendType = "pass" +) + +// This order makes sure the OS-specific backends +// are picked over the more generic backends. +var backendOrder = []BackendType{ + // Windows + WinCredBackend, + // MacOS + KeychainBackend, + // Linux + SecretServiceBackend, + KWalletBackend, + KeyCtlBackend, + // General + PassBackend, + FileBackend, +} + +var supportedBackends = map[BackendType]opener{} + +// AvailableBackends provides a slice of all available backend keys on the current OS. +func AvailableBackends() []BackendType { + b := []BackendType{} + for _, k := range backendOrder { + _, ok := supportedBackends[k] + if ok { + b = append(b, k) + } + } + return b +} + +type opener func(cfg Config) (Keyring, error) + +// Open will open a specific keyring backend. +func Open(cfg Config) (Keyring, error) { + if cfg.AllowedBackends == nil { + cfg.AllowedBackends = AvailableBackends() + } + debugf("Considering backends: %v", cfg.AllowedBackends) + for _, backend := range cfg.AllowedBackends { + if opener, ok := supportedBackends[backend]; ok { + openBackend, err := opener(cfg) + if err != nil { + debugf("Failed backend %s: %s", backend, err) + continue + } + return openBackend, nil + } + } + return nil, ErrNoAvailImpl +} + +// Item is a thing stored on the keyring. +type Item struct { + Key string + Data []byte + Label string + Description string + + // Backend specific config + KeychainNotTrustApplication bool + KeychainNotSynchronizable bool +} + +// Metadata is information about a thing stored on the keyring; retrieving +// metadata must not require authentication. The embedded Item should be +// filled in with an empty Data field. +// It's allowed for Item to be a nil pointer, indicating that all we +// have is the timestamps. +type Metadata struct { + *Item + ModificationTime time.Time +} + +// Keyring provides the uniform interface over the underlying backends. +type Keyring interface { + // Returns an Item matching the key or ErrKeyNotFound + Get(key string) (Item, error) + // Returns the non-secret parts of an Item + GetMetadata(key string) (Metadata, error) + // Stores an Item on the keyring + Set(item Item) error + // Removes the item with matching key + Remove(key string) error + // Provides a slice of all keys stored on the keyring + Keys() ([]string, error) +} + +// ErrNoAvailImpl is returned by Open when a backend cannot be found. +var ErrNoAvailImpl = errors.New("Specified keyring backend not available") + +// ErrKeyNotFound is returned by Keyring Get when the item is not on the keyring. +var ErrKeyNotFound = errors.New("The specified item could not be found in the keyring") + +// ErrMetadataNeedsCredentials is returned when Metadata is called against a +// backend which requires credentials even to see metadata. +var ErrMetadataNeedsCredentials = errors.New("The keyring backend requires credentials for metadata access") + +// ErrMetadataNotSupported is returned when Metadata is not available for the backend. +var ErrMetadataNotSupported = errors.New("The keyring backend does not support metadata access") + +var ( + // Debug specifies whether to print debugging output. + Debug bool +) + +func debugf(pattern string, args ...interface{}) { + if Debug { + log.Printf("[keyring] "+pattern, args...) + } +} diff --git a/vendor/github.com/99designs/keyring/kwallet.go b/vendor/github.com/99designs/keyring/kwallet.go new file mode 100644 index 0000000..771baba --- /dev/null +++ b/vendor/github.com/99designs/keyring/kwallet.go @@ -0,0 +1,237 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "encoding/json" + "os" + + "github.com/godbus/dbus" +) + +const ( + dbusServiceName = "org.kde.kwalletd5" + dbusPath = "/modules/kwalletd5" +) + +func init() { + if os.Getenv("DISABLE_KWALLET") == "1" { + return + } + + // silently fail if dbus isn't available + _, err := dbus.SessionBus() + if err != nil { + return + } + + supportedBackends[KWalletBackend] = opener(func(cfg Config) (Keyring, error) { + if cfg.ServiceName == "" { + cfg.ServiceName = "kdewallet" + } + + if cfg.KWalletAppID == "" { + cfg.KWalletAppID = "keyring" + } + + if cfg.KWalletFolder == "" { + cfg.KWalletFolder = "keyring" + } + + wallet, err := newKwallet() + if err != nil { + return nil, err + } + + ring := &kwalletKeyring{ + wallet: *wallet, + name: cfg.ServiceName, + appID: cfg.KWalletAppID, + folder: cfg.KWalletFolder, + } + + return ring, ring.openWallet() + }) +} + +type kwalletKeyring struct { + wallet kwalletBinding + name string + handle int32 + appID string + folder string +} + +func (k *kwalletKeyring) openWallet() error { + isOpen, err := k.wallet.IsOpen(k.handle) + if err != nil { + return err + } + + if !isOpen { + handle, err := k.wallet.Open(k.name, 0, k.appID) + if err != nil { + return err + } + k.handle = handle + } + + return nil +} + +func (k *kwalletKeyring) Get(key string) (Item, error) { + err := k.openWallet() + if err != nil { + return Item{}, err + } + + data, err := k.wallet.ReadEntry(k.handle, k.folder, key, k.appID) + if err != nil { + return Item{}, err + } + if len(data) == 0 { + return Item{}, ErrKeyNotFound + } + + item := Item{} + err = json.Unmarshal(data, &item) + if err != nil { + return Item{}, err + } + + return item, nil +} + +// GetMetadata for kwallet returns an error indicating that it's unsupported +// for this backend. +// +// The only APIs found around KWallet are for retrieving content, no indication +// found in docs for methods to use to retrieve metadata without needing unlock +// credentials. +func (k *kwalletKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNeedsCredentials +} + +func (k *kwalletKeyring) Set(item Item) error { + err := k.openWallet() + if err != nil { + return err + } + + data, err := json.Marshal(item) + if err != nil { + return err + } + + err = k.wallet.WriteEntry(k.handle, k.folder, item.Key, data, k.appID) + if err != nil { + return err + } + + return nil +} + +func (k *kwalletKeyring) Remove(key string) error { + err := k.openWallet() + if err != nil { + return err + } + + err = k.wallet.RemoveEntry(k.handle, k.folder, key, k.appID) + if err != nil { + return err + } + + return nil +} + +func (k *kwalletKeyring) Keys() ([]string, error) { + err := k.openWallet() + if err != nil { + return []string{}, err + } + + entries, err := k.wallet.EntryList(k.handle, k.folder, k.appID) + if err != nil { + return []string{}, err + } + + return entries, nil +} + +func newKwallet() (*kwalletBinding, error) { + conn, err := dbus.SessionBus() + if err != nil { + return nil, err + } + + return &kwalletBinding{ + conn.Object(dbusServiceName, dbusPath), + }, nil +} + +// Dumb Dbus bindings for kwallet bindings with types. +type kwalletBinding struct { + dbus dbus.BusObject +} + +// method bool org.kde.KWallet.isOpen(int handle) +func (k *kwalletBinding) IsOpen(handle int32) (bool, error) { + call := k.dbus.Call("org.kde.KWallet.isOpen", 0, handle) + if call.Err != nil { + return false, call.Err + } + + return call.Body[0].(bool), call.Err +} + +// method int org.kde.KWallet.open(QString wallet, qlonglong wId, QString appid) +func (k *kwalletBinding) Open(name string, wID int64, appid string) (int32, error) { + call := k.dbus.Call("org.kde.KWallet.open", 0, name, wID, appid) + if call.Err != nil { + return 0, call.Err + } + + return call.Body[0].(int32), call.Err +} + +// method QStringList org.kde.KWallet.entryList(int handle, QString folder, QString appid) +func (k *kwalletBinding) EntryList(handle int32, folder string, appid string) ([]string, error) { + call := k.dbus.Call("org.kde.KWallet.entryList", 0, handle, folder, appid) + if call.Err != nil { + return []string{}, call.Err + } + + return call.Body[0].([]string), call.Err +} + +// method int org.kde.KWallet.writeEntry(int handle, QString folder, QString key, QByteArray value, QString appid) +func (k *kwalletBinding) WriteEntry(handle int32, folder string, key string, value []byte, appid string) error { + call := k.dbus.Call("org.kde.KWallet.writeEntry", 0, handle, folder, key, value, appid) + if call.Err != nil { + return call.Err + } + + return call.Err +} + +// method int org.kde.KWallet.removeEntry(int handle, QString folder, QString key, QString appid) +func (k *kwalletBinding) RemoveEntry(handle int32, folder string, key string, appid string) error { + call := k.dbus.Call("org.kde.KWallet.removeEntry", 0, handle, folder, key, appid) + if call.Err != nil { + return call.Err + } + + return call.Err +} + +// method QByteArray org.kde.KWallet.readEntry(int handle, QString folder, QString key, QString appid) +func (k *kwalletBinding) ReadEntry(handle int32, folder string, key string, appid string) ([]byte, error) { + call := k.dbus.Call("org.kde.KWallet.readEntry", 0, handle, folder, key, appid) + if call.Err != nil { + return []byte{}, call.Err + } + + return call.Body[0].([]byte), call.Err +} diff --git a/vendor/github.com/99designs/keyring/pass.go b/vendor/github.com/99designs/keyring/pass.go new file mode 100644 index 0000000..5ca0bf0 --- /dev/null +++ b/vendor/github.com/99designs/keyring/pass.go @@ -0,0 +1,166 @@ +//go:build !windows +// +build !windows + +package keyring + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func init() { + supportedBackends[PassBackend] = opener(func(cfg Config) (Keyring, error) { + var err error + + pass := &passKeyring{ + passcmd: cfg.PassCmd, + dir: cfg.PassDir, + prefix: cfg.PassPrefix, + } + + if pass.passcmd == "" { + pass.passcmd = "pass" + } + + if pass.dir == "" { + if passDir, found := os.LookupEnv("PASSWORD_STORE_DIR"); found { + pass.dir = passDir + } else { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + pass.dir = filepath.Join(homeDir, ".password-store") + } + } + + pass.dir, err = ExpandTilde(pass.dir) + if err != nil { + return nil, err + } + + // fail if the pass program is not available + _, err = exec.LookPath(pass.passcmd) + if err != nil { + return nil, errors.New("The pass program is not available") + } + + return pass, nil + }) +} + +type passKeyring struct { + dir string + passcmd string + prefix string +} + +func (k *passKeyring) pass(args ...string) *exec.Cmd { + cmd := exec.Command(k.passcmd, args...) + if k.dir != "" { + cmd.Env = append(os.Environ(), fmt.Sprintf("PASSWORD_STORE_DIR=%s", k.dir)) + } + cmd.Stderr = os.Stderr + + return cmd +} + +func (k *passKeyring) Get(key string) (Item, error) { + if !k.itemExists(key) { + return Item{}, ErrKeyNotFound + } + + name := filepath.Join(k.prefix, key) + cmd := k.pass("show", name) + output, err := cmd.Output() + if err != nil { + return Item{}, err + } + + var decoded Item + err = json.Unmarshal(output, &decoded) + + return decoded, err +} + +func (k *passKeyring) GetMetadata(key string) (Metadata, error) { + return Metadata{}, nil +} + +func (k *passKeyring) Set(i Item) error { + bytes, err := json.Marshal(i) + if err != nil { + return err + } + + name := filepath.Join(k.prefix, i.Key) + cmd := k.pass("insert", "-m", "-f", name) + cmd.Stdin = strings.NewReader(string(bytes)) + + err = cmd.Run() + if err != nil { + return err + } + + return nil +} + +func (k *passKeyring) Remove(key string) error { + if !k.itemExists(key) { + return ErrKeyNotFound + } + + name := filepath.Join(k.prefix, key) + cmd := k.pass("rm", "-f", name) + err := cmd.Run() + if err != nil { + return err + } + + return nil +} + +func (k *passKeyring) itemExists(key string) bool { + var path = filepath.Join(k.dir, k.prefix, key+".gpg") + _, err := os.Stat(path) + + return err == nil +} + +func (k *passKeyring) Keys() ([]string, error) { + var keys = []string{} + var path = filepath.Join(k.dir, k.prefix) + + info, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return keys, nil + } + return keys, err + } + if !info.IsDir() { + return keys, fmt.Errorf("%s is not a directory", path) + } + + err = filepath.Walk(path, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() && filepath.Ext(p) == ".gpg" { + name := strings.TrimPrefix(p, path) + if name[0] == os.PathSeparator { + name = name[1:] + } + keys = append(keys, name[:len(name)-4]) + } + return nil + }) + + return keys, err +} diff --git a/vendor/github.com/99designs/keyring/prompt.go b/vendor/github.com/99designs/keyring/prompt.go new file mode 100644 index 0000000..59ad81c --- /dev/null +++ b/vendor/github.com/99designs/keyring/prompt.go @@ -0,0 +1,27 @@ +package keyring + +import ( + "fmt" + "os" + + "golang.org/x/term" +) + +// PromptFunc is a function used to prompt the user for a password. +type PromptFunc func(string) (string, error) + +func TerminalPrompt(prompt string) (string, error) { + fmt.Printf("%s: ", prompt) + b, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", err + } + fmt.Println() + return string(b), nil +} + +func FixedStringPrompt(value string) PromptFunc { + return func(_ string) (string, error) { + return value, nil + } +} diff --git a/vendor/github.com/99designs/keyring/secretservice.go b/vendor/github.com/99designs/keyring/secretservice.go new file mode 100644 index 0000000..2a1f9d9 --- /dev/null +++ b/vendor/github.com/99designs/keyring/secretservice.go @@ -0,0 +1,293 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "encoding/hex" + "encoding/json" + "errors" + + "strings" + + "github.com/godbus/dbus" + "github.com/gsterjov/go-libsecret" +) + +func init() { + // silently fail if dbus isn't available + _, err := dbus.SessionBus() + if err != nil { + return + } + + supportedBackends[SecretServiceBackend] = opener(func(cfg Config) (Keyring, error) { + if cfg.ServiceName == "" { + cfg.ServiceName = "secret-service" + } + if cfg.LibSecretCollectionName == "" { + cfg.LibSecretCollectionName = cfg.ServiceName + } + + service, err := libsecret.NewService() + if err != nil { + return &secretsKeyring{}, err + } + + ring := &secretsKeyring{ + name: cfg.LibSecretCollectionName, + service: service, + } + + return ring, ring.openSecrets() + }) +} + +type secretsKeyring struct { + name string + service *libsecret.Service + collection *libsecret.Collection + session *libsecret.Session +} + +var errCollectionNotFound = errors.New("The collection does not exist. Please add a key first") + +func decodeKeyringString(src string) string { + var dst strings.Builder + for i := 0; i < len(src); i++ { + if src[i] != '_' { + dst.WriteString(string(src[i])) + } else { + if i+3 > len(src) { + return src + } + hexstring := src[i+1 : i+3] + decoded, err := hex.DecodeString(hexstring) + if err != nil { + return src + } + dst.Write(decoded) + i += 2 + } + } + return dst.String() +} + +func (k *secretsKeyring) openSecrets() error { + session, err := k.service.Open() + if err != nil { + return err + } + k.session = session + + // get the collection if it already exists + collections, err := k.service.Collections() + if err != nil { + return err + } + + path := libsecret.DBusPath + "/collection/" + k.name + + for _, collection := range collections { + if decodeKeyringString(string(collection.Path())) == path { + c := collection // fix variable into the local variable to ensure it's referenced correctly, see https://github.com/kyoh86/exportloopref + k.collection = &c + return nil + } + } + + return nil +} + +func (k *secretsKeyring) openCollection() error { + if err := k.openSecrets(); err != nil { + return err + } + + if k.collection == nil { + return errCollectionNotFound + // return &secretsError{fmt.Sprintf( + // "The collection %q does not exist. Please add a key first", + // k.name, + // )} + } + + return nil +} + +func (k *secretsKeyring) Get(key string) (Item, error) { + if err := k.openCollection(); err != nil { + if err == errCollectionNotFound { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + + items, err := k.collection.SearchItems(key) + if err != nil { + return Item{}, err + } + + if len(items) == 0 { + return Item{}, ErrKeyNotFound + } + + // use the first item whenever there are multiples + // with the same profile name + item := items[0] + + locked, err := item.Locked() + if err != nil { + return Item{}, err + } + + if locked { + if err := k.service.Unlock(item); err != nil { + return Item{}, err + } + } + + secret, err := item.GetSecret(k.session) + if err != nil { + return Item{}, err + } + + // pack the secret into the item + var ret Item + if err = json.Unmarshal(secret.Value, &ret); err != nil { + return Item{}, err + } + + return ret, err +} + +// GetMetadata for libsecret returns an error indicating that it's unsupported +// for this backend. +// +// libsecret actually implements a metadata system which we could use, "Secret +// Attributes"; I found no indication in documentation of anything like an +// automatically maintained last-modification timestamp, so to use this we'd +// need to have a SetMetadata API too. Which we're not yet doing, but feel +// free to contribute patches. +func (k *secretsKeyring) GetMetadata(key string) (Metadata, error) { + return Metadata{}, ErrMetadataNeedsCredentials +} + +func (k *secretsKeyring) Set(item Item) error { + err := k.openSecrets() + if err != nil { + return err + } + + // create the collection if it doesn't already exist + if k.collection == nil { + collection, err := k.service.CreateCollection(k.name) + if err != nil { + return err + } + + k.collection = collection + } + + if err := k.ensureCollectionUnlocked(); err != nil { + return err + } + + // create the new item + data, err := json.Marshal(item) + if err != nil { + return err + } + + secret := libsecret.NewSecret(k.session, []byte{}, data, "application/json") + + if _, err := k.collection.CreateItem(item.Key, secret, true); err != nil { + return err + } + + return nil +} + +func (k *secretsKeyring) Remove(key string) error { + if err := k.openCollection(); err != nil { + if err == errCollectionNotFound { + return ErrKeyNotFound + } + return err + } + + items, err := k.collection.SearchItems(key) + if err != nil { + return err + } + + // nothing to delete + if len(items) == 0 { + return nil + } + + // we dont want to delete more than one anyway + // so just get the first item found + item := items[0] + + locked, err := item.Locked() + if err != nil { + return err + } + + if locked { + if err := k.service.Unlock(item); err != nil { + return err + } + } + + if err := item.Delete(); err != nil { + return err + } + + return nil +} + +func (k *secretsKeyring) Keys() ([]string, error) { + if err := k.openCollection(); err != nil { + if err == errCollectionNotFound { + return []string{}, nil + } + return nil, err + } + if err := k.ensureCollectionUnlocked(); err != nil { + return nil, err + } + items, err := k.collection.Items() + if err != nil { + return nil, err + } + keys := []string{} + for _, item := range items { + label, err := item.Label() // FIXME: err is being silently ignored + if err == nil { + keys = append(keys, label) + } + } + return keys, nil +} + +// deleteCollection deletes the keyring's collection if it exists. This is mainly to support testing. +func (k *secretsKeyring) deleteCollection() error { + if err := k.openCollection(); err != nil { + return err + } + return k.collection.Delete() +} + +// unlock the collection if it's locked +func (k *secretsKeyring) ensureCollectionUnlocked() error { + locked, err := k.collection.Locked() + if err != nil { + return err + } + if !locked { + return nil + } + return k.service.Unlock(k.collection) +} diff --git a/vendor/github.com/99designs/keyring/tilde.go b/vendor/github.com/99designs/keyring/tilde.go new file mode 100644 index 0000000..c1847a6 --- /dev/null +++ b/vendor/github.com/99designs/keyring/tilde.go @@ -0,0 +1,22 @@ +package keyring + +import ( + "os" + "path/filepath" + "strings" +) + +var tildePrefix = string([]rune{'~', filepath.Separator}) + +// ExpandTilde will expand tilde (~/ or ~\ depending on OS) for the user home directory. +func ExpandTilde(dir string) (string, error) { + if strings.HasPrefix(dir, tildePrefix) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + dir = strings.Replace(dir, "~", homeDir, 1) + debugf("Expanded file dir to %s", dir) + } + return dir, nil +} diff --git a/vendor/github.com/99designs/keyring/wincred.go b/vendor/github.com/99designs/keyring/wincred.go new file mode 100644 index 0000000..2ed43f2 --- /dev/null +++ b/vendor/github.com/99designs/keyring/wincred.go @@ -0,0 +1,98 @@ +//go:build windows +// +build windows + +package keyring + +import ( + "strings" + "syscall" + + "github.com/danieljoos/wincred" +) + +// ERROR_NOT_FOUND from https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--1000-1299- +const elementNotFoundError = syscall.Errno(1168) + +type windowsKeyring struct { + name string + prefix string +} + +func init() { + supportedBackends[WinCredBackend] = opener(func(cfg Config) (Keyring, error) { + name := cfg.ServiceName + if name == "" { + name = "default" + } + + prefix := cfg.WinCredPrefix + if prefix == "" { + prefix = "keyring" + } + + return &windowsKeyring{ + name: name, + prefix: prefix, + }, nil + }) +} + +func (k *windowsKeyring) Get(key string) (Item, error) { + cred, err := wincred.GetGenericCredential(k.credentialName(key)) + if err != nil { + if err == elementNotFoundError { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + + item := Item{ + Key: key, + Data: cred.CredentialBlob, + } + + return item, nil +} + +// GetMetadata for pass returns an error indicating that it's unsupported +// for this backend. +// TODO: This is a stub. Look into whether pass would support metadata in a usable way for keyring. +func (k *windowsKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNotSupported +} + +func (k *windowsKeyring) Set(item Item) error { + cred := wincred.NewGenericCredential(k.credentialName(item.Key)) + cred.CredentialBlob = item.Data + return cred.Write() +} + +func (k *windowsKeyring) Remove(key string) error { + cred, err := wincred.GetGenericCredential(k.credentialName(key)) + if err != nil { + if err == elementNotFoundError { + return ErrKeyNotFound + } + return err + } + return cred.Delete() +} + +func (k *windowsKeyring) Keys() ([]string, error) { + results := []string{} + + if creds, err := wincred.List(); err == nil { + for _, cred := range creds { + prefix := k.credentialName("") + if strings.HasPrefix(cred.TargetName, prefix) { + results = append(results, strings.TrimPrefix(cred.TargetName, prefix)) + } + } + } + + return results, nil +} + +func (k *windowsKeyring) credentialName(key string) string { + return k.prefix + ":" + k.name + ":" + key +} |
