check / check (push) Waiting to run
init, vault create, unlocker add, and add, import and generate secret with --force asked for the mnemonic or passphrase, or unlocked the vault, while holding the state directory lock, so every other command that changes anything waited at the prompt. They now ask and unlock first and lock after. Under the lock, unlocker add fails if the vault was removed, or created again with another key, and refuses a GPG key added meanwhile; a forced add of a secret created meanwhile lets go of the lock, gets the key and locks again. secret.CreatePGPUnlocker takes the unlocked vault; SetMnemonic and SetUnlockPassphrase are gone. Not done: secret move between vaults still unlocks both under the lock (#189). Model: opus-5-5
210 lines
6.0 KiB
Go
210 lines
6.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/cobra"
|
|
"sneak.berlin/go/secret/internal/bip39"
|
|
"sneak.berlin/go/secret/internal/secret"
|
|
"sneak.berlin/go/secret/internal/vault"
|
|
)
|
|
|
|
// errPassphraseMismatch is returned when passphrase confirmation fails
|
|
var errPassphraseMismatch = errors.New("passphrases do not match")
|
|
|
|
// defaultVaultName is the name of the vault `secret init` creates
|
|
const defaultVaultName = "default"
|
|
|
|
// 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.ReadMnemonic("Enter your BIP39 mnemonic phrase: ")
|
|
if err != nil {
|
|
secret.Debug("Failed to read mnemonic", "error", err)
|
|
|
|
return nil, nil, err
|
|
}
|
|
|
|
return mnemonicBuffer, mnemonicBuffer.Destroy, nil
|
|
}
|
|
|
|
// Init initializes the secret manager. It asks for the mnemonic and the
|
|
// passphrase before it takes the state directory lock, so that no other
|
|
// command waits while the user answers, and holds the lock while
|
|
// initialize runs.
|
|
func (cli *Instance) Init(cmd *cobra.Command) error {
|
|
secret.Debug("Starting secret manager initialization")
|
|
|
|
// Refuse an existing default vault before asking for anything
|
|
err := vault.CheckNewVaultName(cli.fs, cli.stateDir, defaultVaultName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create default vault: %w", err)
|
|
}
|
|
|
|
// 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()
|
|
|
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
return cli.initialize(cmd, mnemonic, passphraseBuffer)
|
|
}
|
|
|
|
// initialize creates the state directory, and the default vault from
|
|
// mnemonic with a passphrase unlocker protected by passphrase. Another
|
|
// command may have created the vault since Init checked: vault.CreateVault
|
|
// checks again.
|
|
func (cli *Instance) initialize(
|
|
cmd *cobra.Command, mnemonic, passphrase *memguard.LockedBuffer,
|
|
) error {
|
|
// 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)
|
|
}
|
|
|
|
// Create the default vault with its passphrase unlocker
|
|
secret.Debug("Creating default vault")
|
|
|
|
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, defaultVaultName,
|
|
mnemonic, passphrase)
|
|
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("Initialized secrets manager at: %s\n", stateDir)
|
|
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
|
|
}
|