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>
431 lines
13 KiB
Go
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
|
|
}
|