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

431 lines
13 KiB
Go

package action
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
"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"
gpgcli "github.com/gopasspw/gopass/internal/backend/crypto/gpg/cli"
"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/pwgen/xkcdgen"
"github.com/gopasspw/gopass/pkg/termio"
"github.com/urfave/cli/v2"
)
// Setup will invoke the onboarding / setup wizard.
func (s *Action) Setup(c *cli.Context) error {
ctx := ctxutil.WithGlobalFlags(c)
remote := c.String("remote")
team := c.String("alias")
create := c.Bool("create")
ctx, err := initParseContext(ctx, c)
if err != nil {
return err
}
out.Printf(ctx, logo)
out.Printf(ctx, "🌟 Welcome to gopass!")
out.Printf(ctx, "🌟 Initializing a new password store ...")
if backend.HasCryptoBackend(ctx) {
out.Printf(ctx, "🔐 Using crypto backend: %s", backend.GetCryptoBackend(ctx))
}
if backend.HasStorageBackend(ctx) {
out.Printf(ctx, "💾 Using storage backend: %s", backend.GetStorageBackend(ctx))
}
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)
// 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, "Store is already initialized. Aborting wizard to avoid overwriting existing data.")
return nil
}
debug.Log("Starting Onboarding Wizard - remote: %s - team: %s - create: %t - name: %s - email: %s", remote, team, create, ctxutil.GetUsername(ctx), ctxutil.GetEmail(ctx))
crypto := s.getCryptoFor(ctx, team)
if crypto == nil {
return fmt.Errorf("can not continue without crypto")
}
debug.Log("Crypto Backend initialized as: %s", crypto.Name())
if err := s.initCheckPrivateKeys(ctx, crypto); err != nil {
return fmt.Errorf("failed to check private keys: %w", err)
}
// if a git remote is given, clone it and exit
if remote != "" && team == "" {
if err := s.clone(ctx, remote, "", ""); err != nil {
return fmt.Errorf("failed to clone remote %q: %w", remote, err)
}
return nil
}
// if a git remote and a team name are given attempt unattended team setup.
if remote != "" && team != "" {
if create {
return s.initCreateTeam(ctx, team, remote)
}
return s.initJoinTeam(ctx, team, remote)
}
if team == "" && create {
return fmt.Errorf("can not create a team without a team name")
}
// assume local setup by default, remotes can be added easily later.
if err := s.initLocal(ctx, remote); err != nil {
debug.Log("Setup failed. initLocal error: %s", err)
return err
}
debug.Log("Setup finished. All systems go. 🚀")
return nil
}
func (s *Action) initCheckPrivateKeys(ctx context.Context, crypto backend.Crypto) error {
// 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.V(1).Log("We have useable private keys")
return nil
}
func (s *Action) initGenerateIdentity(ctx context.Context, crypto backend.Crypto, name, email string) error {
out.Printf(ctx, "🧪 Creating cryptographic key pair (%s) ...", crypto.Name())
if crypto.Name() == gpgcli.Name {
var err error
out.Printf(ctx, "🎩 Gathering information for the %s key pair ...", crypto.Name())
name, err = termio.AskForString(ctx, "🚶 What is your name?", name)
if err != nil {
return err
}
email, err = termio.AskForString(ctx, "📧 What is your email?", email)
if err != nil {
return err
}
if strings.TrimSpace(email) == "" {
return fmt.Errorf("⛔️ Please enter a valid email address to proceed")
}
}
passphrase := xkcdgen.Random()
pwGenerated := true
// support fully automated setup (e.g. for tests)
//nolint:nestif
if ctxutil.HasPasswordCallback(ctx) {
pw, err := ctxutil.GetPasswordCallback(ctx)("", true)
if err == nil {
passphrase = string(pw)
}
pwGenerated = false
} else {
want, err := termio.AskForBool(ctx, "⚠ Do you want to enter a passphrase? (otherwise we generate one for you)", false)
if err != nil {
return err
}
if want {
pwGenerated = false
sv, err := termio.AskForPassword(ctx, "passphrase for your new keypair", true)
if err != nil {
return fmt.Errorf("failed to read passphrase: %w", err)
}
passphrase = sv
}
}
if crypto.Name() == "gpgcli" {
// Note: This issue shouldn't matter much past Linux Kernel 5.6,
// eventually we might want to remove this notice. Only applies to
// GPG.
out.Printf(ctx, "⏳ This can take a long time. If you get impatient see https://go.gopass.pw/entropy")
if want, err := termio.AskForBool(ctx, "Continue?", true); err != nil || !want {
return fmt.Errorf("user aborted: %w", err)
}
}
if pwGenerated {
out.Printf(ctx, color.MagentaString("Passphrase: ")+passphrase)
out.Noticef(ctx, "You need to remember this very well!")
// Prompt to confirm that the user noted their passphrase
if want, err := termio.AskForBool(ctx, "Did you save your passphrase?", true); err != nil || !want {
return fmt.Errorf("user did not confirm saving the passphrase: %w", err)
}
}
if _, err := crypto.GenerateIdentity(ctx, name, email, passphrase); err != nil {
return fmt.Errorf("failed to create new private key: %w", err)
}
out.OKf(ctx, "Key pair for %s generated", crypto.Name())
out.Notice(ctx, "🔐 We need to unlock your newly created private key now! Please enter the passphrase you just generated.")
// avoid the gpg cache or we won't find the newly created key
kl, err := crypto.ListIdentities(gpg.WithUseCache(ctx, false))
if err != nil {
return fmt.Errorf("failed to list private keys: %w", err)
}
if len(kl) > 1 {
out.Notice(ctx, "More than one private key detected. Make sure to use the correct one!")
}
if len(kl) < 1 {
return fmt.Errorf("failed to create a usable key pair")
}
// we can export the generated key to the current directory for convenience.
if err := s.initExportPublicKey(ctx, crypto, kl[0]); err != nil {
return err
}
out.OKf(ctx, "Key pair %s validated", kl[0])
return nil
}
type keyExporter interface {
ExportPublicKey(ctx context.Context, id string) ([]byte, error)
}
func (s *Action) initExportPublicKey(ctx context.Context, crypto backend.Crypto, key string) error {
exp, ok := crypto.(keyExporter)
if !ok {
debug.Log("crypto backend %T can not export public keys", crypto)
return nil
}
fn := key + ".pub.key"
want, err := termio.AskForBool(ctx, fmt.Sprintf("Do you want to export your public key to %q?", fn), false)
if err != nil {
return err
}
if !want {
return nil
}
pk, err := exp.ExportPublicKey(ctx, key)
if err != nil {
return fmt.Errorf("failed to export public key: %w", err)
}
if err := os.WriteFile(fn, pk, 0o6444); err != nil {
out.Errorf(ctx, "❌ Failed to export public key %q: %q", fn, err)
return err
}
out.Printf(ctx, "✴ Public key exported to %q", fn)
return nil
}
func (s *Action) initHasUseablePrivateKeys(ctx context.Context, crypto backend.Crypto) bool {
debug.Log("checking for existing, usable identities / private keys for %s", crypto.Name())
kl, err := crypto.ListIdentities(ctx)
if err != nil {
return false
}
debug.Log("available private keys: %q for %s", kl, crypto.Name())
return len(kl) > 0
}
func (s *Action) initSetupGitRemote(ctx context.Context, team, remote string) error {
var err error
remote, err = termio.AskForString(ctx, "Please enter the git remote for your shared store", remote)
if err != nil {
return fmt.Errorf("failed to read user input: %w", err)
}
// omit RCS output.
ctx = ctxutil.WithHidden(ctx, true)
if err := s.Store.RCSAddRemote(ctx, team, "origin", remote); err != nil {
return fmt.Errorf("failed to add git remote: %w", err)
}
// initial pull, in case the remote is non-empty.
if err := s.Store.RCSPull(ctx, team, "origin", ""); err != nil {
debug.Log("Initial git pull failed: %s", err)
}
if err := s.Store.RCSPush(ctx, team, "origin", ""); err != nil {
return fmt.Errorf("failed to push to git remote: %w", err)
}
return nil
}
// initLocal will initialize a local store, useful for local-only setups or as
// part of team setups to create the root store.
func (s *Action) initLocal(ctx context.Context, remote string) error {
path := ""
if s.Store != nil {
path = s.Store.Path()
}
out.Printf(ctx, "🌟 Configuring your password store ...")
if err := s.init(ctxutil.WithHidden(ctx, true), "", path); err != nil {
return fmt.Errorf("failed to init local store: %w", err)
}
if backend.GetStorageBackend(ctx) == backend.GitFS {
debug.Log("configuring git remotes")
if want, err := termio.AskForBool(ctx, "❓ Do you want to add a git remote?", false); (err == nil && want) || remote != "" {
out.Printf(ctx, "Configuring the git remote ...")
if err := s.initSetupGitRemote(ctx, "", remote); err != nil {
return fmt.Errorf("failed to setup git remote: %w", err)
}
}
}
// TODO remotes for fossil, etc.
// detect and add mount a for passage
if err := s.initDetectPassage(ctx); err != nil {
out.Warningf(ctx, "Failed to add passage mount: %s", err)
}
out.OKf(ctx, "Configuration written")
return nil
}
func (s *Action) initDetectPassage(ctx context.Context) error {
pIds := age.PassageIDFile()
if !fsutil.IsFile(pIds) {
debug.Log("no passage identities found at %s", pIds)
return nil
}
pDir := filepath.Dir(pIds)
if err := s.Store.AddMount(ctx, "passage", pDir); err != nil {
return fmt.Errorf("failed to mount passage dir: %w", err)
}
out.OKf(ctx, "Detected passage store at %s. Mounted below passage/.", pDir)
return nil
}
// initCreateTeam will create a local root store and a shared team store.
func (s *Action) initCreateTeam(ctx context.Context, team, remote string) error {
var err error
out.Printf(ctx, "Creating a new team ...")
if err := s.initLocal(ctx, ""); err != nil {
return fmt.Errorf("failed to create local store: %w", err)
}
// name of the new team.
team, err = termio.AskForString(ctx, out.Prefix(ctx)+"Please enter the name of your team (may contain slashes)", team)
if err != nil {
return fmt.Errorf("failed to read user input: %w", err)
}
ctx = out.AddPrefix(ctx, "["+team+"] ")
out.Printf(ctx, "Initializing your shared store ...")
if err := s.init(ctxutil.WithHidden(ctx, true), team, ""); err != nil {
return fmt.Errorf("failed to init shared store: %w", err)
}
out.OKf(ctx, "Done. Initialized the store.")
out.Printf(ctx, "Configuring the git remote ...")
if err := s.initSetupGitRemote(ctx, team, remote); err != nil {
return fmt.Errorf("failed to setup git remote: %w", err)
}
out.OKf(ctx, "Done. Created Team %q", team)
return nil
}
// initJoinTeam will create a local root store and clone an existing store to
// a mount.
func (s *Action) initJoinTeam(ctx context.Context, team, remote string) error {
var err error
out.Printf(ctx, "Joining existing team ...")
if err := s.initLocal(ctx, ""); err != nil {
return fmt.Errorf("failed to create local store: %w", err)
}
// name of the existing team.
team, err = termio.AskForString(ctx, out.Prefix(ctx)+"Please enter the name of your team (may contain slashes)", team)
if err != nil {
return err
}
ctx = out.AddPrefix(ctx, "["+team+"]")
out.Printf(ctx, "Configuring git remote ...")
remote, err = termio.AskForString(ctx, out.Prefix(ctx)+"Please enter the git remote for your shared store", remote)
if err != nil {
return err
}
out.Printf(ctx, "Cloning from the git remote ...")
if err := s.clone(ctxutil.WithHidden(ctx, true), remote, team, ""); err != nil {
return fmt.Errorf("failed to clone repo: %w", err)
}
out.OKf(ctx, "Done. Joined Team %q", team)
out.Noticef(ctx, "You still need to request access to decrypt secrets!")
return nil
}