cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / keyring / tools.go
tools.go 4.1K
1package keyring23import (4 "bytes"5 "context"6 "encoding/hex"7 "errors"8 "fmt"9 "os/exec"10 "strings"11)1213// runFunc runs a program with args, writing stdin to it, and returns its14// stdout. Tests replace it to see exactly what would run.15type runFunc func(ctx context.Context, name string, args []string, stdin string) (string, error)1617// exitError is a program that ran and failed.18type exitError struct {19 code int20 stderr string21}2223func (e *exitError) Error() string {24 if e.stderr != "" {25 return fmt.Sprintf("exit status %d: %s", e.code, e.stderr)26 }27 return fmt.Sprintf("exit status %d", e.code)28}2930func run(ctx context.Context, name string, args []string, stdin string) (string, error) {31 cmd := exec.CommandContext(ctx, name, args...)32 cmd.Stdin = strings.NewReader(stdin)33 var out, errOut bytes.Buffer34 cmd.Stdout, cmd.Stderr = &out, &errOut35 err := cmd.Run()36 var ee *exec.ExitError37 if errors.As(err, &ee) {38 return out.String(), &exitError{code: ee.ExitCode(), stderr: firstLine(errOut.String())}39 }40 return out.String(), err41}4243func firstLine(s string) string {44 s, _, _ = strings.Cut(strings.TrimSpace(s), "\n")45 return strings.TrimSpace(s)46}4748func exitCode(err error) int {49 var ee *exitError50 if errors.As(err, &ee) {51 return ee.code52 }53 return -154}5556// keychain is the macOS Keychain, through /usr/bin/security. The secret57// is written with "security -i", which reads its command from stdin, as58// hex (-X), so it's never an argument of any process.59type keychain struct{ run runFunc }6061// errItemNotFound is security's exit status for a missing item.62const errItemNotFound = 446364const securityPath = "/usr/bin/security"6566func (k keychain) Name() string { return "macOS Keychain" }6768func (k keychain) Get(ctx context.Context) (string, error) {69 out, err := k.run(ctx, securityPath, []string{"find-generic-password", "-s", Service, "-a", Account, "-w"}, "")70 if exitCode(err) == errItemNotFound {71 return "", ErrNotFound72 }73 if err != nil {74 return "", fmt.Errorf("keychain: %w", err)75 }76 return strings.TrimRight(out, "\r\n"), nil77}7879func (k keychain) Set(ctx context.Context, secret string) error {80 cmd := fmt.Sprintf("add-generic-password -U -s %s -a %s -l %q -X %s\n", Service, Account, Label, hex.EncodeToString([]byte(secret)))81 if _, err := k.run(ctx, securityPath, []string{"-i"}, cmd); err != nil {82 return fmt.Errorf("keychain: %w", err)83 }84 return nil85}8687func (k keychain) Delete(ctx context.Context) error {88 _, err := k.run(ctx, securityPath, []string{"delete-generic-password", "-s", Service, "-a", Account}, "")89 if exitCode(err) == errItemNotFound {90 return ErrNotFound91 }92 if err != nil {93 return fmt.Errorf("keychain: %w", err)94 }95 return nil96}9798// secretService is the freedesktop Secret Service (GNOME Keyring,99// KWallet, KeePassXC), through libsecret's secret-tool, which reads the100// secret to store from stdin.101type secretService struct{ run runFunc }102103func (s secretService) Name() string { return "Secret Service" }104105func attrs(verb string, extra ...string) []string {106 return append(append([]string{verb}, extra...), "service", Service, "account", Account)107}108109func (s secretService) Get(ctx context.Context) (string, error) {110 out, err := s.run(ctx, "secret-tool", attrs("lookup"), "")111 var ee *exitError112 switch {113 case err == nil && out != "":114 return strings.TrimRight(out, "\r\n"), nil115 case err == nil, errors.As(err, &ee) && ee.code == 1 && ee.stderr == "":116 return "", ErrNotFound // secret-tool exits 1, silently, when nothing matches117 }118 return "", s.wrap(err)119}120121func (s secretService) Set(ctx context.Context, secret string) error {122 _, err := s.run(ctx, "secret-tool", attrs("store", "--label="+Label), secret)123 return s.wrap(err)124}125126func (s secretService) Delete(ctx context.Context) error {127 if _, err := s.Get(ctx); err != nil {128 return err129 }130 _, err := s.run(ctx, "secret-tool", attrs("clear"), "")131 return s.wrap(err)132}133134func (s secretService) wrap(err error) error {135 if errors.Is(err, exec.ErrNotFound) {136 return errors.New("secret-tool isn't installed (libsecret-tools on Debian and Ubuntu, libsecret on Fedora and Arch)")137 }138 if err != nil {139 return fmt.Errorf("secret service: %w", err)140 }141 return nil142}