mirror of
https://github.com/hanzoai/sqlcipher.git
synced 2026-08-06 17:32:17 +00:00
Golden vectors pin the KDF, the page-authentication key derivation and the
exact on-disk bytes of a page under a fixed IV, so a refactor cannot silently
change the format.
The vectors alone would only pin this port to itself, so both are
cross-verified against libsqlcipher 4.5.6 by decrypting databases the C
library actually wrote:
testdata/c-4.5.6.db raw key (x'HEX'), 7 pages, overflow +
index + random and zero blobs
testdata/c-4.5.6-passphrase.db passphrase, proving PBKDF2-HMAC-SHA512 at
256000 iterations agrees byte for byte
Fail-closed is asserted, not assumed: wrong key, wrong salt, wrong
passphrase, a flipped ciphertext/IV/tag bit, and a page replayed at another
page number all return ErrKey with no data.
Note the reserve trailer carries the IV and is fresh on every write, so a
round-trip is the identity on the usable bytes, not on the trailer.
210 lines
7.8 KiB
Go
210 lines
7.8 KiB
Go
// Package sqlcipher implements the SQLCipher 4 on-disk page format in pure Go.
|
|
//
|
|
// It is a port of the SQLCipher codec (https://github.com/sqlcipher/sqlcipher,
|
|
// v4.5.6, BSD-3, Copyright (c) ZETETIC LLC) — see NOTICE. It knows nothing about
|
|
// database/sql or SQLite drivers: it is the format, and only the format. Anything
|
|
// that must read or write SQLCipher files — a driver, a backup or replication
|
|
// path, a migration or a forensic tool — uses this package directly.
|
|
//
|
|
// # The format
|
|
//
|
|
// A SQLCipher database is a SQLite database whose pages are individually
|
|
// encrypted, with per-page reserve bytes carrying the IV and authentication tag:
|
|
//
|
|
// page N on disk: [ ciphertext | IV (16) | HMAC-SHA512 (64) ]
|
|
// page 1 on disk: [ salt (16) | ciphertext | IV (16) | HMAC-SHA512 (64) ]
|
|
//
|
|
// Pages are independent: no cross-page state, no chaining. The salt is the first
|
|
// SaltSize bytes of page 1 and is stored in the clear — it is the only part of
|
|
// the file that is not ciphertext. Because page 1's first SaltSize bytes hold the
|
|
// salt instead of SQLite's "SQLite format 3\x00" magic, page 1 encrypts only the
|
|
// bytes after that offset; Decrypt restores the magic, which is what SQLite's
|
|
// pager expects to see.
|
|
//
|
|
// The ciphertext is AES-256-CBC with no padding, so its length is always a
|
|
// multiple of the AES block size. The IV is fresh on every page write. The HMAC
|
|
// covers ciphertext || IV || page number (little-endian uint32), which
|
|
// authenticates the page contents, the IV, and the page's position in the file —
|
|
// so pages cannot be reordered, and a modified IV is detected.
|
|
//
|
|
// # Keying
|
|
//
|
|
// A Key is either a raw 32-byte key (no KDF — SQLCipher's x'HEX' form) or a
|
|
// passphrase (PBKDF2-HMAC-SHA512, 256000 iterations by default). Either way the
|
|
// page-authentication key is a second, distinct key derived from the page
|
|
// encryption key using the salt masked with 0x3a and 2 PBKDF2 iterations.
|
|
//
|
|
// # Failing closed
|
|
//
|
|
// Decrypt authenticates before it decrypts and returns ErrKey on any HMAC
|
|
// mismatch. A wrong key errors; it never returns garbage plaintext.
|
|
package sqlcipher
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/pbkdf2"
|
|
"crypto/sha512"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// Format sizes, all fixed by SQLCipher 4.
|
|
const (
|
|
SaltSize = 16 // KDF salt: the first bytes of page 1, stored in the clear
|
|
KeySize = 32 // AES-256
|
|
IVSize = 16 // AES block size
|
|
HMACSize = 64 // SHA-512 digest
|
|
|
|
// Reserve is the per-page trailer SQLite must leave free at the end of every
|
|
// page: IV || HMAC, rounded up to a multiple of the AES block size (it is
|
|
// already a multiple, so 16+64=80). SQLite records it in byte 20 of the
|
|
// database header, so a database carries its own reserve size.
|
|
Reserve = IVSize + HMACSize
|
|
|
|
// DefaultPageSize and DefaultIter are SQLCipher 4's defaults.
|
|
DefaultPageSize = 4096
|
|
DefaultIter = 256000
|
|
|
|
blockSize = 16 // AES
|
|
fastIter = 2 // PBKDF2 iterations for the page-authentication key
|
|
saltMask = 0x3a // masks the salt so the two keys derive from distinct salts
|
|
)
|
|
|
|
// magic is the SQLite file header. It occupies page 1's first SaltSize bytes in
|
|
// a plaintext database, where an encrypted database keeps the salt instead.
|
|
var magic = []byte("SQLite format 3\x00")
|
|
|
|
// ErrKey reports that a page did not authenticate: the key is wrong, or the page
|
|
// was corrupted or tampered with. The two are deliberately indistinguishable.
|
|
var ErrKey = errors.New("sqlcipher: wrong key or corrupted page")
|
|
|
|
// Params are the format parameters that vary between databases. The zero value
|
|
// means SQLCipher 4 defaults; set a field only to interoperate with a database
|
|
// that was written with a non-default PRAGMA.
|
|
type Params struct {
|
|
PageSize int // cipher_page_size; 0 means DefaultPageSize
|
|
Iter int // kdf_iter, passphrase keying only; 0 means DefaultIter
|
|
}
|
|
|
|
func (p Params) pageSize() int {
|
|
if p.PageSize == 0 {
|
|
return DefaultPageSize
|
|
}
|
|
return p.PageSize
|
|
}
|
|
|
|
func (p Params) iter() int {
|
|
if p.Iter == 0 {
|
|
return DefaultIter
|
|
}
|
|
return p.Iter
|
|
}
|
|
|
|
// Key is database key material: a raw key or a passphrase. It is inert until
|
|
// bound to a database salt by NewCodec.
|
|
type Key struct {
|
|
raw []byte
|
|
pass string
|
|
}
|
|
|
|
// RawKey uses key directly as the page encryption key, skipping the passphrase
|
|
// KDF. key must be KeySize bytes. This is SQLCipher's x'HEX' keying form and the
|
|
// one Hanzo uses: keys come from KMS already uniformly random, so a KDF over them
|
|
// would buy nothing.
|
|
func RawKey(key []byte) Key { return Key{raw: key} }
|
|
|
|
// Passphrase derives the page encryption key from pass via PBKDF2-HMAC-SHA512.
|
|
func Passphrase(pass string) Key { return Key{pass: pass} }
|
|
|
|
// Codec encrypts and decrypts the pages of one database. It is immutable and safe
|
|
// for concurrent use.
|
|
type Codec struct {
|
|
pageSize int
|
|
salt []byte // SaltSize bytes, page 1's plaintext prefix
|
|
key []byte // page encryption key
|
|
mac []byte // page authentication key
|
|
rand io.Reader
|
|
}
|
|
|
|
// NewCodec binds key material to a database salt. salt is SaltSize bytes, read
|
|
// from the first bytes of page 1 of an existing database (see FileSalt), or freshly
|
|
// random for a new one.
|
|
//
|
|
// Deriving from a passphrase runs Params.Iter PBKDF2 iterations and is
|
|
// deliberately slow; do it once per database, not once per page.
|
|
func NewCodec(k Key, salt []byte, p Params) (*Codec, error) {
|
|
if len(salt) != SaltSize {
|
|
return nil, fmt.Errorf("sqlcipher: salt must be %d bytes, got %d", SaltSize, len(salt))
|
|
}
|
|
if n := p.pageSize(); n < 512 || n > 65536 || n&(n-1) != 0 {
|
|
return nil, fmt.Errorf("sqlcipher: page size %d is not a power of two in [512,65536]", n)
|
|
}
|
|
|
|
var key []byte
|
|
switch {
|
|
case k.raw != nil:
|
|
if len(k.raw) != KeySize {
|
|
return nil, fmt.Errorf("sqlcipher: raw key must be %d bytes, got %d", KeySize, len(k.raw))
|
|
}
|
|
key = k.raw
|
|
case k.pass != "":
|
|
var err error
|
|
if key, err = pbkdf2.Key(sha512.New, k.pass, salt, p.iter(), KeySize); err != nil {
|
|
return nil, fmt.Errorf("sqlcipher: derive key: %w", err)
|
|
}
|
|
default:
|
|
return nil, errors.New("sqlcipher: no key material")
|
|
}
|
|
|
|
// The page-authentication key derives from the page encryption key over the
|
|
// masked salt, so the two keys are distinct but both reproducible from the
|
|
// passphrase and the file.
|
|
masked := make([]byte, SaltSize)
|
|
for i, b := range salt {
|
|
masked[i] = b ^ saltMask
|
|
}
|
|
mac, err := pbkdf2.Key(sha512.New, string(key), masked, fastIter, KeySize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sqlcipher: derive page-authentication key: %w", err)
|
|
}
|
|
|
|
return &Codec{pageSize: p.pageSize(), salt: bytes.Clone(salt), key: bytes.Clone(key), mac: mac}, nil
|
|
}
|
|
|
|
// PageSize is the on-disk size of every page, including the reserve trailer.
|
|
func (c *Codec) PageSize() int { return c.pageSize }
|
|
|
|
// Salt returns the database salt.
|
|
func (c *Codec) Salt() []byte { return bytes.Clone(c.salt) }
|
|
|
|
// FileSalt reads the salt from page 1 of an encrypted database. page1 need only
|
|
// be the first SaltSize bytes of the file.
|
|
func FileSalt(page1 []byte) ([]byte, error) {
|
|
if len(page1) < SaltSize {
|
|
return nil, fmt.Errorf("sqlcipher: need %d bytes to read the salt, got %d", SaltSize, len(page1))
|
|
}
|
|
return bytes.Clone(page1[:SaltSize]), nil
|
|
}
|
|
|
|
// offset is the number of leading bytes page pgno does not encrypt: page 1 keeps
|
|
// the salt in the clear, every other page encrypts from byte 0.
|
|
func offset(pgno uint32) int {
|
|
if pgno == 1 {
|
|
return SaltSize
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// pageMAC authenticates ciphertext||IV for page pgno. Binding the page number
|
|
// stops a valid page from being replayed at another position in the file; it is
|
|
// little-endian to match SQLCipher's default (CIPHER_FLAG_LE_PGNO), which is
|
|
// a fixed byte order rather than the host's so files stay portable.
|
|
func (c *Codec) pageMAC(pgno uint32, data []byte) []byte {
|
|
m := hmac.New(sha512.New, c.mac)
|
|
m.Write(data)
|
|
m.Write([]byte{byte(pgno), byte(pgno >> 8), byte(pgno >> 16), byte(pgno >> 24)})
|
|
return m.Sum(nil)
|
|
}
|