mirror of
https://github.com/gopasspw/gopass.git
synced 2026-10-04 14:57:51 +02:00
165 lines
3.4 KiB
Go
165 lines
3.4 KiB
Go
package age
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gopasspw/gopass/internal/cache"
|
|
"github.com/gopasspw/gopass/internal/config"
|
|
"github.com/gopasspw/gopass/pkg/debug"
|
|
"github.com/gopasspw/gopass/pkg/pinentry/cli"
|
|
"github.com/twpayne/go-pinentry/v4"
|
|
"github.com/zalando/go-keyring"
|
|
)
|
|
|
|
type cacher interface {
|
|
Get(string) (string, bool)
|
|
Set(string, string)
|
|
Remove(string)
|
|
Purge()
|
|
}
|
|
|
|
type osKeyring struct {
|
|
knownKeys map[string]bool
|
|
}
|
|
|
|
func newOsKeyring() *osKeyring {
|
|
return &osKeyring{
|
|
knownKeys: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
func (o *osKeyring) Get(key string) (string, bool) {
|
|
sec, err := keyring.Get("gopass", key)
|
|
if err != nil {
|
|
debug.Log("failed to get %s from OS keyring: %w", key, err)
|
|
|
|
return "", false
|
|
}
|
|
o.knownKeys[name] = true
|
|
|
|
return sec, true
|
|
}
|
|
|
|
func (o *osKeyring) Set(name, value string) {
|
|
if err := keyring.Set("gopass", name, value); err != nil {
|
|
debug.Log("failed to set %s: %w", name, err)
|
|
}
|
|
o.knownKeys[name] = true
|
|
}
|
|
|
|
func (o *osKeyring) Remove(name string) {
|
|
if err := keyring.Delete("gopass", name); err != nil {
|
|
debug.Log("failed to remove %s from keyring: %s", name, err)
|
|
|
|
return
|
|
}
|
|
o.knownKeys[name] = false
|
|
}
|
|
|
|
func (o *osKeyring) Purge() {
|
|
// purge all known keys. only useful for the REPL case.
|
|
// Does not persist across restarts.
|
|
for k, v := range o.knownKeys {
|
|
if !v {
|
|
continue
|
|
}
|
|
if err := keyring.Delete("gopass", k); err != nil {
|
|
debug.Log("failed to remove %s from keyring: %s", k, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
type askPass struct {
|
|
testing bool
|
|
cache cacher
|
|
}
|
|
|
|
func newAskPass(ctx context.Context) *askPass {
|
|
a := &askPass{
|
|
cache: cache.NewInMemTTL[string, string](time.Hour, 24*time.Hour),
|
|
}
|
|
|
|
if config.Bool(ctx, "age.usekeychain") {
|
|
if err := keyring.Set("gopass", "sentinel", "empty"); err == nil {
|
|
debug.V(1).Log("using OS keychain to cache age credentials")
|
|
a.cache = newOsKeyring()
|
|
}
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
func (a *askPass) Ping(_ context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (a *askPass) Passphrase(key string, reason string, repeat bool) (string, error) {
|
|
if value, found := a.cache.Get(key); found || a.testing {
|
|
debug.V(1).Log("Read value for %s from cache", key)
|
|
|
|
return value, nil
|
|
}
|
|
debug.V(1).Log("Value for %s not found in cache", key)
|
|
|
|
pw, err := a.getPassphrase(reason, repeat)
|
|
if err != nil {
|
|
return "", fmt.Errorf("pinentry error: %w", err)
|
|
}
|
|
|
|
debug.V(1).Log("Updated value for %s in cache", key)
|
|
a.cache.Set(key, pw)
|
|
|
|
return pw, nil
|
|
}
|
|
|
|
func (a *askPass) getPassphrase(reason string, repeat bool) (string, error) {
|
|
opts := []pinentry.ClientOption{
|
|
pinentry.WithBinaryNameFromGnuPGAgentConf(),
|
|
pinentry.WithDesc(strings.TrimSuffix(reason, ":") + "."),
|
|
pinentry.WithGPGTTY(),
|
|
pinentry.WithPrompt("Passphrase:"),
|
|
pinentry.WithTitle("gopass"),
|
|
}
|
|
if repeat {
|
|
opts = append(opts, pinentry.WithRepeat("Confirm"))
|
|
} else {
|
|
opts = append(opts,
|
|
pinentry.WithOption(pinentry.OptionAllowExternalPasswordCache),
|
|
pinentry.WithKeyInfo("gopass/age-identities"),
|
|
)
|
|
}
|
|
|
|
p, err := pinentry.NewClient(opts...)
|
|
if err != nil {
|
|
debug.Log("Pinentry not found: %q", err)
|
|
// use CLI fallback
|
|
pf := cli.New()
|
|
if repeat {
|
|
_ = pf.Set("REPEAT")
|
|
}
|
|
|
|
return pf.GetPIN()
|
|
}
|
|
defer func() {
|
|
_ = p.Close()
|
|
}()
|
|
|
|
result, err := p.GetPIN()
|
|
if err != nil {
|
|
return "", fmt.Errorf("pinentry error: %w", err)
|
|
}
|
|
|
|
return result.PIN, nil
|
|
}
|
|
|
|
func (a *askPass) Remove(key string) {
|
|
a.cache.Remove(key)
|
|
}
|
|
|
|
func (a *askPass) Lock() {
|
|
a.cache.Purge()
|
|
}
|