Skip to content

Commit 5f0c59d

Browse files
committed
fix(crypto): reject legacy page format in encrypted files and strip encryption on empty recipients
- Decoder now rejects pages without the size flag when the file is encrypted, preventing bypass of per-page AEAD verification - FileSpec.WriteTo clears encryption header fields when no recipient keys are configured, preventing corrupt output from rekey without -encrypt-to
1 parent afd26cf commit 5f0c59d

File tree

3 files changed

+64
-0
lines changed

3 files changed

+64
-0
lines changed

decoder.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ func (dec *Decoder) DecodePage(hdr *PageHeader, data []byte) error {
279279
return err
280280
}
281281

282+
// Encrypted files must use the size-prefixed block format.
283+
if dec.encrypted && hdr.Flags&PageHeaderFlagSize == 0 {
284+
return fmt.Errorf("encrypted file contains page without size flag (pgno=%d)", hdr.Pgno)
285+
}
286+
282287
// Read page data using format-specific approach.
283288
if hdr.Flags&PageHeaderFlagSize != 0 {
284289
// New block format: read size prefix, then data.

decoder_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,56 @@ func TestDecoder_Encrypted(t *testing.T) {
431431
})
432432
}
433433

434+
func TestFileSpec_StripEncryptionWhenNoRecipients(t *testing.T) {
435+
pub, priv, _ := ltx.GenerateKeyPair()
436+
437+
pageData := make([]byte, 1024)
438+
pageData[0] = 0xAB
439+
440+
chksum := ltx.ChecksumFlag | ltx.ChecksumPage(1, pageData)
441+
442+
// Create encrypted file.
443+
var encBuf bytes.Buffer
444+
spec := ltx.FileSpec{
445+
Header: ltx.Header{
446+
Version: ltx.Version, PageSize: 1024, Commit: 1,
447+
MinTXID: 1, MaxTXID: 1, Timestamp: 1000,
448+
},
449+
Pages: []ltx.PageSpec{{Header: ltx.PageHeader{Pgno: 1}, Data: pageData}},
450+
Trailer: ltx.Trailer{PostApplyChecksum: chksum},
451+
452+
RecipientPublicKeys: [][]byte{pub},
453+
}
454+
if _, err := spec.WriteTo(&encBuf); err != nil {
455+
t.Fatal(err)
456+
}
457+
458+
// Read back with key.
459+
var spec2 ltx.FileSpec
460+
if _, err := spec2.ReadFromWithKey(bytes.NewReader(encBuf.Bytes()), priv); err != nil {
461+
t.Fatal(err)
462+
}
463+
464+
// Write without recipients — should produce a valid unencrypted file.
465+
spec2.RecipientPublicKeys = nil
466+
var plainBuf bytes.Buffer
467+
if _, err := spec2.WriteTo(&plainBuf); err != nil {
468+
t.Fatal(err)
469+
}
470+
471+
// Verify the output is readable without a key.
472+
var spec3 ltx.FileSpec
473+
if _, err := spec3.ReadFrom(bytes.NewReader(plainBuf.Bytes())); err != nil {
474+
t.Fatalf("expected unencrypted file to be readable without key: %v", err)
475+
}
476+
if spec3.Header.Encrypted() {
477+
t.Fatal("expected header to not have encrypted flag")
478+
}
479+
if !bytes.Equal(spec3.Pages[0].Data, pageData) {
480+
t.Fatal("page data mismatch")
481+
}
482+
}
483+
434484
func TestDecoder_64KBPageSize(t *testing.T) {
435485
const pageSize = 65536 // 64KB - maximum SQLite page size
436486

file_spec.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ func (s *FileSpec) WriteTo(dst io.Writer) (n int64, err error) {
2626
if err := enc.SetEncryption(s.RecipientPublicKeys); err != nil {
2727
return 0, fmt.Errorf("set encryption: %s", err)
2828
}
29+
} else {
30+
// Clear encryption header fields when no recipients are configured.
31+
// This prevents writing a corrupt file when a previously-encrypted
32+
// FileSpec is written without recipients (e.g. ltx rekey without -encrypt-to).
33+
s.Header.Flags &^= HeaderFlagEncryptedHPKE
34+
s.Header.RecipientCount = 0
35+
s.Header.KEMID = 0
36+
s.Header.KDFID = 0
37+
s.Header.AEADID = 0
2938
}
3039

3140
if err := enc.EncodeHeader(s.Header); err != nil {

0 commit comments

Comments
 (0)