Dominik Schulz 792f8b07e2
[chore] Initial fixes and added a warning for CryptFS and JJFS (#3270)
These backends are not ready, yet.

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
2025-11-12 21:04:55 +01:00

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
}