Files
hanzo-dev 6c0e1a409f feat: EmptyPlaintext mints the reserve-80 seed for a new encrypted database
EncryptFile requires its source to reserve Reserve (80) bytes per page, but a
plaintext SQLite engine cannot be told to create a database with a non-zero
reserve through SQL (the reserve is SQLITE_FCNTL_RESERVE_BYTES, not a PRAGMA).
So a pure-Go writer that wants a fresh encrypted database had nowhere to start.

EmptyPlaintext returns a minimal empty SQLite database (one leaf page, empty
sqlite_master) with byte 20 = Reserve, per the SQLite file format spec. A driver
opens it, writes its schema (SQLite honours the header reserve, so every page it
writes leaves room for the IV+HMAC trailer), then EncryptFile turns the result
into a SQLCipher database.

- empty.go: EmptyPlaintext(Params) — validates page size (power of two, usable
  >= SQLite's 480-byte minimum, so >= 1024 with an 80-byte reserve; 65536 encoded
  as the value 1 per the spec).
- empty_test.go (zero-dep): structural layout + EncryptFile/DecryptFile round trip.
- parity: TestParityNewDatabaseFromSeed — mint the seed, modernc writes a table,
  this port encrypts, and the C SQLCipher library reads back what Go wrote.
2026-07-23 09:55:11 -07:00

80 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sqlcipher
import (
"encoding/binary"
"fmt"
)
// EmptyPlaintext returns the bytes of a minimal empty SQLite database — one page,
// an empty sqlite_master, no tables — that reserves Reserve (80) bytes per page.
//
// It is the plaintext seed a NEW encrypted database is built from. EncryptFile
// requires its source to already reserve Reserve bytes per page (header byte 20),
// because the per-page IV+HMAC trailer is written into that reserve; a database
// that does not reserve it cannot be encrypted without losing data. A plaintext
// SQLite engine cannot be asked to create a database with a non-zero reserve
// through SQL (the reserve is set via SQLITE_FCNTL_RESERVE_BYTES, not a PRAGMA),
// so a pure-Go writer that wants a fresh encrypted database starts from this seed:
// open it, write the schema (SQLite honours the reserve recorded in the header, so
// every page it writes leaves room for the trailer), then EncryptFile the result.
//
// The layout follows the SQLite file format
// (https://www.sqlite.org/fileformat.html §1.3 "Database Header", §1.6 "B-tree
// Pages"): a single leaf table b-tree page holding an empty sqlite_master, with
// byte 20 set to Reserve and the cell-content-area pointer at the usable-size
// boundary (pageSize Reserve). The in-header page count is 1 and the file is
// exactly one page, so an engine trusts the header size (the change counter equals
// the version-valid-for field). p.PageSize selects the page size (0 ⇒
// DefaultPageSize); p.Iter is unused here.
func EmptyPlaintext(p Params) ([]byte, error) {
ps := p.pageSize()
if ps < 512 || ps > 65536 || ps&(ps-1) != 0 {
return nil, fmt.Errorf("sqlcipher: page size %d is not a power of two in [512,65536]", ps)
}
if ps-Reserve < 480 {
// SQLite requires the usable size (pageSize reserve) to be at least 480
// bytes, so a 512-byte page cannot carry an 80-byte reserve (432 usable):
// the smallest page size that can be a SQLCipher database is 1024.
return nil, fmt.Errorf("sqlcipher: page size %d with reserve %d leaves %d usable, below SQLite's 480-byte minimum", ps, Reserve, ps-Reserve)
}
page := make([]byte, ps)
// ── database header (bytes 0..99) ──
copy(page[0:16], magic) // "SQLite format 3\0"
// Page size is a big-endian uint16, EXCEPT 65536 is stored as the value 1
// (it does not fit in two bytes) — SQLite file format §1.3.
if ps == 65536 {
binary.BigEndian.PutUint16(page[16:18], 1)
} else {
binary.BigEndian.PutUint16(page[16:18], uint16(ps))
}
page[18] = 1 // file format write version (rollback journal)
page[19] = 1 // file format read version
page[20] = Reserve // reserved bytes per page — the load-bearing field
page[21] = 64 // max embedded payload fraction (fixed)
page[22] = 32 // min embedded payload fraction (fixed)
page[23] = 32 // leaf payload fraction (fixed)
binary.BigEndian.PutUint32(page[24:28], 1) // file change counter
binary.BigEndian.PutUint32(page[28:32], 1) // database size in pages
// bytes 32..43: freelist trunk + count = 0; schema cookie (40..43) = 0
binary.BigEndian.PutUint32(page[44:48], 4) // schema format number (4 = current)
// bytes 48..55: default page cache = 0; largest-root-page (autovacuum) = 0
binary.BigEndian.PutUint32(page[56:60], 1) // text encoding = 1 (UTF-8)
// bytes 60..91: user version, incremental-vacuum, application id, reserved = 0
binary.BigEndian.PutUint32(page[92:96], 1) // version-valid-for = change counter ⇒ trust header size
binary.BigEndian.PutUint32(page[96:100], 3046000) // SQLITE_VERSION_NUMBER (informational)
// ── page-1 b-tree page header (begins at byte 100, after the db header) ──
usable := ps - Reserve
page[100] = 0x0D // leaf table b-tree page
binary.BigEndian.PutUint16(page[101:103], 0) // first freeblock (none)
binary.BigEndian.PutUint16(page[103:105], 0) // number of cells
binary.BigEndian.PutUint16(page[105:107], uint16(usable)) // cell content area start = usable size
page[107] = 0 // fragmented free bytes
// no cell pointers, no cells; the rest of the usable region is free space and
// the final Reserve bytes are the (unused, plaintext) reserve.
return page, nil
}