check / check (pull_request) Successful in 1m22s
Production encryption and decryption already run through blobgen; the crypto package (Encryptor, Decryptor, UpdateRecipients, the fx Module) and Vaultik.GetEncryptor/GetDecryptor had no production caller. Delete crypto and route verify --deep through the same blobgen reader restore uses: it parses the age key once and reads both the database (still streamed to a temp file) and every blob through blobgen.NewReader. The second, blob-ID hash step is now one exported function, blobgen.DoubleSHA256, called by the three former copies. Writer.Sum256 (the double hash) becomes Writer.ContentID so it no longer shares the name Sum256 with Reader.Sum256 (the single plaintext hash). CompressData and CompressStream, unused outside tests, are deleted. Delete the unused, never-adopted secret/config newtypes in internal/types (the redacting AgeSecretKey and AWSSecretAccessKey plus AgeRecipient, S3Endpoint, BucketName, S3Prefix, AWSRegion, AWSAccessKeyID). Delete the uncalled CleanupIncompleteSnapshots (and deleteSnapshot, its only caller, now dead) and correct ARCHITECTURE.md. The only multi-recipient test moves to blobgen; the pre-#131 encrypted-bytes hash test is removed. Model: opus-4-8
128 lines
4.1 KiB
Go
128 lines
4.1 KiB
Go
package blobgen_test
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/blobgen"
|
|
)
|
|
|
|
// TestWriterHashIsDoubleHash verifies that Writer.ContentID() returns
|
|
// the double hash SHA256(SHA256(plaintext)) for security.
|
|
// Double hashing prevents attackers from confirming existence of known content.
|
|
func TestWriterHashIsDoubleHash(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test data - random data that doesn't compress well
|
|
testData := make([]byte, 1024*1024) // 1MB
|
|
_, err := rand.Read(testData)
|
|
require.NoError(t, err)
|
|
|
|
// Test recipient (generated with age-keygen)
|
|
testRecipient := "age1cplgrwj77ta54dnmydvvmzn64ltk83ankxl5sww04mrtmu62kv3s89gmvv"
|
|
|
|
// Create a buffer to capture the encrypted output
|
|
var encryptedBuf bytes.Buffer
|
|
|
|
// Create blobgen writer
|
|
writer, err := blobgen.NewWriter(&encryptedBuf, 3, []string{testRecipient})
|
|
require.NoError(t, err)
|
|
|
|
// Write test data
|
|
n, err := writer.Write(testData)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, len(testData), n)
|
|
|
|
// Close to flush all data
|
|
err = writer.Close()
|
|
require.NoError(t, err)
|
|
|
|
// Get the hash from the writer
|
|
writerHash := hex.EncodeToString(writer.ContentID())
|
|
|
|
// Calculate the expected double hash: SHA256(SHA256(plaintext))
|
|
firstHash := sha256.Sum256(testData)
|
|
secondHash := sha256.Sum256(firstHash[:])
|
|
expectedDoubleHash := hex.EncodeToString(secondHash[:])
|
|
|
|
// Also compute single hash to verify it's different
|
|
singleHashStr := hex.EncodeToString(firstHash[:])
|
|
|
|
t.Logf("Input size: %d bytes", len(testData))
|
|
t.Logf("Single hash (SHA256(data)): %s", singleHashStr)
|
|
t.Logf("Double hash (SHA256(SHA256(data))): %s", expectedDoubleHash)
|
|
t.Logf("Writer hash: %s", writerHash)
|
|
|
|
// The writer hash should match the double hash
|
|
assert.Equal(t, expectedDoubleHash, writerHash,
|
|
"Writer.ContentID() should return SHA256(SHA256(plaintext)) for security")
|
|
|
|
// Verify it's NOT the single hash (would leak information)
|
|
assert.NotEqual(t, singleHashStr, writerHash,
|
|
"Writer hash should not be single hash (would allow content confirmation attacks)")
|
|
}
|
|
|
|
// TestWriterDeterministicHash verifies that the same input always produces
|
|
// the same hash, even with non-deterministic encryption.
|
|
func TestWriterDeterministicHash(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test data
|
|
testData := []byte("Hello, World! This is test data for deterministic hashing.")
|
|
|
|
// Test recipient
|
|
testRecipient := "age1cplgrwj77ta54dnmydvvmzn64ltk83ankxl5sww04mrtmu62kv3s89gmvv"
|
|
|
|
// Create two writers and verify they produce the same hash
|
|
var buf1, buf2 bytes.Buffer
|
|
|
|
writer1, err := blobgen.NewWriter(&buf1, 3, []string{testRecipient})
|
|
require.NoError(t, err)
|
|
_, err = writer1.Write(testData)
|
|
require.NoError(t, err)
|
|
require.NoError(t, writer1.Close())
|
|
|
|
writer2, err := blobgen.NewWriter(&buf2, 3, []string{testRecipient})
|
|
require.NoError(t, err)
|
|
_, err = writer2.Write(testData)
|
|
require.NoError(t, err)
|
|
require.NoError(t, writer2.Close())
|
|
|
|
hash1 := hex.EncodeToString(writer1.ContentID())
|
|
hash2 := hex.EncodeToString(writer2.ContentID())
|
|
|
|
// Hashes should be identical (deterministic)
|
|
assert.Equal(t, hash1, hash2, "Same input should produce same hash")
|
|
|
|
// Encrypted outputs should be different (non-deterministic encryption)
|
|
assert.NotEqual(t, buf1.Bytes(), buf2.Bytes(),
|
|
"Encrypted outputs should differ due to non-deterministic encryption")
|
|
|
|
t.Logf("Hash 1: %s", hash1)
|
|
t.Logf("Hash 2: %s", hash2)
|
|
t.Logf("Encrypted size 1: %d bytes", buf1.Len())
|
|
t.Logf("Encrypted size 2: %d bytes", buf2.Len())
|
|
}
|
|
|
|
// TestNewWriterSecretKeyNotEchoed verifies that a secret key mistakenly passed
|
|
// as a recipient does not appear in the returned error. A recipient string can
|
|
// be sensitive, so the error must name only the position, not the value.
|
|
func TestNewWriterSecretKeyNotEchoed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
secretKey := "AGE-SECRET-KEY-19CR5YSFW59HM4TLD6GX" +
|
|
"VEDMZFTVVF7PPHKUT68TXSFPK7APHXA2QS2NJA5"
|
|
|
|
var buf bytes.Buffer
|
|
|
|
_, err := blobgen.NewWriter(&buf, 3, []string{secretKey})
|
|
require.Error(t, err)
|
|
assert.NotContains(t, err.Error(), secretKey,
|
|
"error must not echo the recipient value")
|
|
}
|