check / check (push) Waiting to run
secret rm, secret version rm, secret vault remove and secret unlocker remove ask [y/N] on a terminal, naming what they remove, and go ahead only on y or yes. Without --force, a command whose stdin is not a terminal fails at once. --force, now also on rm and version rm, removes without asking; it replaces the old refusals to remove a vault with secrets or the last unlocker without --force. The checks run and the question is asked before the state directory lock is taken; under the lock the checks run again, and nothing is removed if they would ask a different question. Model: opus-5-5
695 lines
19 KiB
Go
695 lines
19 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"slices"
|
|
"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/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
"github.com/tyler-smith/go-bip39"
|
|
)
|
|
|
|
// Sentinel errors for vault operations
|
|
var (
|
|
errMnemonicEmpty = errors.New("mnemonic cannot be empty")
|
|
errInvalidMnemonicPhrase = errors.New("invalid BIP39 mnemonic phrase")
|
|
errInvalidMnemonic = errors.New("invalid BIP39 mnemonic")
|
|
errVaultHasLongTermKey = errors.New(
|
|
"already has a long-term key configured")
|
|
errMnemonicEnvNotSet = errors.New(
|
|
"SB_SECRET_MNEMONIC environment variable not set")
|
|
errPassphraseEnvNotSet = errors.New(
|
|
"SB_UNLOCK_PASSPHRASE environment variable not set")
|
|
errCannotRemoveLastVault = errors.New("cannot remove the last vault")
|
|
)
|
|
|
|
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())
|
|
cmd.AddCommand(newVaultRemoveCmd())
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newVaultListCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: cmdUseList,
|
|
Aliases: []string{"ls"},
|
|
Short: "List available vaults",
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
|
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
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, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
destroySecrets := cli.readSecretEnv()
|
|
defer destroySecrets()
|
|
|
|
return cli.CreateVault(cmd, args[0])
|
|
},
|
|
}
|
|
}
|
|
|
|
func newVaultSelectCmd() *cobra.Command {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize CLI: %v", err)
|
|
}
|
|
|
|
return &cobra.Command{
|
|
Use: "select <name>",
|
|
Short: "Select a vault as current",
|
|
Args: cobra.ExactArgs(1),
|
|
ValidArgsFunction: getVaultNamesCompletionFunc(cli.fs, cli.stateDir),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
return cli.SelectVault(cmd, args[0])
|
|
},
|
|
}
|
|
}
|
|
|
|
func newVaultImportCmd() *cobra.Command {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize CLI: %v", err)
|
|
}
|
|
|
|
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),
|
|
ValidArgsFunction: getVaultNamesCompletionFunc(cli.fs, cli.stateDir),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
vaultName := "default"
|
|
if len(args) > 0 {
|
|
vaultName = args[0]
|
|
}
|
|
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
destroySecrets := cli.readSecretEnv()
|
|
defer destroySecrets()
|
|
|
|
return cli.VaultImport(cmd, vaultName)
|
|
},
|
|
}
|
|
}
|
|
|
|
func newVaultRemoveCmd() *cobra.Command {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize CLI: %v", err)
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "remove <name>",
|
|
Aliases: []string{"rm"},
|
|
Short: "Remove a vault",
|
|
Long: `Remove a vault and all its secrets. Asks for ` +
|
|
`confirmation first, naming how many secrets the vault ` +
|
|
`holds; when stdin is not a terminal, fails unless --force ` +
|
|
`is given. Will automatically switch to another vault if ` +
|
|
`removing the currently selected one. The last vault ` +
|
|
`cannot be removed.`,
|
|
Args: cobra.ExactArgs(1),
|
|
ValidArgsFunction: getVaultNamesCompletionFunc(cli.fs, cli.stateDir),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
force, _ := cmd.Flags().GetBool("force")
|
|
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
return cli.RemoveVault(cmd, args[0], force)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolP("force", "f", false,
|
|
"Remove without asking for confirmation, even a vault that contains secrets")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// 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 := ""
|
|
|
|
currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err == nil {
|
|
currentVault = currentVlt.GetName()
|
|
}
|
|
|
|
result := map[string]any{
|
|
"vaults": vaults,
|
|
"currentVault": 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 := ""
|
|
|
|
currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if 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
|
|
}
|
|
|
|
// resolvePassphrase returns the unlock passphrase from the environment,
|
|
// cli.UnlockPassphrase, or prompts the user for it with confirmation. The
|
|
// returned cleanup function must be deferred by the caller.
|
|
func (cli *Instance) resolvePassphrase() (*memguard.LockedBuffer, func(), error) {
|
|
if cli.UnlockPassphrase != nil {
|
|
secret.Debug("Using unlock passphrase from environment variable")
|
|
|
|
return cli.UnlockPassphrase, func() {}, nil
|
|
}
|
|
|
|
secret.Debug("Prompting user for unlock passphrase")
|
|
|
|
// Use secure passphrase input with confirmation
|
|
passphraseBuffer, err := readSecurePassphrase("Enter passphrase for unlocker: ")
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to read passphrase: %w", err)
|
|
}
|
|
|
|
return passphraseBuffer, passphraseBuffer.Destroy, 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)
|
|
|
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
mnemonic, cleanupMnemonic, err := cli.promptMnemonic()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanupMnemonic()
|
|
|
|
mnemonicStr := mnemonic.String()
|
|
if mnemonicStr == "" {
|
|
return errMnemonicEmpty
|
|
}
|
|
|
|
// Validate the mnemonic
|
|
mnemonicWords := strings.Fields(mnemonicStr)
|
|
secret.Debug("Validating BIP39 mnemonic", "word_count", len(mnemonicWords))
|
|
|
|
if !bip39.IsMnemonicValid(mnemonicStr) {
|
|
return errInvalidMnemonicPhrase
|
|
}
|
|
|
|
// Ask for the unlocker passphrase before creating the vault, so that
|
|
// stopping at the prompt leaves no vault without an unlocker behind
|
|
passphraseBuffer, cleanupPassphrase, err := cli.resolvePassphrase()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanupPassphrase()
|
|
|
|
// Create the vault - it will handle key derivation internally
|
|
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name, mnemonic)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the vault metadata to retrieve the derivation index
|
|
vaultDir := filepath.Join(cli.stateDir, "vaults.d", name)
|
|
|
|
metadata, err := vault.LoadVaultMetadata(cli.fs, vaultDir)
|
|
if err != nil {
|
|
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 {
|
|
return fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
|
|
}
|
|
|
|
// 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(passphraseBuffer)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create unlocker: %w", err)
|
|
}
|
|
|
|
cmd.Printf("Created vault '%s'\n", vlt.GetName())
|
|
cmd.Printf("Long-term public key: %s\n", ltIdentity.Recipient().String())
|
|
cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
|
|
|
return nil
|
|
}
|
|
|
|
// SelectVault selects a vault as the current one
|
|
func (cli *Instance) SelectVault(cmd *cobra.Command, name string) error {
|
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
err = vault.SelectVault(cli.fs, cli.stateDir, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Printf("Selected vault '%s' as current\n", name)
|
|
|
|
return nil
|
|
}
|
|
|
|
// vaultImportPreflight verifies the vault exists without a long-term key
|
|
// and returns the vault directory, public key path, and validated mnemonic
|
|
func (cli *Instance) vaultImportPreflight(
|
|
vlt *vault.Vault, vaultName string,
|
|
) (string, string, string, error) {
|
|
// 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' %w",
|
|
vaultName, errVaultDoesNotExist)
|
|
}
|
|
|
|
// Check if vault already has a public key
|
|
pubKeyPath := vaultDir + "/pub.age"
|
|
|
|
exists, err = afero.Exists(cli.fs, pubKeyPath)
|
|
if err != nil {
|
|
return "", "", "", fmt.Errorf("failed to check %s: %w", pubKeyPath, err)
|
|
}
|
|
|
|
if exists {
|
|
return "", "", "", fmt.Errorf("vault '%s' %w",
|
|
vaultName, errVaultHasLongTermKey)
|
|
}
|
|
|
|
// Get mnemonic from environment
|
|
if cli.Mnemonic == nil {
|
|
return "", "", "", errMnemonicEnvNotSet
|
|
}
|
|
|
|
mnemonic := cli.Mnemonic.String()
|
|
|
|
// Validate the mnemonic
|
|
mnemonicWords := strings.Fields(mnemonic)
|
|
secret.Debug("Validating BIP39 mnemonic", "word_count", len(mnemonicWords))
|
|
|
|
if !bip39.IsMnemonicValid(mnemonic) {
|
|
return "", "", "", errInvalidMnemonic
|
|
}
|
|
|
|
return vaultDir, pubKeyPath, mnemonic, nil
|
|
}
|
|
|
|
// updateVaultImportMetadata stores the derivation info in vault metadata
|
|
func updateVaultImportMetadata(
|
|
fs afero.Fs, vaultDir string, derivationIndex uint32,
|
|
publicKeyHash, familyHash string,
|
|
) error {
|
|
// Load existing metadata
|
|
existingMetadata, err := vault.LoadVaultMetadata(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
|
|
|
|
err = vault.SaveVaultMetadata(fs, vaultDir, existingMetadata)
|
|
if 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")
|
|
|
|
return nil
|
|
}
|
|
|
|
// VaultImport imports a mnemonic into a specific vault, holding the state
|
|
// directory lock while importMnemonic runs
|
|
func (cli *Instance) VaultImport(cmd *cobra.Command, vaultName string) error {
|
|
err := vault.ValidateVaultName(vaultName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
return cli.importMnemonic(cmd, vaultName)
|
|
}
|
|
|
|
// importMnemonic gives the vault a long-term key derived from the mnemonic
|
|
// and a passphrase unlocker
|
|
func (cli *Instance) importMnemonic(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)
|
|
|
|
vaultDir, pubKeyPath, mnemonic, err := cli.vaultImportPreflight(vlt, vaultName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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)
|
|
|
|
err = secret.WriteFileAtomic(cli.fs, pubKeyPath, []byte(ltPublicKey))
|
|
if 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()))
|
|
|
|
err = updateVaultImportMetadata(
|
|
cli.fs, vaultDir, derivationIndex, publicKeyHash, familyHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get passphrase from environment variable
|
|
passphraseBuffer := cli.UnlockPassphrase
|
|
if passphraseBuffer == nil {
|
|
return errPassphraseEnvNotSet
|
|
}
|
|
|
|
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(passphraseBuffer)
|
|
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
|
|
}
|
|
|
|
// countVaultSecrets returns the number of secrets in the vault directory
|
|
func (cli *Instance) countVaultSecrets(vaultDir string) (int, error) {
|
|
secretsDir := filepath.Join(vaultDir, "secrets.d")
|
|
|
|
exists, err := afero.DirExists(cli.fs, secretsDir)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to check secrets directory %s: %w",
|
|
secretsDir, err)
|
|
}
|
|
|
|
if !exists {
|
|
return 0, nil
|
|
}
|
|
|
|
entries, err := afero.ReadDir(cli.fs, secretsDir)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to read secrets directory %s: %w",
|
|
secretsDir, err)
|
|
}
|
|
|
|
return len(entries), nil
|
|
}
|
|
|
|
// switchAwayFromVault selects another vault as current before removal
|
|
func (cli *Instance) switchAwayFromVault(
|
|
cmd *cobra.Command, vaults []string, name string,
|
|
) error {
|
|
// Find another vault to switch to
|
|
var newVault string
|
|
|
|
for _, v := range vaults {
|
|
if v != name {
|
|
newVault = v
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// Switch to the new vault
|
|
err := vault.SelectVault(cli.fs, cli.stateDir, newVault)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to switch to vault '%s': %w", newVault, err)
|
|
}
|
|
|
|
cmd.Printf("Switched current vault to '%s'\n", newVault)
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveVault removes a vault and all its secrets, after asking the user
|
|
// to confirm unless force is set.
|
|
func (cli *Instance) RemoveVault(cmd *cobra.Command, name string, force bool) error {
|
|
err := vault.ValidateVaultName(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var found vaultToRemove
|
|
|
|
release, err := cli.askThenLock(cmd, force, func() (string, error) {
|
|
var err error
|
|
|
|
found, err = cli.findVaultToRemove(name)
|
|
|
|
return found.question, err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
// If removing current vault, switch to another vault first
|
|
if found.isCurrent {
|
|
err = cli.switchAwayFromVault(cmd, found.vaults, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Remove the vault directory
|
|
err = secret.RemoveDirAtomic(cli.fs, found.dir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove vault directory: %w", err)
|
|
}
|
|
|
|
cmd.Printf("Removed vault '%s'\n", name)
|
|
|
|
if found.secrets > 0 {
|
|
cmd.Printf("Warning: Vault contained secrets that have been " +
|
|
"permanently deleted\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// vaultToRemove is what removing a vault removes, as findVaultToRemove
|
|
// found it.
|
|
type vaultToRemove struct {
|
|
// dir is the vault's directory, which holds all its secrets.
|
|
dir string
|
|
secrets int
|
|
// vaults lists every vault, this one included, and isCurrent is set
|
|
// when this one is the current vault.
|
|
vaults []string
|
|
isCurrent bool
|
|
// question names what is removed, for the user to confirm.
|
|
question string
|
|
}
|
|
|
|
// findVaultToRemove checks that the vault exists and is not the last one,
|
|
// and counts its secrets.
|
|
func (cli *Instance) findVaultToRemove(name string) (vaultToRemove, error) {
|
|
vaults, err := vault.ListVaults(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return vaultToRemove{}, fmt.Errorf("failed to list vaults: %w", err)
|
|
}
|
|
|
|
if !slices.Contains(vaults, name) {
|
|
return vaultToRemove{},
|
|
fmt.Errorf("vault '%s' %w", name, errVaultDoesNotExist)
|
|
}
|
|
|
|
if len(vaults) == 1 {
|
|
return vaultToRemove{}, errCannotRemoveLastVault
|
|
}
|
|
|
|
currentVault, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return vaultToRemove{},
|
|
fmt.Errorf("failed to get current vault: %w", err)
|
|
}
|
|
|
|
vaultDir, err := vault.NewVault(cli.fs, cli.stateDir, name).GetDirectory()
|
|
if err != nil {
|
|
return vaultToRemove{},
|
|
fmt.Errorf("failed to get vault directory: %w", err)
|
|
}
|
|
|
|
secrets, err := cli.countVaultSecrets(vaultDir)
|
|
if err != nil {
|
|
return vaultToRemove{}, err
|
|
}
|
|
|
|
return vaultToRemove{
|
|
dir: vaultDir,
|
|
secrets: secrets,
|
|
vaults: vaults,
|
|
isCurrent: currentVault.GetName() == name,
|
|
question: fmt.Sprintf(
|
|
"Permanently remove vault '%s' and its %d secret(s)?",
|
|
name, secrets),
|
|
}, nil
|
|
}
|