latest
This commit is contained in:
248
internal/cli/vault.go
Normal file
248
internal/cli/vault.go
Normal file
@@ -0,0 +1,248 @@
|
||||
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 <name>",
|
||||
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 <name>",
|
||||
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 <vault-name>",
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user