package cli import ( "errors" "fmt" "log" "log/slog" "os" "strings" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "github.com/awnumar/memguard" "github.com/spf13/cobra" "github.com/tyler-smith/go-bip39" ) // errPassphraseMismatch is returned when passphrase confirmation fails var errPassphraseMismatch = errors.New("passphrases do not match") // NewInitCmd creates the init command func NewInitCmd() *cobra.Command { return &cobra.Command{ Use: "init", Short: "Initialize the secrets manager", Long: `Create the necessary directory structure for storing ` + `secrets and generate encryption keys.`, RunE: RunInit, } } // RunInit is the exported function that handles the init command func RunInit(cmd *cobra.Command, _ []string) error { cli, err := NewCLIInstance() if err != nil { log.Fatalf("failed to initialize CLI: %v", err) } destroySecrets := cli.readSecretEnv() defer destroySecrets() return cli.Init(cmd) } // promptMnemonic returns the mnemonic from the environment, cli.Mnemonic, // or reads it interactively. The returned cleanup function must be deferred // by the caller. func (cli *Instance) promptMnemonic() (*memguard.LockedBuffer, func(), error) { if cli.Mnemonic != nil { secret.Debug("Using mnemonic from environment variable") return cli.Mnemonic, func() {}, nil } secret.Debug("Prompting user for mnemonic phrase") // Read mnemonic securely without echo mnemonicBuffer, err := secret.ReadPassphrase("Enter your BIP39 mnemonic phrase: ") if err != nil { secret.Debug("Failed to read mnemonic from stdin", "error", err) return nil, nil, fmt.Errorf("failed to read mnemonic: %w", err) } fmt.Fprintln(os.Stderr) // Add newline after hidden input return mnemonicBuffer, mnemonicBuffer.Destroy, nil } // Init initializes the secret manager, holding the state directory lock // while initialize runs func (cli *Instance) Init(cmd *cobra.Command) error { release, err := vault.LockStateDir(cli.fs, cli.stateDir) if err != nil { return err } defer release() return cli.initialize(cmd) } // initialize creates the state directory, the default vault and its first // unlocker func (cli *Instance) initialize(cmd *cobra.Command) error { secret.Debug("Starting secret manager initialization") // Create state directory stateDir := cli.GetStateDir() secret.DebugWith("Creating state directory", slog.String("path", stateDir)) err := cli.fs.MkdirAll(stateDir, secret.DirPerms) if err != nil { secret.Debug("Failed to create state directory", "error", err) return fmt.Errorf("failed to create state directory: %w", err) } if cmd != nil { cmd.Printf("Initialized secrets manager at: %s\n", stateDir) } // Prompt for mnemonic mnemonic, cleanupMnemonic, err := cli.promptMnemonic() if err != nil { return err } defer cleanupMnemonic() mnemonicStr := mnemonic.String() if mnemonicStr == "" { secret.Debug("Empty mnemonic provided") return errMnemonicEmpty } // Validate the mnemonic using BIP39 secret.DebugWith("Validating BIP39 mnemonic", slog.Int("word_count", len(strings.Fields(mnemonicStr)))) if !bip39.IsMnemonicValid(mnemonicStr) { secret.Debug("Invalid BIP39 mnemonic provided") return fmt.Errorf( "%w\nRun 'secret generate mnemonic' to create a valid mnemonic", errInvalidMnemonicPhrase) } // Ask for the unlocker passphrase before creating the vault, so that // stopping at the prompt leaves no vault without an unlocker behind passphraseBuffer, cleanupPassphrase, err := cli.resolvePassphrase() if err != nil { return err } defer cleanupPassphrase() // Create the default vault with its passphrase unlocker secret.Debug("Creating default vault") vlt, err := vault.CreateVault(cli.fs, cli.stateDir, "default", mnemonic, passphraseBuffer) if err != nil { secret.Debug("Failed to create default vault", "error", err) return fmt.Errorf("failed to create default vault: %w", err) } ltIdentity, err := vlt.GetOrDeriveLongTermKey() if err != nil { return fmt.Errorf("failed to get long-term key: %w", err) } unlocker, err := vlt.GetCurrentUnlocker() if err != nil { return err } if cmd != nil { cmd.Printf("\nDefault vault created and configured\n") cmd.Printf("Long-term public key: %s\n", ltIdentity.Recipient().String()) cmd.Printf("Unlocker ID: %s\n", unlocker.GetID()) cmd.Println("\nYour secret manager is ready to use!") cmd.Println("Note: When using SB_SECRET_MNEMONIC environment variable,") cmd.Println("unlockers are not required for secret operations.") } return nil } // readSecurePassphrase reads a passphrase securely from the terminal without echoing // This version adds confirmation (read twice) for creating new unlockers // Returns a LockedBuffer containing the passphrase func readSecurePassphrase(prompt string) (*memguard.LockedBuffer, error) { // Get the first passphrase passphraseBuffer1, err := secret.ReadPassphrase(prompt) if err != nil { return nil, err } // Read confirmation passphrase passphraseBuffer2, err := secret.ReadPassphrase("Confirm passphrase: ") if err != nil { passphraseBuffer1.Destroy() return nil, fmt.Errorf("failed to read passphrase confirmation: %w", err) } // Compare passphrases if passphraseBuffer1.String() != passphraseBuffer2.String() { passphraseBuffer1.Destroy() passphraseBuffer2.Destroy() return nil, errPassphraseMismatch } // Clean up the second buffer, we'll return the first passphraseBuffer2.Destroy() // Return the first buffer (caller is responsible for destroying it) return passphraseBuffer1, nil }