package cli import ( "encoding/json" "fmt" "os" "path/filepath" "syscall" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/tyler-smith/go-bip39" "golang.org/x/term" ) func newVaultCmd() *cobra.Command { cmd := &cobra.Command{ Use: "vault", Short: "Manage vaults", Long: `Create, list, and select vaults for organizing secrets.`, } cmd.AddCommand(newVaultListCmd()) cmd.AddCommand(newVaultCreateCmd()) cmd.AddCommand(newVaultSelectCmd()) cmd.AddCommand(newVaultImportCmd()) return cmd } func newVaultListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List available vaults", RunE: func(cmd *cobra.Command, args []string) error { jsonOutput, _ := cmd.Flags().GetBool("json") cli := NewCLIInstance() return cli.VaultList(jsonOutput) }, } cmd.Flags().Bool("json", false, "Output in JSON format") return cmd } func newVaultCreateCmd() *cobra.Command { return &cobra.Command{ Use: "create ", Short: "Create a new vault", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cli := NewCLIInstance() return cli.VaultCreate(args[0]) }, } } func newVaultSelectCmd() *cobra.Command { return &cobra.Command{ Use: "select ", Short: "Select a vault as current", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cli := NewCLIInstance() return cli.VaultSelect(args[0]) }, } } func newVaultImportCmd() *cobra.Command { return &cobra.Command{ Use: "import ", Short: "Import a mnemonic into a vault", Long: `Import a BIP39 mnemonic phrase into the specified vault (default if not specified).`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { vaultName := "default" if len(args) > 0 { vaultName = args[0] } cli := NewCLIInstance() return cli.Import(vaultName) }, } } // VaultList lists available vaults func (cli *CLIInstance) VaultList(jsonOutput bool) error { vaults, err := secret.ListVaults(cli.fs, cli.stateDir) if err != nil { return err } if jsonOutput { // JSON output output := map[string]interface{}{ "vaults": vaults, } jsonBytes, err := json.MarshalIndent(output, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } fmt.Println(string(jsonBytes)) } else { // Pretty table output if len(vaults) == 0 { fmt.Println("No vaults found.") fmt.Println("Run 'secret init' to create the default vault.") return nil } // Get current vault for highlighting currentVault, err := secret.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { fmt.Printf("%-20s %s\n", "VAULT", "STATUS") fmt.Printf("%-20s %s\n", "-----", "------") for _, vault := range vaults { fmt.Printf("%-20s %s\n", vault, "") } } else { fmt.Printf("%-20s %s\n", "VAULT", "STATUS") fmt.Printf("%-20s %s\n", "-----", "------") for _, vault := range vaults { status := "" if vault == currentVault.Name { status = "(current)" } fmt.Printf("%-20s %s\n", vault, status) } } fmt.Printf("\nTotal: %d vault(s)\n", len(vaults)) } return nil } // VaultCreate creates a new vault func (cli *CLIInstance) VaultCreate(name string) error { _, err := secret.CreateVault(cli.fs, cli.stateDir, name) return err } // VaultSelect selects a vault as current func (cli *CLIInstance) VaultSelect(name string) error { return secret.SelectVault(cli.fs, cli.stateDir, name) } // Import imports a mnemonic into a vault func (cli *CLIInstance) Import(vaultName string) error { var mnemonicStr string // Check if mnemonic is set in environment variable if envMnemonic := os.Getenv(secret.EnvMnemonic); envMnemonic != "" { mnemonicStr = envMnemonic } else { // Read mnemonic from stdin using shared line reader var err error mnemonicStr, err = readLineFromStdin("Enter your BIP39 mnemonic phrase: ") if err != nil { return fmt.Errorf("failed to read mnemonic: %w", err) } } if mnemonicStr == "" { return fmt.Errorf("mnemonic cannot be empty") } // Validate the mnemonic using BIP39 if !bip39.IsMnemonicValid(mnemonicStr) { return fmt.Errorf("invalid BIP39 mnemonic phrase\nRun 'secret generate mnemonic' to create a valid mnemonic") } return cli.importMnemonic(vaultName, mnemonicStr) } // importMnemonic imports a BIP39 mnemonic into the specified vault func (cli *CLIInstance) importMnemonic(vaultName, mnemonic string) error { // Derive long-term keypair from mnemonic ltIdentity, err := agehd.DeriveIdentity(mnemonic, 0) if err != nil { return fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) } // Check if vault exists stateDir := cli.GetStateDir() vaultDir := filepath.Join(stateDir, "vaults.d", vaultName) exists, err := afero.DirExists(cli.fs, vaultDir) if err != nil { return fmt.Errorf("failed to check if vault exists: %w", err) } if !exists { return fmt.Errorf("vault %s does not exist", vaultName) } // Store long-term public key in vault ltPubKey := ltIdentity.Recipient().String() if err := afero.WriteFile(cli.fs, filepath.Join(vaultDir, "pub.age"), []byte(ltPubKey), 0600); err != nil { return fmt.Errorf("failed to write long-term public key: %w", err) } // Get the vault instance and unlock it vault := secret.NewVault(cli.fs, vaultName, cli.stateDir) vault.Unlock(ltIdentity) fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName) fmt.Printf("Long-term public key: %s\n", ltPubKey) // Try to create unlock key only if running interactively if term.IsTerminal(int(syscall.Stderr)) { // Get or create passphrase for unlock key var passphraseStr string if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" { passphraseStr = envPassphrase } else { // Use secure passphrase input with confirmation passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ") if err != nil { fmt.Printf("Warning: Failed to create unlock key: %v\n", err) fmt.Printf("You can create unlock keys later with 'secret keys add passphrase'\n") return nil } } // Create passphrase-protected unlock key (vault is now unlocked) passphraseKey, err := vault.CreatePassphraseKey(passphraseStr) if err != nil { fmt.Printf("Warning: Failed to create unlock key: %v\n", err) fmt.Printf("You can create unlock keys later with 'secret keys add passphrase'\n") return nil } fmt.Printf("Unlock key ID: %s\n", passphraseKey.GetMetadata().ID) } else { fmt.Printf("Running in non-interactive mode - unlock key not created\n") fmt.Printf("You can create unlock keys later with 'secret keys add passphrase'\n") } return nil }