package cli import ( "fmt" "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/afero" "github.com/spf13/cobra" "github.com/tyler-smith/go-bip39" ) // 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 := NewCLIInstance() return cli.Init(cmd) } // 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)) if err := cli.fs.MkdirAll(stateDir, secret.DirPerms); 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 var mnemonicStr string if envMnemonic := os.Getenv(secret.EnvMnemonic); envMnemonic != "" { secret.Debug("Using mnemonic from environment variable") mnemonicStr = envMnemonic } else { 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 fmt.Errorf("failed to read mnemonic: %w", err) } defer mnemonicBuffer.Destroy() mnemonicStr = mnemonicBuffer.String() fmt.Fprintln(os.Stderr) // Add newline after hidden input } if mnemonicStr == "" { secret.Debug("Empty mnemonic provided") return fmt.Errorf("mnemonic cannot be empty") } // 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("invalid BIP39 mnemonic phrase\nRun 'secret generate mnemonic' to create a valid mnemonic") } // Set mnemonic in environment for CreateVault to use originalMnemonic := os.Getenv(secret.EnvMnemonic) _ = os.Setenv(secret.EnvMnemonic, mnemonicStr) defer func() { if originalMnemonic != "" { _ = os.Setenv(secret.EnvMnemonic, originalMnemonic) } else { _ = os.Unsetenv(secret.EnvMnemonic) } }() // 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 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 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 fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) } ltPubKey := ltIdentity.Recipient().String() // Unlock the vault with the derived long-term key vlt.Unlock(ltIdentity) // Prompt for passphrase for unlocker var passphraseBuffer *memguard.LockedBuffer if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" { secret.Debug("Using unlock passphrase from environment variable") passphraseBuffer = memguard.NewBufferFromBytes([]byte(envPassphrase)) } else { secret.Debug("Prompting user for unlock passphrase") // Use secure passphrase input with confirmation passphraseBuffer, err = readSecurePassphrase("Enter passphrase for unlocker: ") if err != nil { secret.Debug("Failed to read unlock passphrase", "error", err) return fmt.Errorf("failed to read passphrase: %w", 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) } // Encrypt long-term private key to the unlocker unlockerDir := passphraseUnlocker.GetDirectory() // Read unlocker public key unlockerPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockerDir, "pub.age")) if err != nil { return fmt.Errorf("failed to read unlocker public key: %w", err) } unlockerRecipient, err := age.ParseX25519Recipient(string(unlockerPubKeyData)) if err != nil { return fmt.Errorf("failed to parse unlocker public key: %w", err) } // Encrypt long-term private key to unlocker // Use memguard to protect the private key in memory ltPrivKeyBuffer := memguard.NewBufferFromBytes([]byte(ltIdentity.String())) defer ltPrivKeyBuffer.Destroy() encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyBuffer, unlockerRecipient) if err != nil { return fmt.Errorf("failed to encrypt long-term private key: %w", err) } // Write encrypted long-term private key ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age") if err := afero.WriteFile(cli.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil { return fmt.Errorf("failed to write encrypted long-term private key: %w", err) } 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, fmt.Errorf("passphrases do not match") } // 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 }