Test-only. internal/blobgen and internal/types had no negative or boundary coverage. Adds, in package blobgen_test: Writer-to-Reader round trips at the 64 KiB age-segment edges for random and compressible data, checking plaintext, byte counts and the reader/writer hashes by decrypting; a wrong-identity open; truncation and single-byte corruption of a multi-segment blob at every region; trailing bytes, empty input and garbage; rejected and accepted compression levels; nil, empty and invalid recipients; and a failing destination. In package types_test: Value/Scan round trips, NULL, wrong-type and malformed Scan, Parse and IsZero for FileID and BlobID. The "cut right after the age header and nonce" truncation is excluded: it reads as valid and empty today and belongs to #152. Model: opus-4-8
120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
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 "--- <mac>\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
|
|
}
|