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:
22
go.mod
Normal file
22
go.mod
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module github.com/salvacybersec/keyhunter
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.11.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.70.0 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.48.1 // indirect
|
||||||
|
)
|
||||||
33
go.sum
Normal file
33
go.sum
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||||
|
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA=
|
||||||
|
modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||||
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