The function was using defer to destroy password buffers, which caused the buffers to be freed before the function returned. This led to a SIGBUS error when trying to access the destroyed buffer's memory. Changed to manual memory management to ensure buffers are only destroyed when no longer needed, and the first buffer is returned directly to the caller who is responsible for destroying it.
231 lines
7.3 KiB
Go
231 lines
7.3 KiB
Go
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
|
|
}
|