forked from sneak/secret
Add blank lines before return statements in all files to satisfy the nlreturn linter. This improves code readability by providing visual separation before return statements. Changes made across 24 files: - internal/cli/*.go - internal/secret/*.go - internal/vault/*.go - pkg/agehd/agehd.go - pkg/bip85/bip85.go All 143 nlreturn issues have been resolved.
293 lines
8.2 KiB
Go
293 lines
8.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"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 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.ListVaults(cmd, 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.CreateVault(cmd, 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.SelectVault(cmd, 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.VaultImport(cmd, vaultName)
|
|
},
|
|
}
|
|
}
|
|
|
|
// ListVaults lists all available vaults
|
|
func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
|
|
vaults, err := vault.ListVaults(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if jsonOutput { //nolint:nestif // Separate JSON and text output formatting logic
|
|
// Get current vault name for context
|
|
currentVault := ""
|
|
if currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir); err == nil {
|
|
currentVault = currentVlt.GetName()
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"vaults": vaults,
|
|
"current_vault": currentVault,
|
|
}
|
|
|
|
jsonBytes, err := json.MarshalIndent(result, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Println(string(jsonBytes))
|
|
} else {
|
|
// Text output
|
|
cmd.Println("Available vaults:")
|
|
if len(vaults) == 0 {
|
|
cmd.Println(" (none)")
|
|
} else {
|
|
// Try to get current vault for marking
|
|
currentVault := ""
|
|
if currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir); err == nil {
|
|
currentVault = currentVlt.GetName()
|
|
}
|
|
|
|
for _, vaultName := range vaults {
|
|
if vaultName == currentVault {
|
|
cmd.Printf(" %s (current)\n", vaultName)
|
|
} else {
|
|
cmd.Printf(" %s\n", vaultName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateVault creates a new vault
|
|
func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
|
|
secret.Debug("Creating new vault", "name", name, "state_dir", cli.stateDir)
|
|
|
|
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Printf("Created vault '%s'\n", vlt.GetName())
|
|
|
|
return nil
|
|
}
|
|
|
|
// SelectVault selects a vault as the current one
|
|
func (cli *Instance) SelectVault(cmd *cobra.Command, name string) error {
|
|
if err := vault.SelectVault(cli.fs, cli.stateDir, name); err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Printf("Selected vault '%s' as current\n", name)
|
|
|
|
return nil
|
|
}
|
|
|
|
// VaultImport imports a mnemonic into a specific vault
|
|
func (cli *Instance) VaultImport(cmd *cobra.Command, vaultName string) error {
|
|
secret.Debug("Importing mnemonic into vault", "vault_name", vaultName, "state_dir", cli.stateDir)
|
|
|
|
// Get the specific vault by name
|
|
vlt := vault.NewVault(cli.fs, cli.stateDir, vaultName)
|
|
|
|
// Check if vault exists
|
|
vaultDir, err := vlt.GetDirectory()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Check if vault already has a public key
|
|
pubKeyPath := fmt.Sprintf("%s/pub.age", vaultDir)
|
|
if _, err := cli.fs.Stat(pubKeyPath); err == nil {
|
|
return fmt.Errorf("vault '%s' already has a long-term key configured", vaultName)
|
|
}
|
|
|
|
// Get mnemonic from environment
|
|
mnemonic := os.Getenv(secret.EnvMnemonic)
|
|
if mnemonic == "" {
|
|
return fmt.Errorf("SB_SECRET_MNEMONIC environment variable not set")
|
|
}
|
|
|
|
// Validate the mnemonic
|
|
mnemonicWords := strings.Fields(mnemonic)
|
|
secret.Debug("Validating BIP39 mnemonic", "word_count", len(mnemonicWords))
|
|
if !bip39.IsMnemonicValid(mnemonic) {
|
|
return fmt.Errorf("invalid BIP39 mnemonic")
|
|
}
|
|
|
|
// Get the next available derivation index for this mnemonic
|
|
derivationIndex, err := vault.GetNextDerivationIndex(cli.fs, cli.stateDir, mnemonic)
|
|
if err != nil {
|
|
secret.Debug("Failed to get next derivation index", "error", err)
|
|
|
|
return fmt.Errorf("failed to get next derivation index: %w", err)
|
|
}
|
|
secret.Debug("Using derivation index", "index", derivationIndex)
|
|
|
|
// Derive long-term key from mnemonic with the appropriate index
|
|
secret.Debug("Deriving long-term key from mnemonic", "index", derivationIndex)
|
|
ltIdentity, err := agehd.DeriveIdentity(mnemonic, derivationIndex)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to derive long-term key: %w", err)
|
|
}
|
|
|
|
// Store long-term public key in vault
|
|
ltPublicKey := ltIdentity.Recipient().String()
|
|
secret.Debug("Storing long-term public key", "pubkey", ltPublicKey, "vault_dir", vaultDir)
|
|
|
|
if err := afero.WriteFile(cli.fs, pubKeyPath, []byte(ltPublicKey), secret.FilePerms); err != nil {
|
|
return fmt.Errorf("failed to store long-term public key: %w", err)
|
|
}
|
|
|
|
// Calculate public key hash from the actual derivation index being used
|
|
// This is used to verify that the derived key matches what was stored
|
|
publicKeyHash := vault.ComputeDoubleSHA256([]byte(ltIdentity.Recipient().String()))
|
|
|
|
// Calculate family hash from index 0 (same for all vaults with this mnemonic)
|
|
// This is used to identify which vaults belong to the same mnemonic family
|
|
identity0, err := agehd.DeriveIdentity(mnemonic, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to derive identity for index 0: %w", err)
|
|
}
|
|
familyHash := vault.ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
|
|
|
|
// Load existing metadata
|
|
existingMetadata, err := vault.LoadVaultMetadata(cli.fs, vaultDir)
|
|
if err != nil {
|
|
// If metadata doesn't exist, create new
|
|
existingMetadata = &vault.Metadata{
|
|
CreatedAt: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Update metadata with new derivation info
|
|
existingMetadata.DerivationIndex = derivationIndex
|
|
existingMetadata.PublicKeyHash = publicKeyHash
|
|
existingMetadata.MnemonicFamilyHash = familyHash
|
|
|
|
if err := vault.SaveVaultMetadata(cli.fs, vaultDir, existingMetadata); err != nil {
|
|
secret.Debug("Failed to save vault metadata", "error", err)
|
|
|
|
return fmt.Errorf("failed to save vault metadata: %w", err)
|
|
}
|
|
secret.Debug("Saved vault metadata with derivation index and public key hash")
|
|
|
|
// Get passphrase from environment variable
|
|
passphraseStr := os.Getenv(secret.EnvUnlockPassphrase)
|
|
if passphraseStr == "" {
|
|
return fmt.Errorf("SB_UNLOCK_PASSPHRASE environment variable not set")
|
|
}
|
|
|
|
secret.Debug("Using unlock passphrase from environment variable")
|
|
|
|
// Unlock the vault with the derived long-term key
|
|
vlt.Unlock(ltIdentity)
|
|
|
|
// Create passphrase-protected unlocker
|
|
secret.Debug("Creating passphrase-protected unlocker")
|
|
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
|
|
if err != nil {
|
|
secret.Debug("Failed to create unlocker", "error", err)
|
|
|
|
return fmt.Errorf("failed to create unlocker: %w", err)
|
|
}
|
|
|
|
cmd.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
|
cmd.Printf("Long-term public key: %s\n", ltPublicKey)
|
|
cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
|
|
|
return nil
|
|
}
|