package cli import ( "errors" "fmt" "log" "log/slog" "os" "path/filepath" "strings" "filippo.io/age" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "git.eeqj.de/sneak/secret/pkg/agehd" "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) } return cli.Init(cmd) } // promptMnemonic reads the mnemonic from the environment or interactively. // The returned cleanup function must be deferred by the caller. func promptMnemonic() (string, func(), error) { if envMnemonic := os.Getenv(secret.EnvMnemonic); envMnemonic != "" { secret.Debug("Using mnemonic from environment variable") return envMnemonic, 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, fmt.Errorf("failed to read mnemonic: %w", err) } fmt.Fprintln(os.Stderr) // Add newline after hidden input return mnemonicBuffer.String(), mnemonicBuffer.Destroy, nil } // setupDefaultVault creates the default vault and derives its long-term // identity from the mnemonic func (cli *Instance) setupDefaultVault( stateDir, mnemonicStr string, ) (*vault.Vault, *age.X25519Identity, error) { // Create the default vault - it will handle key derivation internally secret.Debug("Creating default vault") vlt, err := vault.CreateVault(cli.fs, cli.stateDir, "default") if err != nil { secret.Debug("Failed to create default vault", "error", err) return nil, nil, fmt.Errorf("failed to create default vault: %w", err) } // Get the vault metadata to retrieve the derivation index vaultDir := filepath.Join(stateDir, "vaults.d", "default") metadata, err := vault.LoadVaultMetadata(cli.fs, vaultDir) if err != nil { secret.Debug("Failed to load vault metadata", "error", err) return nil, nil, fmt.Errorf("failed to load vault metadata: %w", err) } // Derive the long-term key using the same index that CreateVault used ltIdentity, err := agehd.DeriveIdentity(mnemonicStr, metadata.DerivationIndex) if err != nil { secret.Debug("Failed to derive long-term key", "error", err) return nil, nil, fmt.Errorf( "failed to derive long-term key from mnemonic: %w", err) } return vlt, ltIdentity, nil } // Init initializes the secret manager func (cli *Instance) Init(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 mnemonicStr, cleanupMnemonic, err := promptMnemonic() if err != nil { return err } defer cleanupMnemonic() 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) } // Set mnemonic in environment for CreateVault to use restoreMnemonicEnv := setMnemonicEnv(mnemonicStr) defer restoreMnemonicEnv() // Create the default vault and derive its long-term key vlt, ltIdentity, err := cli.setupDefaultVault(stateDir, mnemonicStr) if err != nil { return err } ltPubKey := ltIdentity.Recipient().String() // Unlock the vault with the derived long-term key vlt.Unlock(ltIdentity) // Prompt for passphrase for unlocker passphraseBuffer, err := resolvePassphrase() if err != nil { return err } defer passphraseBuffer.Destroy() // Create passphrase-protected unlocker secret.Debug("Creating passphrase-protected unlocker") passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer) if err != nil { secret.Debug("Failed to create unlocker", "error", err) return fmt.Errorf("failed to create unlocker: %w", err) } // Note: CreatePassphraseUnlocker already encrypts and writes the long-term // private key to longterm.age, so no need to do it again here. if cmd != nil { cmd.Printf("\nDefault vault created and configured\n") cmd.Printf("Long-term public key: %s\n", ltPubKey) cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.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 }