Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ jobs:
#go-version-file: 'go.mod'
check-latest: true
- run: sudo apt-get update
- run: sudo apt-get install pass gnome-keyring dbus-x11
- run: sudo apt-get install pass gnome-keyring dbus-x11 age
- run: |
git clone https://github.com/FiloSottile/passage /tmp/passage
cd /tmp/passage
sudo make install

- run: go test -race ./...
mac:
runs-on: macos-latest
Expand All @@ -35,5 +40,11 @@ jobs:
#go-version: 1.19
go-version-file: 'go.mod'
check-latest: true
- run: brew install pass
- run: brew install pass age
- run: |
git clone https://github.com/FiloSottile/passage /tmp/passage
cd /tmp/passage
make install PREFIX="$(brew --cellar)/passage/$(git describe --tags)"
brew link passage

- run: go test -race ./...
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Currently Keyring supports the following backends
* 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/)
* [Passage](https://github.com/FiloSottile/passage)
* [Encrypted file (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)
* [KeyCtl](https://linux.die.net/man/1/keyctl)
* [1Password Connect](https://developer.1password.com/docs/connect/)
Expand Down
2 changes: 2 additions & 0 deletions keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
WinCredBackend BackendType = "wincred"
FileBackend BackendType = "file"
PassBackend BackendType = "pass"
PassageBackend BackendType = "passage"
OPBackend BackendType = "op"
OPConnectBackend BackendType = "op-connect"
OPDesktopBackend BackendType = "op-desktop"
Expand All @@ -42,6 +43,7 @@ var backendOrder = []BackendType{
KeyCtlBackend,
// General
PassBackend,
PassageBackend,
FileBackend,
// 1Password
OPConnectBackend,
Expand Down
165 changes: 165 additions & 0 deletions passage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//go:build !windows

package keyring

import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)

func init() {
supportedBackends[PassageBackend] = opener(func(cfg Config) (Keyring, error) {
var err error

passage := &passageKeyring{
passcmd: cfg.PassCmd,
dir: cfg.PassDir,
prefix: cfg.PassPrefix,
}

if passage.passcmd == "" {
passage.passcmd = "passage"
}

if passage.dir == "" {
if passDir, found := os.LookupEnv("PASSAGE_DIR"); found {
passage.dir = passDir
} else {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
passage.dir = filepath.Join(homeDir, ".passage/store")
}
}

passage.dir, err = ExpandTilde(passage.dir)
if err != nil {
return nil, err
}

// fail if the pass program is not available
_, err = exec.LookPath(passage.passcmd)
if err != nil {
return nil, errors.New("The passage program is not available")
}

return passage, nil
})
}

type passageKeyring struct {
dir string
passcmd string
prefix string
}

func (k *passageKeyring) pass(args ...string) *exec.Cmd {
cmd := exec.Command(k.passcmd, args...)
if k.dir != "" {
cmd.Env = append(os.Environ(), fmt.Sprintf("PASSAGE_DIR=%s", k.dir))
}
cmd.Stderr = os.Stderr

return cmd
}

func (k *passageKeyring) 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 *passageKeyring) GetMetadata(key string) (Metadata, error) {
return Metadata{}, nil
}

func (k *passageKeyring) 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 *passageKeyring) 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 *passageKeyring) itemExists(key string) bool {
var path = filepath.Join(k.dir, k.prefix, key+".age")
_, err := os.Stat(path)

return err == nil
}

func (k *passageKeyring) 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) == ".age" {
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
}
Loading