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/spf13/afero" "github.com/spf13/cobra" "github.com/tyler-smith/go-bip39" ) 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: func(cmd *cobra.Command, args []string) error { cli := NewCLIInstance() return cli.Init(cmd) }, } } // Init initializes the secrets manager func (cli *CLIInstance) 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, 0700); 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 from stdin using shared line reader var err error mnemonicStr, err = readLineFromStdin("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) } } 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") } // Derive long-term keypair from mnemonic secret.DebugWith("Deriving long-term key from mnemonic", slog.Int("index", 0)) ltIdentity, err := agehd.DeriveIdentity(mnemonicStr, 0) 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) } // Create the default vault 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) } // Set as current vault secret.Debug("Setting default vault as current") if err := vault.SelectVault(cli.fs, cli.stateDir, "default"); err != nil { secret.Debug("Failed to select default vault", "error", err) return fmt.Errorf("failed to select default vault: %w", err) } // Store long-term public key in vault vaultDir := filepath.Join(stateDir, "vaults.d", "default") ltPubKey := ltIdentity.Recipient().String() secret.DebugWith("Storing long-term public key", slog.String("pubkey", ltPubKey), slog.String("vault_dir", vaultDir)) if err := afero.WriteFile(cli.fs, filepath.Join(vaultDir, "pub.age"), []byte(ltPubKey), 0600); err != nil { secret.Debug("Failed to write long-term public key", "error", err) return fmt.Errorf("failed to write long-term public key: %w", err) } // Unlock the vault with the derived long-term key vlt.Unlock(ltIdentity) // Prompt for passphrase for unlock key var passphraseStr string if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" { secret.Debug("Using unlock passphrase from environment variable") passphraseStr = envPassphrase } else { secret.Debug("Prompting user for unlock passphrase") // Use secure passphrase input with confirmation passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ") if err != nil { secret.Debug("Failed to read unlock passphrase", "error", err) return fmt.Errorf("failed to read passphrase: %w", err) } } // Create passphrase-protected unlock key secret.Debug("Creating passphrase-protected unlock key") passphraseKey, err := vlt.CreatePassphraseKey(passphraseStr) if err != nil { secret.Debug("Failed to create unlock key", "error", err) return fmt.Errorf("failed to create unlock key: %w", err) } // Encrypt long-term private key to the unlock key unlockKeyDir := passphraseKey.GetDirectory() // Read unlock key public key unlockPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockKeyDir, "pub.age")) if err != nil { return fmt.Errorf("failed to read unlock key public key: %w", err) } unlockRecipient, err := age.ParseX25519Recipient(string(unlockPubKeyData)) if err != nil { return fmt.Errorf("failed to parse unlock key public key: %w", err) } // Encrypt long-term private key to unlock key ltPrivKeyData := []byte(ltIdentity.String()) encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyData, unlockRecipient) if err != nil { return fmt.Errorf("failed to encrypt long-term private key: %w", err) } // Write encrypted long-term private key if err := afero.WriteFile(cli.fs, filepath.Join(unlockKeyDir, "longterm.age"), encryptedLtPrivKey, 0600); 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("Unlock key ID: %s\n", passphraseKey.GetID()) cmd.Println("\nYour secret manager is ready to use!") cmd.Println("Note: When using SB_SECRET_MNEMONIC environment variable,") cmd.Println("unlock keys 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 unlock keys func readSecurePassphrase(prompt string) (string, error) { // Get the first passphrase passphrase1, err := secret.ReadPassphrase(prompt) if err != nil { return "", err } // Read confirmation passphrase passphrase2, err := secret.ReadPassphrase("Confirm passphrase: ") if err != nil { return "", fmt.Errorf("failed to read passphrase confirmation: %w", err) } // Compare passphrases if passphrase1 != passphrase2 { return "", fmt.Errorf("passphrases do not match") } return passphrase1, nil }