//nolint:testpackage // white-box test of unexported internals package secret import ( "errors" "os" "path/filepath" "strings" "testing" "filippo.io/age" "github.com/awnumar/memguard" "github.com/spf13/afero" "sneak.berlin/go/secret/pkg/agehd" ) // testMnemonicValue is the standard BIP39 test vector mnemonic. // //nolint:dupword // BIP39 test mnemonic repeats words by design const testMnemonicValue = "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about" var ( errMnemonicNotSet = errors.New("mock vault has no mnemonic") errNotImplementedInMock = errors.New("not implemented in mock") ) // TestMain makes passphrase encryption in the tests cheap; see // ScryptWorkFactor. func TestMain(m *testing.M) { ScryptWorkFactor = 1 os.Exit(m.Run()) } // MockVault is a test implementation of the VaultInterface type MockVault struct { name string fs afero.Fs directory string derivationIndex uint32 mnemonic *memguard.LockedBuffer } func (m *MockVault) GetDirectory() (string, error) { return m.directory, nil } func (m *MockVault) AddSecret(name string, value *memguard.LockedBuffer, _ bool) error { // Create secret directory with proper storage name conversion storageName := strings.ReplaceAll(name, "/", "%") secretDir := filepath.Join(m.directory, "secrets.d", storageName) err := m.fs.MkdirAll(secretDir, 0o700) if err != nil { return err } // Create version directory with proper path versionName := "20240101.001" // Use a fixed version name for testing versionDir := filepath.Join(secretDir, "versions", versionName) err = m.fs.MkdirAll(versionDir, 0o700) if err != nil { return err } // Read the vault's long-term public key ltPubKeyPath := filepath.Join(m.directory, "pub.age") // Derive long-term key using the vault's derivation index if m.mnemonic == nil { return errMnemonicNotSet } ltIdentity, err := agehd.DeriveIdentity(m.mnemonic.String(), m.derivationIndex) if err != nil { return err } // Write long-term public key if it doesn't exist _, err = m.fs.Stat(ltPubKeyPath) if os.IsNotExist(err) { pubKey := ltIdentity.Recipient().String() err = afero.WriteFile(m.fs, ltPubKeyPath, []byte(pubKey), 0o600) if err != nil { return err } } err = m.writeVersionFiles(versionDir, value, ltIdentity) if err != nil { return err } // Create current file pointing to the version (just the version name) currentLink := filepath.Join(secretDir, "current") return afero.WriteFile(m.fs, currentLink, []byte(versionName), 0o600) } func (m *MockVault) GetName() string { return m.name } //nolint:ireturn // implements VaultInterface func (m *MockVault) GetFilesystem() afero.Fs { return m.fs } //nolint:ireturn // implements VaultInterface func (m *MockVault) GetCurrentUnlocker() (Unlocker, error) { return nil, errNotImplementedInMock } func (m *MockVault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) { return nil, errNotImplementedInMock } func (m *MockVault) SetMnemonic(mnemonic *memguard.LockedBuffer) { m.mnemonic = mnemonic } func (m *MockVault) SetUnlockPassphrase(_ *memguard.LockedBuffer) {} func (m *MockVault) CreatePassphraseUnlocker( _ *memguard.LockedBuffer, ) (*PassphraseUnlocker, error) { return nil, errNotImplementedInMock } // writeVersionFiles generates a version keypair and writes the version // key and value files for the mock vault. func (m *MockVault) writeVersionFiles( versionDir string, value *memguard.LockedBuffer, ltIdentity *age.X25519Identity, ) error { // Generate version-specific keypair versionIdentity, err := age.GenerateX25519Identity() if err != nil { return err } // Write version public key pubKeyPath := filepath.Join(versionDir, "pub.age") err = afero.WriteFile( m.fs, pubKeyPath, []byte(versionIdentity.Recipient().String()), 0o600) if err != nil { return err } // Encrypt value to version's public key (value is already a LockedBuffer) encryptedValue, err := EncryptToRecipient(value, versionIdentity.Recipient()) if err != nil { return err } // Write encrypted value valuePath := filepath.Join(versionDir, "value.age") err = afero.WriteFile(m.fs, valuePath, encryptedValue, 0o600) if err != nil { return err } // Encrypt version private key to long-term public key versionPrivKeyBuffer := memguard.NewBufferFromBytes([]byte(versionIdentity.String())) defer versionPrivKeyBuffer.Destroy() encryptedPrivKey, err := EncryptToRecipient( versionPrivKeyBuffer, ltIdentity.Recipient()) if err != nil { return err } // Write encrypted version private key privKeyPath := filepath.Join(versionDir, "priv.age") return afero.WriteFile(m.fs, privKeyPath, encryptedPrivKey, 0o600) } // setupMockVaultDirs creates the vault directory structure, long-term // public key, and current vault pointer for tests. func setupMockVaultDirs(t *testing.T, fs afero.Fs, baseDir, vaultDir string) { t.Helper() // Create vault directory structure err := fs.MkdirAll(filepath.Join(vaultDir, "secrets.d"), DirPerms) if err != nil { t.Fatalf("Failed to create vault directory: %v", err) } // Generate a long-term keypair for the vault using the test mnemonic ltIdentity, err := agehd.DeriveIdentity(testMnemonicValue, 0) if err != nil { t.Fatalf("Failed to generate long-term identity: %v", err) } // Write long-term public key ltPubKeyPath := filepath.Join(vaultDir, "pub.age") err = afero.WriteFile( fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600, ) if err != nil { t.Fatalf("Failed to write long-term public key: %v", err) } // Set current vault currentVaultPath := filepath.Join(baseDir, "currentvault") err = afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), FilePerms) if err != nil { t.Fatalf("Failed to set current vault: %v", err) } } // verifySecretFiles checks that AddSecret created the expected version // files for the secret. func verifySecretFiles(t *testing.T, fs afero.Fs, vaultDir, secretName string) { t.Helper() secretDir := filepath.Join(vaultDir, "secrets.d", secretName) // Check versions directory exists versionsDir := filepath.Join(secretDir, "versions") versionsDirExists, err := afero.DirExists(fs, versionsDir) if err != nil || !versionsDirExists { t.Fatalf("versions directory was not created") } // Check current file exists and points at a version currentVersion, err := GetCurrentVersion(fs, secretDir) if err != nil { t.Fatalf("Failed to get current version: %v", err) } // Check value.age exists in the version directory versionDir := filepath.Join(versionsDir, currentVersion) valueExists, err := afero.Exists(fs, filepath.Join(versionDir, "value.age")) if err != nil || !valueExists { t.Fatalf("value.age file was not created in version directory") } } //nolint:paralleltest // subtests share one vault, order matters func TestPerSecretKeyFunctionality(t *testing.T) { // Create an in-memory filesystem for testing fs := afero.NewMemMapFs() mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonicValue)) defer mnemonic.Destroy() // Set up a test vault structure baseDir := "/test-config/berlin.sneak.pkg.secret" vaultDir := filepath.Join(baseDir, "vaults.d", "test-vault") setupMockVaultDirs(t, fs, baseDir, vaultDir) // Create vault instance using the mock vault vault := &MockVault{ name: "test-vault", fs: fs, directory: vaultDir, derivationIndex: 0, mnemonic: mnemonic, } // Test data secretName := "test-secret" secretValue := []byte("this is a test secret value") // Create a secure buffer for the test value valueBuffer := memguard.NewBufferFromBytes(secretValue) defer valueBuffer.Destroy() // Test AddSecret t.Run("AddSecret", func(t *testing.T) { err := vault.AddSecret(secretName, valueBuffer, false) if err != nil { t.Fatalf("AddSecret failed: %v", err) } // Verify that all expected files were created verifySecretFiles(t, fs, vaultDir, secretName) t.Logf("All expected files created successfully with versioning") }) // Create a Secret object to test with secret := NewSecret(vault, secretName) // Test GetValue (this will need to be modified since we're using a mock vault) t.Run("GetSecret", func(t *testing.T) { // This test is simplified since we're not implementing the full encryption/decryption // in the mock. We just verify the Secret object is created correctly. if secret.Name != secretName { t.Fatalf("Secret name doesn't match. Expected: %s, Got: %s", secretName, secret.Name) } if secret.vault != vault { t.Fatalf("Secret vault reference doesn't match expected vault") } t.Logf("Successfully created Secret object with correct properties") }) // Test Exists t.Run("SecretExists", func(t *testing.T) { exists, err := secret.Exists() if err != nil { t.Fatalf("Error checking if secret exists: %v", err) } if !exists { t.Fatalf("Secret should exist but Exists() returned false") } t.Logf("Secret.Exists() works correctly") }) }