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, parsing the age key once. The second blob-ID hash step is now one exported blobgen.DoubleSHA256; Writer.Sum256 (the double hash) becomes Writer.ContentID so it no longer collides with Reader.Sum256 (the single plaintext hash). Also delete the never-adopted internal/types newtypes and the uncalled CleanupIncompleteSnapshots and its now-dead deleteSnapshot caller, and correct ARCHITECTURE.md. No production behavior changes. 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")
|
|
}
|