test(01-foundation-03): add failing tests for storage layer
- Tests for AES-256-GCM encrypt/decrypt roundtrip - Tests for Argon2id key derivation determinism - Tests for SQLite open with schema tables - Tests for SaveFinding/ListFindings with encryption contract - Tests verify raw BLOB does not contain plaintext key
This commit is contained in:
127
pkg/storage/db_test.go
Normal file
127
pkg/storage/db_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package storage_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/salvacybersec/keyhunter/pkg/storage"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDBOpen(t *testing.T) {
|
||||
db, err := storage.Open(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
// Verify schema tables exist
|
||||
rows, err := db.SQL().Query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
require.NoError(t, err)
|
||||
defer rows.Close()
|
||||
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var name string
|
||||
require.NoError(t, rows.Scan(&name))
|
||||
tables = append(tables, name)
|
||||
}
|
||||
assert.Contains(t, tables, "findings")
|
||||
assert.Contains(t, tables, "scans")
|
||||
assert.Contains(t, tables, "settings")
|
||||
}
|
||||
|
||||
func TestEncryptDecryptRoundtrip(t *testing.T) {
|
||||
key := make([]byte, 32) // all-zero key for test
|
||||
for i := range key {
|
||||
key[i] = byte(i)
|
||||
}
|
||||
plaintext := []byte("sk-proj-supersecretapikey1234")
|
||||
|
||||
ciphertext, err := storage.Encrypt(plaintext, key)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(ciphertext), len(plaintext), "ciphertext should be longer than plaintext")
|
||||
|
||||
recovered, err := storage.Decrypt(ciphertext, key)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, plaintext, recovered)
|
||||
}
|
||||
|
||||
func TestEncryptNonDeterministic(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
plain := []byte("test-key")
|
||||
ct1, err1 := storage.Encrypt(plain, key)
|
||||
ct2, err2 := storage.Encrypt(plain, key)
|
||||
require.NoError(t, err1)
|
||||
require.NoError(t, err2)
|
||||
assert.NotEqual(t, ct1, ct2, "same plaintext encrypted twice should produce different ciphertext")
|
||||
}
|
||||
|
||||
func TestDecryptWrongKey(t *testing.T) {
|
||||
key1 := make([]byte, 32)
|
||||
key2 := make([]byte, 32)
|
||||
key2[0] = 0xFF
|
||||
|
||||
ct, err := storage.Encrypt([]byte("secret"), key1)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = storage.Decrypt(ct, key2)
|
||||
assert.Error(t, err, "decryption with wrong key should fail")
|
||||
}
|
||||
|
||||
func TestArgon2KeyDerivation(t *testing.T) {
|
||||
passphrase := []byte("my-secure-passphrase")
|
||||
salt := []byte("1234567890abcdef") // 16 bytes
|
||||
|
||||
key1 := storage.DeriveKey(passphrase, salt)
|
||||
key2 := storage.DeriveKey(passphrase, salt)
|
||||
|
||||
assert.Equal(t, 32, len(key1), "derived key must be 32 bytes")
|
||||
assert.Equal(t, key1, key2, "same passphrase+salt must produce same key")
|
||||
}
|
||||
|
||||
func TestNewSalt(t *testing.T) {
|
||||
salt1, err1 := storage.NewSalt()
|
||||
salt2, err2 := storage.NewSalt()
|
||||
require.NoError(t, err1)
|
||||
require.NoError(t, err2)
|
||||
assert.Equal(t, 16, len(salt1))
|
||||
assert.NotEqual(t, salt1, salt2, "two salts should differ")
|
||||
}
|
||||
|
||||
func TestSaveFindingEncrypted(t *testing.T) {
|
||||
db, err := storage.Open(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
// Derive a test key
|
||||
key := storage.DeriveKey([]byte("testpassphrase"), []byte("testsalt1234xxxx"))
|
||||
|
||||
plainKey := "sk-proj-test1234567890abcdefghijklmnopqr"
|
||||
f := storage.Finding{
|
||||
ProviderName: "openai",
|
||||
KeyValue: plainKey,
|
||||
Confidence: "high",
|
||||
SourcePath: "/test/file.env",
|
||||
SourceType: "file",
|
||||
LineNumber: 42,
|
||||
}
|
||||
|
||||
id, err := db.SaveFinding(f, key)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, id, int64(0))
|
||||
|
||||
findings, err := db.ListFindings(key)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, findings, 1)
|
||||
assert.Equal(t, plainKey, findings[0].KeyValue)
|
||||
assert.Equal(t, "openai", findings[0].ProviderName)
|
||||
// Verify masking
|
||||
assert.Equal(t, "sk-proj-...opqr", findings[0].KeyMasked)
|
||||
|
||||
// Verify encryption contract: raw BLOB bytes in the database must NOT contain the plaintext key.
|
||||
// This confirms Encrypt() was called before INSERT, not that the key was stored verbatim.
|
||||
var rawBlob []byte
|
||||
require.NoError(t, db.SQL().QueryRow("SELECT key_value FROM findings WHERE id = ?", id).Scan(&rawBlob))
|
||||
assert.False(t, bytes.Contains(rawBlob, []byte(plainKey)),
|
||||
"raw database BLOB must not contain plaintext key — encryption was not applied")
|
||||
}
|
||||
Reference in New Issue
Block a user