Remove secret vault import (closes #139)
check / check (push) Waiting to run

`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
This commit is contained in:
2026-10-14 22:54:58 +00:00
parent faf46d12ff
commit 0d18c3d4b8
11 changed files with 43 additions and 453 deletions
+1 -7
View File
@@ -277,10 +277,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 <secret-name> [--input=file] [--output=file]`
@@ -386,9 +382,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.
The prompt asks on the terminal the command runs in, not on stdin, so it also
asks when stdin is a pipe, as in `echo value | secret add name --force` or
+24
View File
@@ -141,6 +141,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
+5 -3
View File
@@ -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 <name> (to create another vault)")
return nil
}
+2 -11
View File
@@ -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)
-77
View File
@@ -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,
+5 -13
View File
@@ -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)
}},
+1 -5
View File
@@ -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)
}},
-16
View File
@@ -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)
}
-231
View File
@@ -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 <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 {
@@ -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")
+5 -5
View File
@@ -215,11 +215,11 @@ func processMnemonicForVault(
// CreateVault creates a new vault and selects it as the current vault. When
// mnemonic is not nil, the vault's long-term key is derived from it, and the
// returned vault has it as its Mnemonic; when it is nil, the vault has no
// long-term key until one is imported. When passphrase is not nil, the vault
// gets a passphrase unlocker protected by it, as its current unlocker; that
// needs a mnemonic. It refuses a vault that already exists before writing
// anything: creating it again would replace its keys, and its secrets could
// no longer be decrypted. The commands that call it hold the state directory
// long-term key. When passphrase is not nil, the vault gets a passphrase
// unlocker protected by it, as its current unlocker; that needs a mnemonic.
// It refuses a vault that already exists before writing anything: creating
// it again would replace its keys, and its secrets could no longer be
// decrypted. The commands that call it hold the state directory
// lock, so no other command can create the vault between the check and the
// writes.
//
-85
View File
@@ -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()