mirror of
https://github.com/gopasspw/gopass.git
synced 2025-12-08 19:24:54 +00:00
These backends are not ready, yet. Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
290 lines
8.8 KiB
Go
290 lines
8.8 KiB
Go
package action
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gopasspw/gopass/internal/action/exit"
|
|
"github.com/gopasspw/gopass/internal/backend"
|
|
"github.com/gopasspw/gopass/internal/backend/crypto/age"
|
|
"github.com/gopasspw/gopass/internal/backend/crypto/gpg"
|
|
"github.com/gopasspw/gopass/internal/config"
|
|
"github.com/gopasspw/gopass/internal/cui"
|
|
"github.com/gopasspw/gopass/internal/out"
|
|
"github.com/gopasspw/gopass/internal/store/root"
|
|
"github.com/gopasspw/gopass/pkg/ctxutil"
|
|
"github.com/gopasspw/gopass/pkg/debug"
|
|
"github.com/gopasspw/gopass/pkg/fsutil"
|
|
"github.com/gopasspw/gopass/pkg/set"
|
|
"github.com/gopasspw/gopass/pkg/termio"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
// Clone will fetch and mount a new password store from a git repo.
|
|
// It can also be used to clone a new password store to a submount.
|
|
func (s *Action) Clone(c *cli.Context) error {
|
|
ctx := ctxutil.WithGlobalFlags(c)
|
|
if c.IsSet("crypto") {
|
|
var err error
|
|
ctx, err = backend.WithCryptoBackendString(ctx, c.String("crypto"))
|
|
if err != nil {
|
|
return exit.Error(exit.Unknown, err, "Failed to set crypto backend: %s", err)
|
|
}
|
|
}
|
|
|
|
if c.IsSet("storage") {
|
|
var err error
|
|
ctx, err = backend.WithStorageBackendString(ctx, c.String("storage"))
|
|
if err != nil {
|
|
return exit.Error(exit.Unknown, err, "Failed to set storage backend: %s", err)
|
|
}
|
|
}
|
|
|
|
path := c.String("path")
|
|
|
|
if c.Args().Len() < 1 {
|
|
return exit.Error(exit.Usage, nil, "Usage: %s clone repo [mount]", s.Name)
|
|
}
|
|
|
|
// gopass clone [--crypto=foo] [--path=/some/store] git://foo/bar team0.
|
|
repo := c.Args().Get(0)
|
|
mount := ""
|
|
if c.Args().Len() > 1 {
|
|
mount = c.Args().Get(1)
|
|
}
|
|
|
|
out.Printf(ctx, logo)
|
|
out.Printf(ctx, "🌟 Welcome to gopass!")
|
|
out.Printf(ctx, "🌟 Cloning an existing password store from %q ...", repo)
|
|
|
|
if name := termio.DetectName(ctx, c); name != "" {
|
|
ctx = ctxutil.WithUsername(ctx, name)
|
|
}
|
|
if email := termio.DetectEmail(ctx, c); email != "" {
|
|
ctx = ctxutil.WithEmail(ctx, email)
|
|
}
|
|
|
|
// age: only native keys
|
|
// "[ssh] types should only be used for compatibility with existing keys,
|
|
// and native X25519 keys should be preferred otherwise."
|
|
// https://pkg.go.dev/filippo.io/age@v1.0.0/agessh#pkg-overview.
|
|
ctx = age.WithOnlyNative(ctx, true)
|
|
// gpg: only trusted keys
|
|
// only list "usable" / properly trused and signed GPG keys by requesting
|
|
// always trust is false. Ignored for other backends. See
|
|
// https://www.gnupg.org/gph/en/manual/r1554.html.
|
|
ctx = gpg.WithAlwaysTrust(ctx, false)
|
|
|
|
if err := s.clone(ctx, repo, mount, path); err != nil {
|
|
return err
|
|
}
|
|
|
|
// need to re-initialize the root store or it's already initialized
|
|
// and won't properly set up crypto according to our context.
|
|
s.Store = root.New(s.cfg)
|
|
inited, err := s.Store.IsInitialized(ctx)
|
|
if err != nil {
|
|
return exit.Error(exit.Unknown, err, "Failed to check store status: %s", err)
|
|
}
|
|
|
|
if !inited {
|
|
out.Errorf(ctx, "Failed to clone")
|
|
|
|
return nil
|
|
}
|
|
|
|
if !c.Bool("check-keys") {
|
|
return nil
|
|
}
|
|
|
|
return s.cloneCheckDecryptionKeys(ctx, mount)
|
|
}
|
|
|
|
// storageBackendOrDefault will return a storage backend that can be clone,
|
|
// i.e. specifically backend.FS can't be cloned.
|
|
func storageBackendOrDefault(ctx context.Context, repo string) backend.StorageBackend {
|
|
// first try to get it from the context.
|
|
if be := backend.GetStorageBackend(ctx); be != backend.FS {
|
|
return be
|
|
}
|
|
|
|
if strings.HasSuffix(repo, ".fossil") {
|
|
return backend.FossilFS
|
|
}
|
|
|
|
if strings.HasSuffix(repo, ".git") {
|
|
return backend.GitFS
|
|
}
|
|
|
|
debug.Log("falling back to the default storage backend for clone (GitFS)")
|
|
|
|
return backend.GitFS
|
|
}
|
|
|
|
func (s *Action) clone(ctx context.Context, repo, mount, path string) error {
|
|
if path == "" {
|
|
path = config.PwStoreDir(mount)
|
|
}
|
|
|
|
inited, err := s.Store.IsInitialized(ctxutil.WithGitInit(ctx, false))
|
|
if err != nil {
|
|
return exit.Error(exit.Unknown, err, "Failed to initialized stores: %s", err)
|
|
}
|
|
|
|
if mount == "" && inited {
|
|
return exit.Error(exit.AlreadyInitialized, nil, "Can not clone %s to the root store, as this store is already initialized. Please try cloning to a submount: `%s clone %s sub`", repo, s.Name, repo)
|
|
}
|
|
|
|
// make sure the parent directory exists.
|
|
if parentPath := filepath.Dir(path); !fsutil.IsDir(parentPath) {
|
|
if err := os.MkdirAll(parentPath, 0o700); err != nil {
|
|
return exit.Error(exit.Unknown, err, "Failed to create parent directory for clone: %s", err)
|
|
}
|
|
}
|
|
|
|
// clone repo.
|
|
sb := storageBackendOrDefault(ctx, repo)
|
|
out.Noticef(ctx, "Cloning %s repository %q to %q ...", sb, repo, path)
|
|
_, err = backend.Clone(ctx, sb, repo, path)
|
|
if err != nil {
|
|
return exit.Error(exit.Git, err, "failed to clone repo %q to %q: %s", repo, path, err)
|
|
}
|
|
|
|
// add mount.
|
|
debug.Log("Mounting cloned repo %q at %q", path, mount)
|
|
if err := s.cloneAddMount(ctx, mount, path); err != nil {
|
|
return err
|
|
}
|
|
|
|
// try to init repo config.
|
|
out.Noticef(ctx, "Configuring %s repository ...", sb)
|
|
|
|
// ask for config values.
|
|
username, email, err := s.cloneGetGitConfig(ctx, mount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// initialize repo config.
|
|
if err := s.Store.RCSInitConfig(ctx, mount, username, email); err != nil {
|
|
debug.Log("Stacktrace: %+v\n", err)
|
|
out.Errorf(ctx, "Failed to configure %s: %s", sb, err)
|
|
}
|
|
|
|
if mount != "" {
|
|
mount = " " + mount
|
|
}
|
|
|
|
out.Printf(ctx, "Your password store is ready to use! Have a look around: `%s list%s`\n", s.Name, mount)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Action) cloneCheckDecryptionKeys(ctx context.Context, mount string) error {
|
|
crypto := s.getCryptoFor(ctx, mount)
|
|
if crypto == nil {
|
|
return fmt.Errorf("can not continue without crypto")
|
|
}
|
|
debug.Log("Crypto Backend initialized as: %s", crypto.Name())
|
|
|
|
// check for existing GPG/Age keypairs (private/secret keys). We need at least
|
|
// one useable key pair. If none exists try to create one.
|
|
if !s.initHasUseablePrivateKeys(ctx, crypto) {
|
|
out.Printf(ctx, "🔐 No useable cryptographic keys. Generating new key pair")
|
|
if crypto.Name() == "gpgcli" {
|
|
out.Printf(ctx, "🕰 Key generation may take up to a few minutes")
|
|
}
|
|
if err := s.initGenerateIdentity(ctx, crypto, ctxutil.GetUsername(ctx), ctxutil.GetEmail(ctx)); err != nil {
|
|
return fmt.Errorf("failed to create new private key: %w", err)
|
|
}
|
|
out.Printf(ctx, "🔐 Cryptographic keys generated")
|
|
}
|
|
|
|
debug.Log("We have useable private keys")
|
|
|
|
recpSet := set.New(s.Store.ListRecipients(ctx, mount)...)
|
|
ids, err := crypto.ListIdentities(ctx)
|
|
if err != nil {
|
|
out.Warningf(ctx, "Failed to check decryption keys: %s", err)
|
|
|
|
return nil
|
|
}
|
|
|
|
idSet := set.New(ids...)
|
|
// Check whether any of our usable keys are in recpSet
|
|
if _, found := recpSet.Choose(idSet.Contains); found {
|
|
out.Noticef(ctx, "Found valid decryption keys. You can now decrypt your passwords.")
|
|
|
|
return nil
|
|
}
|
|
|
|
var exported bool
|
|
if sub, err := s.Store.GetSubStore(mount); err == nil {
|
|
debug.Log("exporting public keys: %v", idSet.Elements())
|
|
exported, err = sub.UpdateExportedPublicKeys(ctx)
|
|
if err != nil {
|
|
debug.Log("failed to export missing public keys: %w", err)
|
|
}
|
|
} else {
|
|
debug.Log("failed to get sub store: %s", err)
|
|
}
|
|
|
|
out.Noticef(ctx, "Please ask the owner of the password store to add one of your keys: %s", strings.Join(idSet.Elements(), ", "))
|
|
if exported {
|
|
out.Noticef(ctx, "The missing keys were exported to the password store. Run `gopass sync` to push them.")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Action) cloneAddMount(ctx context.Context, mount, path string) error {
|
|
if mount == "" {
|
|
return nil
|
|
}
|
|
|
|
inited, err := s.Store.IsInitialized(ctx)
|
|
if err != nil {
|
|
return exit.Error(exit.Unknown, err, "Failed to initialize store: %s", err)
|
|
}
|
|
|
|
if !inited {
|
|
return exit.Error(exit.NotInitialized, nil, "Root-Store is not initialized. Clone or init root store first")
|
|
}
|
|
|
|
if err := s.Store.AddMount(ctx, mount, path); err != nil {
|
|
return exit.Error(exit.Mount, err, "Failed to add mount: %s", err)
|
|
}
|
|
out.Printf(ctx, "Mounted password store %s at mount point `%s` ...", path, mount)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Action) cloneGetGitConfig(ctx context.Context, name string) (string, string, error) {
|
|
out.Printf(ctx, "🎩 Gathering information for the git repository ...")
|
|
// for convenience, set defaults to user-selected values from available private keys.
|
|
// NB: discarding returned error since this is merely a best-effort look-up for convenience.
|
|
username, email, _ := cui.AskForGitConfigUser(ctx, s.Store.Crypto(ctx, name))
|
|
if username == "" {
|
|
username = termio.DetectName(ctx, nil)
|
|
var err error
|
|
username, err = termio.AskForString(ctx, "🚶 What is your name?", username)
|
|
if err != nil {
|
|
return "", "", exit.Error(exit.IO, err, "Failed to read user input: %s", err)
|
|
}
|
|
}
|
|
|
|
if email == "" {
|
|
email = termio.DetectEmail(ctx, nil)
|
|
var err error
|
|
email, err = termio.AskForString(ctx, "📧 What is your email?", email)
|
|
if err != nil {
|
|
return "", "", exit.Error(exit.IO, err, "Failed to read user input: %s", err)
|
|
}
|
|
}
|
|
|
|
return username, email, nil
|
|
}
|