package blobgen_test import ( "bytes" "io" "testing" "filippo.io/age" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/blobgen" ) // TestReaderRejectsHeaderNonceTruncation guards against a stream cut right // after the age header plus its 16-byte nonce. age.Decrypt still succeeds on // such an object, and the zstd decoder maps the age reader's // io.ErrUnexpectedEOF to a clean io.EOF at frame start, so without the extra // check the truncated stream would read as a valid empty one. Reading it must // now fail. func TestReaderRejectsHeaderNonceTruncation(t *testing.T) { t.Parallel() identity, err := age.GenerateX25519Identity() require.NoError(t, err) // Encrypting empty plaintext yields header + nonce(16) + a single // 16-byte final chunk tag. Dropping the trailing tag leaves exactly the // age header plus its nonce — the truncation point that triggers the bug. var full bytes.Buffer w, err := age.Encrypt(&full, identity.Recipient()) require.NoError(t, err) require.NoError(t, w.Close()) truncated := full.Bytes()[:full.Len()-16] reader, err := blobgen.NewReader(bytes.NewReader(truncated), identity) require.NoError(t, err) defer func() { _ = reader.Close() }() _, err = io.ReadAll(reader) require.Error(t, err) require.ErrorIs(t, err, io.ErrUnexpectedEOF) } // TestReaderReadsGenuinelyEmptyBlob confirms the truncation check does not // reject a legitimately empty payload: a blob written with no data must round // trip back to zero bytes with no error. func TestReaderReadsGenuinelyEmptyBlob(t *testing.T) { t.Parallel() identity, err := age.GenerateX25519Identity() require.NoError(t, err) var encrypted bytes.Buffer writer, err := blobgen.NewWriter( &encrypted, 3, []string{identity.Recipient().String()}) require.NoError(t, err) require.NoError(t, writer.Close()) reader, err := blobgen.NewReader(bytes.NewReader(encrypted.Bytes()), identity) require.NoError(t, err) defer func() { _ = reader.Close() }() data, err := io.ReadAll(reader) require.NoError(t, err) require.Empty(t, data) }