package blobgen_test import ( "bytes" "crypto/rand" "errors" "io" "testing" "filippo.io/age" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/blobgen" ) // ageChunkSize is age's STREAM plaintext chunk size (64 KiB); each encrypted // chunk adds a 16-byte ChaCha20-Poly1305 tag. const ( ageChunkSize = 64 * 1024 ageChunkTagSize = 16 ageSegmentSize = ageChunkSize + ageChunkTagSize ageNonceSize = 16 ) // makeIdentity returns a fresh X25519 identity and its recipient string. func makeIdentity(t *testing.T) (*age.X25519Identity, string) { t.Helper() id, err := age.GenerateX25519Identity() require.NoError(t, err) return id, id.Recipient().String() } // randomBytes returns n cryptographically random bytes, which do not compress // so the encrypted payload spans multiple age segments. func randomBytes(t *testing.T, n int) []byte { t.Helper() b := make([]byte, n) _, err := rand.Read(b) require.NoError(t, err) return b } // compressibleBytes returns n bytes of a repeating pattern, which zstd packs // down to a small payload. func compressibleBytes(n int) []byte { pattern := bytes.Repeat([]byte("compressible-"), n/13+1) return pattern[:n] } // encryptBlob compresses, encrypts and returns a blob for plaintext at // compression level 1. func encryptBlob(t *testing.T, plaintext []byte, recipients ...string) []byte { t.Helper() var buf bytes.Buffer w, err := blobgen.NewWriter(&buf, 1, recipients) require.NoError(t, err) _, err = w.Write(plaintext) require.NoError(t, err) require.NoError(t, w.Close()) return buf.Bytes() } // ageHeaderLen returns the byte length of blob's age header, i.e. the offset // of the 16-byte payload nonce that follows it. The header ends with a MAC // line "--- \n"; the nonce begins right after that newline. func ageHeaderLen(t *testing.T, blob []byte) int { t.Helper() i := bytes.Index(blob, []byte("\n--- ")) require.GreaterOrEqual(t, i, 0, "age MAC footer line not found") nl := bytes.IndexByte(blob[i+1:], '\n') require.GreaterOrEqual(t, nl, 0, "newline ending MAC line not found") return i + 1 + nl + 1 } // requireBlobUnreadable asserts that data never decrypts to a plaintext with a // nil error: either NewReader fails, or reading it does. func requireBlobUnreadable(t *testing.T, data []byte, id age.Identity) { t.Helper() r, err := blobgen.NewReader(bytes.NewReader(data), id) if err != nil { return } _, err = io.ReadAll(r) _ = r.Close() require.Error(t, err, "reading a damaged blob must fail") } // errFailWriter is returned by failAfterWriter once its byte limit is passed. var errFailWriter = errors.New("destination write failed") // failAfterWriter accepts writes until more than limit bytes have been sent, // then fails every write. It models a destination that dies mid-blob. type failAfterWriter struct { limit int written int } func (f *failAfterWriter) Write(p []byte) (int, error) { f.written += len(p) if f.written > f.limit { return 0, errFailWriter } return len(p), nil }