From 956935172bede89330f61fbf1a322b62afe3b359 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 14 Oct 2026 21:14:15 +0000 Subject: [PATCH] Remove secret vault import (closes #139) `secret vault import` took only a vault without a long-term key, and `secret init` and `secret vault create` give every vault one, so it always failed. A mnemonic enters through those two commands, so the command goes, with its tests and its README entry. The help and the note of `secret generate mnemonic` named `secret import`, which imports a file as a secret; they now name `secret init` and `secret vault create`. `secret vault import` now prints the help of `secret vault` and exits 0, as cobra does for any subcommand of `secret vault` it does not know. Judgement call: TestWorkflowMismatch in internal/vault, which replayed the import step by step, is deleted. Model: opus-5-5 --- README.md | 8 +- TODO.md | 8 + internal/cli/create_vault_test.go | 24 +++ internal/cli/generate.go | 8 +- internal/cli/input_errors_test.go | 13 +- internal/cli/integration_test.go | 77 ---------- internal/cli/lock_test.go | 18 +-- internal/cli/path_traversal_test.go | 6 +- internal/cli/unreadable_dir_test.go | 16 -- internal/cli/vault.go | 231 ---------------------------- internal/vault/metadata_test.go | 85 ---------- 11 files changed, 46 insertions(+), 448 deletions(-) diff --git a/README.md b/README.md index 4651c36..922cc77 100644 --- a/README.md +++ b/README.md @@ -268,10 +268,6 @@ Selects an unlocker as the current default for operations. Imports a secret from a file and stores it in the current vault under the given name. -#### `secret vault import [vault-name]` - -Imports a mnemonic phrase into the specified vault (defaults to "default"). - ### Encryption Operations #### `secret encrypt [--input=file] [--output=file]` @@ -377,9 +373,7 @@ line or in a CI job, they end up in shell history and CI logs. `secret` unsets each one as soon as it has read it, so that the programs it runs itself, such as `gpg`, do not inherit it, but that erases nothing: the environment the process started with, and its memory, still hold the value. The interactive prompt, -which every command except `secret vault import` offers when the variable is not -set, is the safer default; `secret vault import` has no prompt and needs both -variables. +which every command offers when the variable is not set, is the safer default. ## Security Features diff --git a/TODO.md b/TODO.md index 857a05f..5680673 100644 --- a/TODO.md +++ b/TODO.md @@ -18,6 +18,14 @@ https://git.eeqj.de/sneak/secret/milestone/12 # Completed Steps +- 2026-10-14: `secret vault import` is gone + (https://git.eeqj.de/sneak/secret/issues/139). It took only a vault without a + long-term key, and `secret init` and `secret vault create` give every vault + one, so it always failed. A mnemonic enters through those two commands, which + the help and the note of `secret generate mnemonic` now name; they named + `secret import`, which imports a file as a secret. `secret vault import` now + prints the help of `secret vault`, which cobra prints for any subcommand of + `secret vault` that does not exist. - 2026-10-14: `secret generate mnemonic`, `secret vault list` and `secret unlocker list` print their output to stdout, as `secret list` does, so it can be piped or captured (https://git.eeqj.de/sneak/secret/issues/138). diff --git a/internal/cli/create_vault_test.go b/internal/cli/create_vault_test.go index 17838d7..964c911 100644 --- a/internal/cli/create_vault_test.go +++ b/internal/cli/create_vault_test.go @@ -140,6 +140,30 @@ func TestVaultCreationLeavesNoSecretInEnvironment(t *testing.T) { run("vault", "create", "work") } +// TestNoVaultImportCommand is a regression test for +// https://git.eeqj.de/sneak/secret/issues/139: `secret vault import` +// refused every vault that `secret vault create` makes, because each has a +// long-term key. The command is gone, so cobra answers it with the help of +// `secret vault`, as it does any subcommand of `secret vault` it does not +// know. +func TestNoVaultImportCommand(t *testing.T) { + t.Setenv(secret.EnvStateDir, t.TempDir()) + + run := func(args ...string) string { + t.Setenv(secret.EnvMnemonic, testMnemonic) + t.Setenv(secret.EnvUnlockPassphrase, testPassphrase) + + output, err := cli.ExecuteCommandInProcess(args, "", nil) + require.NoError(t, err, "%v", args) + + return output + } + + run("vault", "create", "work") + + require.Equal(t, run("vault"), run("vault", "import", "work")) +} + // TestStopAtPassphrasePromptLeavesNothing is a regression test for the // review of https://git.eeqj.de/sneak/secret/pulls/82: `secret init` or // `secret vault create` stopped at the passphrase prompt left a vault with diff --git a/internal/cli/generate.go b/internal/cli/generate.go index 631e6bd..f902358 100644 --- a/internal/cli/generate.go +++ b/internal/cli/generate.go @@ -43,7 +43,7 @@ func newGenerateMnemonicCmd() *cobra.Command { Short: "Generate a random BIP39 mnemonic phrase", Long: `Generate a cryptographically secure random BIP39 ` + `mnemonic phrase that can be used with 'secret init' ` + - `or 'secret import'.`, + `or 'secret vault create'.`, RunE: func(cmd *cobra.Command, _ []string) error { cli, err := NewCLIInstance() if err != nil { @@ -115,8 +115,10 @@ func (cli *Instance) GenerateMnemonic(cmd *cobra.Command) error { " • If you lose this phrase, your secrets cannot be recovered") fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "Use this mnemonic with:") - fmt.Fprintln(os.Stderr, " secret init (to initialize a new secret manager)") - fmt.Fprintln(os.Stderr, " secret import (to import into an existing vault)") + fmt.Fprintln(os.Stderr, + " secret init (to initialize a new secret manager)") + fmt.Fprintln(os.Stderr, + " secret vault create (to create another vault)") return nil } diff --git a/internal/cli/input_errors_test.go b/internal/cli/input_errors_test.go index fae6d1b..54280b3 100644 --- a/internal/cli/input_errors_test.go +++ b/internal/cli/input_errors_test.go @@ -7,12 +7,10 @@ import ( "github.com/awnumar/memguard" "github.com/spf13/afero" "github.com/stretchr/testify/require" - "sneak.berlin/go/secret/internal/vault" ) // TestInvalidMnemonicError checks that every command that takes a mnemonic -// returns errInvalidMnemonicPhrase for one that is not valid BIP39. The vault -// "other" has no long-term key, as vault import needs. +// returns errInvalidMnemonicPhrase for one that is not valid BIP39. func TestInvalidMnemonicError(t *testing.T) { t.Parallel() @@ -24,20 +22,13 @@ func TestInvalidMnemonicError(t *testing.T) { {"secret vault create work", func(c *Instance) error { return c.CreateVault(c.cmd, "work") }}, - {"secret vault import other", func(c *Instance) error { - return c.VaultImport(c.cmd, "other") - }}, } for _, tt := range tests { t.Run(tt.command, func(t *testing.T) { t.Parallel() - fs := afero.NewMemMapFs() - _, err := vault.CreateVault(fs, listTestStateDir, "other", nil, nil) - require.NoError(t, err) - - instance, _ := newTestInstance(fs) + instance, _ := newTestInstance(afero.NewMemMapFs()) instance.Mnemonic = memguard.NewBufferFromBytes([]byte("not a mnemonic")) t.Cleanup(instance.Mnemonic.Destroy) diff --git a/internal/cli/integration_test.go b/internal/cli/integration_test.go index 3507aa6..1d5a3d9 100644 --- a/internal/cli/integration_test.go +++ b/internal/cli/integration_test.go @@ -140,16 +140,6 @@ func TestSecretManagerIntegration(t *testing.T) { // - currentvault symlink updated to point to work test03CreateVault(t, tempDir, runSecret) - // Test 4: Import mnemonic into work vault - // Command: secret vault import work - // Purpose: Import mnemonic with passphrase unlocker - // Expected filesystem: - // - work vault has pub.age file - // - work vault has unlockers.d/passphrase directory - // - Unlocker metadata and encrypted keys present - // NOTE: Skipped because vault creation now includes mnemonic import - // test04ImportMnemonic(t, tempDir, testMnemonic, testPassphrase, runSecretWithEnv) - // Test 5: Add secrets with versioning // Command: echo "password123" | secret add database/password // Purpose: Add a secret and verify versioned storage @@ -535,65 +525,6 @@ func test03CreateVault(t *testing.T, tempDir string, runSecret func(...string) ( assert.Contains(t, output, "work", "should list work vault") } -//nolint:unused // TODO: re-enable when vault import is implemented -func test04ImportMnemonic(t *testing.T, tempDir, testMnemonic, testPassphrase string, runSecretWithEnv func(map[string]string, ...string) (string, error)) { - t.Helper() - - // Import mnemonic into work vault - output, err := runSecretWithEnv(map[string]string{ - secret.EnvMnemonic: testMnemonic, - secret.EnvUnlockPassphrase: testPassphrase, - }, "vault", "import", "work") - - require.NoError(t, err, "vault import should succeed") - assert.Contains(t, output, "Successfully imported mnemonic into vault 'work'", "should confirm import") - - // Verify work vault now has long-term key - workVaultDir := filepath.Join(tempDir, "vaults.d", "work") - pubKeyFile := filepath.Join(workVaultDir, "pub.age") - verifyFileExists(t, pubKeyFile) - - // Verify passphrase unlocker was created - currentUnlocker := readFile(t, filepath.Join(workVaultDir, "current-unlocker")) - passphraseUnlockerDir := filepath.Join(workVaultDir, "unlockers.d", string(currentUnlocker)) - verifyFileExists(t, passphraseUnlockerDir) - - // Check unlocker files - unlockerMetadata := filepath.Join(passphraseUnlockerDir, "unlocker-metadata.json") - verifyFileExists(t, unlockerMetadata) - - encryptedLTPrivKey := filepath.Join(passphraseUnlockerDir, "priv.age") - verifyFileExists(t, encryptedLTPrivKey) - - encryptedLTPubKey := filepath.Join(passphraseUnlockerDir, "pub.age") - verifyFileExists(t, encryptedLTPubKey) - - longtermKeyFile := filepath.Join(passphraseUnlockerDir, "longterm.age") - verifyFileExists(t, longtermKeyFile) - - // Verify vault metadata was created with derivation index - vaultMetadata := filepath.Join(workVaultDir, "vault-metadata.json") - verifyFileExists(t, vaultMetadata) - - metadataBytes := readFile(t, vaultMetadata) - - var metadata map[string]any - - err = json.Unmarshal(metadataBytes, &metadata) - require.NoError(t, err, "vault metadata should be valid JSON") - - // Work vault should have a different derivation index than default (0) - derivIndex, ok := metadata["derivationIndex"].(float64) - require.True(t, ok, "derivationIndex should be a number") - assert.NotEqual(t, float64(0), derivIndex, "work vault should have non-zero derivation index") - - // Verify public key hash is stored - assert.Contains(t, metadata, "publicKeyHash", "should contain public key hash") - pubKeyHash, ok := metadata["publicKeyHash"].(string) - require.True(t, ok, "publicKeyHash should be a string") - assert.NotEmpty(t, pubKeyHash, "public key hash should not be empty") -} - func test05AddSecret(t *testing.T, tempDir, testMnemonic string, runSecret func(...string) (string, error), runSecretWithEnv func(map[string]string, ...string) (string, error), runSecretWithStdin func(string, map[string]string, ...string) (string, error)) { t.Helper() @@ -1939,14 +1870,6 @@ func test23ErrorHandling(t *testing.T, tempDir, secretPath, testMnemonic string, _, err = runSecret("vault", "select", "nonexistent") require.ErrorIs(t, err, vault.ErrVaultNotFound, "select non-existent vault should fail") - // Import to non-existent vault with test passphrase - testPassphrase := "test-passphrase-123" // Define testPassphrase locally - _, err = runSecretWithEnv(map[string]string{ - secret.EnvMnemonic: testMnemonic, - secret.EnvUnlockPassphrase: testPassphrase, - }, "vault", "import", "nonexistent") - require.ErrorIs(t, err, vault.ErrVaultNotFound, "import to non-existent vault should fail") - // Get specific version that doesn't exist _, err = runSecretWithEnv(map[string]string{ secret.EnvMnemonic: testMnemonic, diff --git a/internal/cli/lock_test.go b/internal/cli/lock_test.go index caa2ee3..b199f0d 100644 --- a/internal/cli/lock_test.go +++ b/internal/cli/lock_test.go @@ -282,11 +282,10 @@ func stateDirModTimes(t *testing.T, fs afero.Fs) map[string]int64 { // setupEveryCommand makes what each command in // TestChangingCommandsWaitForLock needs: the current vault "work" with two -// versions of "test/secret", the vault "other" without a long-term key, for -// vault import, and the file testInput. There is no vault "default", which -// init creates. If withUnlocker is set, it also gives "work" a passphrase -// unlocker, which is slow. It returns the older version and the unlocker's -// ID. +// versions of "test/secret", the vault "other", and the file testInput. +// There is no vault "default", which init creates. If withUnlocker is set, +// it also gives "work" a passphrase unlocker, which is slow. It returns the +// older version and the unlocker's ID. func setupEveryCommand( t *testing.T, fs afero.Fs, withUnlocker bool, ) (string, string) { @@ -294,13 +293,9 @@ func setupEveryCommand( mnemonic := testMnemonicBuffer(t) - other, err := vault.CreateVault(fs, testStateDir, "other", mnemonic, nil) + _, err := vault.CreateVault(fs, testStateDir, "other", mnemonic, nil) require.NoError(t, err) - otherDir, err := other.GetDirectory() - require.NoError(t, err) - require.NoError(t, fs.Remove(filepath.Join(otherDir, "pub.age"))) - vlt, err := vault.CreateVault(fs, testStateDir, "work", mnemonic, nil) require.NoError(t, err) @@ -455,9 +450,6 @@ func TestChangingCommandsWaitForLock(t *testing.T) { {"vault select", false, func(cli *Instance, _, _ string) error { return cli.SelectVault(cli.cmd, "other") }}, - {"vault import", false, func(cli *Instance, _, _ string) error { - return cli.VaultImport(cli.cmd, "other") - }}, {"vault rm --force", false, func(cli *Instance, _, _ string) error { return cli.RemoveVault(cli.cmd, "other", true) }}, diff --git a/internal/cli/path_traversal_test.go b/internal/cli/path_traversal_test.go index 7c85250..5046c2c 100644 --- a/internal/cli/path_traversal_test.go +++ b/internal/cli/path_traversal_test.go @@ -306,8 +306,7 @@ func TestInvalidVersionLeavesVaultsUnchanged(t *testing.T) { // TestInvalidVaultNameLeavesStateUnchanged is a regression test for // https://git.eeqj.de/sneak/secret/issues/68, where -// `secret vault import ..` wrote a long-term key and an unlocker into the -// state directory itself, and `secret vault select ..` made it the current +// `secret vault select ..` made the state directory itself the current // vault. Each command that takes a vault name must reject an invalid one // before building a path from it. The instance is given the mnemonic and // the passphrase, and moves and removals use --force, so that only the name @@ -331,9 +330,6 @@ func TestInvalidVaultNameLeavesStateUnchanged(t *testing.T) { {"vault create %q", func(c *cli.Instance, name string) error { return c.CreateVault(cmd, name) }}, - {"vault import %q", func(c *cli.Instance, name string) error { - return c.VaultImport(cmd, name) - }}, {"vault select %q", func(c *cli.Instance, name string) error { return c.SelectVault(cmd, name) }}, diff --git a/internal/cli/unreadable_dir_test.go b/internal/cli/unreadable_dir_test.go index 96da582..fef88c2 100644 --- a/internal/cli/unreadable_dir_test.go +++ b/internal/cli/unreadable_dir_test.go @@ -371,19 +371,3 @@ func TestRemoveSecretAbortsWhenVersionsUnreadable(t *testing.T) { require.NoError(t, err) assert.True(t, exists, "the secret must not be removed") } - -// TestVaultImportAbortsWhenPubKeyUnreadable asserts that a mnemonic import -// stops when whether the vault already has a long-term key cannot be -// determined. -func TestVaultImportAbortsWhenPubKeyUnreadable(t *testing.T) { - t.Parallel() - - base := newListTestVault(t, 1) - instance, cmd := newTestInstance(&statFailFs{ - Fs: base, path: filepath.Join(testVaultDir(listTestVaultName), "pub.age"), - }) - - err := instance.importMnemonic(cmd, listTestVaultName) - - require.ErrorIs(t, err, errStatFailed) -} diff --git a/internal/cli/vault.go b/internal/cli/vault.go index d88e051..ecf874c 100644 --- a/internal/cli/vault.go +++ b/internal/cli/vault.go @@ -8,7 +8,6 @@ import ( "path/filepath" "slices" "strings" - "time" "github.com/awnumar/memguard" "github.com/spf13/afero" @@ -16,19 +15,12 @@ import ( "sneak.berlin/go/secret/internal/bip39" "sneak.berlin/go/secret/internal/secret" "sneak.berlin/go/secret/internal/vault" - "sneak.berlin/go/secret/pkg/agehd" ) // Sentinel errors for vault operations var ( errMnemonicEmpty = errors.New("mnemonic cannot be empty") errInvalidMnemonicPhrase = errors.New("invalid BIP39 mnemonic phrase") - 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") ) @@ -42,7 +34,6 @@ func newVaultCmd() *cobra.Command { cmd.AddCommand(newVaultListCmd()) cmd.AddCommand(newVaultCreateCmd()) cmd.AddCommand(newVaultSelectCmd()) - cmd.AddCommand(newVaultImportCmd()) cmd.AddCommand(newVaultRemoveCmd()) return cmd @@ -111,38 +102,6 @@ func newVaultSelectCmd() *cobra.Command { } } -func newVaultImportCmd() *cobra.Command { - cli, err := NewCLIInstance() - if err != nil { - log.Fatalf("failed to initialize CLI: %v", err) - } - - 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), - 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 { @@ -336,196 +295,6 @@ func (cli *Instance) SelectVault(cmd *cobra.Command, name string) error { 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, vault.ErrVaultNotFound) - } - - // 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 "", "", "", errInvalidMnemonicPhrase - } - - 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") diff --git a/internal/vault/metadata_test.go b/internal/vault/metadata_test.go index f3d6206..0b05b21 100644 --- a/internal/vault/metadata_test.go +++ b/internal/vault/metadata_test.go @@ -296,91 +296,6 @@ func TestSampleHashCalculation(t *testing.T) { } } -func TestWorkflowMismatch(t *testing.T) { - t.Parallel() - - // Create a temporary directory for testing - tempDir := t.TempDir() - fs := afero.NewOsFs() - - // Test Case 1: Create vault WITH mnemonic (like init command) - _, err := vault.CreateVault(fs, tempDir, "default", testMnemonicBuffer(t), nil) - if err != nil { - t.Fatalf("Failed to create vault with mnemonic: %v", err) - } - - // Load metadata for vault1 - vault1Dir := filepath.Join(tempDir, "vaults.d", "default") - - metadata1, err := vault.LoadVaultMetadata(fs, vault1Dir) - if err != nil { - t.Fatalf("Failed to load vault1 metadata: %v", err) - } - - t.Logf("Vault1 (with mnemonic) - DerivationIndex: %d, PublicKeyHash: %s", - metadata1.DerivationIndex, metadata1.PublicKeyHash) - - // Test Case 2: Create vault WITHOUT mnemonic, then import (work vault) - _, err = vault.CreateVault(fs, tempDir, "work", nil, nil) - if err != nil { - t.Fatalf("Failed to create vault without mnemonic: %v", err) - } - - vault2Dir := filepath.Join(tempDir, "vaults.d", "work") - - // Simulate the vault import process: get the next available derivation - // index for this mnemonic - derivationIndex, err := vault.GetNextDerivationIndex(fs, tempDir, testMnemonic) - if err != nil { - t.Fatalf("Failed to get next derivation index: %v", err) - } - - t.Logf("Next derivation index for import: %d", derivationIndex) - - // Calculate public key hash from index 0 (same as in VaultImport) - identity0, err := agehd.DeriveIdentity(testMnemonic, 0) - if err != nil { - t.Fatalf("Failed to derive identity for index 0: %v", err) - } - - publicKeyHash := vault.ComputeDoubleSHA256( - []byte(identity0.Recipient().String())) - - // Load existing metadata and update it (same as in VaultImport) - existingMetadata, err := vault.LoadVaultMetadata(fs, vault2Dir) - if err != nil { - t.Fatalf("Failed to load existing metadata: %v", err) - } - - // Update metadata with new derivation info - existingMetadata.DerivationIndex = derivationIndex - existingMetadata.PublicKeyHash = publicKeyHash - - err = vault.SaveVaultMetadata(fs, vault2Dir, existingMetadata) - if err != nil { - t.Fatalf("Failed to save vault metadata: %v", err) - } - - // Load updated metadata for vault2 - metadata2, err := vault.LoadVaultMetadata(fs, vault2Dir) - if err != nil { - t.Fatalf("Failed to load vault2 metadata: %v", err) - } - - t.Logf("Vault2 (imported mnemonic) - DerivationIndex: %d, PublicKeyHash: %s", - metadata2.DerivationIndex, metadata2.PublicKeyHash) - - // Verify that both vaults have the same public key hash - if metadata1.PublicKeyHash != metadata2.PublicKeyHash { - t.Errorf("Public key hashes don't match!") - t.Logf("Vault1 hash: %s", metadata1.PublicKeyHash) - t.Logf("Vault2 hash: %s", metadata2.PublicKeyHash) - } else { - t.Logf("SUCCESS: Both vaults have the same public key hash: %s", - metadata1.PublicKeyHash) - } -} - func TestReverseEngineerHash(t *testing.T) { t.Parallel()