Skip to main content

sqlite_core/
sqlcipher.rs

1//! `SQLCipher` at-rest decryption → a plaintext `SQLite` byte stream the reader
2//! ([`crate::Database::open`]) consumes unchanged.
3//!
4//! # What `SQLCipher` does (and how we undo it)
5//!
6//! A `SQLCipher` database is an ordinary page-structured `SQLite` file whose every
7//! page is encrypted with **AES-256-CBC** and authenticated with a per-page
8//! **HMAC**. The first 16 bytes of the file are a random **salt** (in place of
9//! the `SQLite format 3\0` magic). Key material is derived with **`PBKDF2`**:
10//!
11//! - encryption key: `PBKDF2(passphrase, salt, kdf_iter, 32)` — or a raw 32-byte
12//!   key used directly (`PRAGMA key = "x'<64 hex>'"`);
13//! - HMAC key: `PBKDF2(encryption_key, salt ^ 0x3a, 2, 32)`.
14//!
15//! Each page's tail holds `[ IV(16) | HMAC | padding ]` occupying `reserve`
16//! bytes. The HMAC authenticates `ciphertext || IV || page_no_le32`. Page 1's
17//! first 16 bytes (the salt) are not encrypted; on decrypt we prepend the
18//! standard magic to reconstruct a valid plaintext page 1. The plaintext header
19//! carries `SQLCipher`'s own reserved-space byte, so the reader computes the
20//! correct usable size with no further help.
21//!
22//! # Version detection
23//!
24//! The two shipped profiles are the `SQLCipher` v4 and v3 defaults; they differ in
25//! `PBKDF2`/HMAC digest (SHA-512 vs SHA-1), iteration count, default page size, and
26//! reserve. Because nothing in the header is readable before decryption, the
27//! version is detected by **HMAC verification on page 1**: the first profile whose
28//! page-1 tag matches the derived key is the correct one. A wrong key/parameters
29//! matches no profile and fails loud ([`DecryptError::KeyOrParametersMismatch`]) —
30//! never a silent wrong-output.
31//!
32//! # Crypto provenance
33//!
34//! Every primitive is an audited `RustCrypto` crate (`pbkdf2`, `hmac`, `sha1`,
35//! `sha2`, `aes`, `cbc`). Nothing here is hand-rolled.
36
37use aes::Aes256;
38use cipher::block_padding::NoPadding;
39use cipher::{BlockDecryptMut, KeyIvInit};
40use hmac::{Hmac, Mac};
41use sha1::Sha1;
42use sha2::Sha512;
43
44/// Per-file random salt length, and the length of page 1's plaintext magic.
45const SALT_LEN: usize = 16;
46/// AES-CBC initialization-vector length (one block).
47const IV_LEN: usize = 16;
48/// AES-256 key length.
49const KEY_LEN: usize = 32;
50/// XOR mask applied to the salt to derive the HMAC-key salt (`SQLCipher`
51/// `HMAC_SALT_MASK`).
52const HMAC_SALT_MASK: u8 = 0x3a;
53/// `PBKDF2` iterations for the HMAC-key derivation (`SQLCipher` `FAST_PBKDF2`).
54const HMAC_KDF_ITER: u32 = 2;
55/// The 16-byte header every plaintext `SQLite` file begins with.
56const SQLITE_MAGIC: &[u8; SALT_LEN] = b"SQLite format 3\x00";
57
58type Aes256CbcDec = cbc::Decryptor<Aes256>;
59
60/// The key supplied by the caller.
61///
62/// Secure-by-design: the two shapes are distinct types, so a raw key can never be
63/// mistaken for a passphrase (which would silently `PBKDF2`-stretch 32 random bytes
64/// and fail to decrypt).
65#[derive(Clone)]
66pub enum SqlCipherKey {
67    /// A user passphrase (`PRAGMA key = 'passphrase'`); the encryption key is
68    /// `PBKDF2`-derived from it and the database's per-file salt.
69    Passphrase(Vec<u8>),
70    /// A raw 32-byte key (`PRAGMA key = "x'<64 hex>'"`), used directly as the
71    /// AES-256 key. The salt for HMAC-key derivation still comes from the file.
72    RawKey([u8; KEY_LEN]),
73}
74
75/// The `SQLCipher` default profile detected for a database.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum SqlCipherVersion {
78    /// `SQLCipher` 4 defaults: `PBKDF2`/HMAC-SHA512, 256 000 iterations, 4096-byte
79    /// pages, 80-byte reserve.
80    V4,
81    /// `SQLCipher` 3 defaults (or `cipher_compatibility = 3`): `PBKDF2`/HMAC-SHA1,
82    /// 64 000 iterations, 1024-byte pages, 48-byte reserve.
83    V3,
84}
85
86/// Why decryption could not proceed. Every variant is a loud, recoverable
87/// failure — decryption never panics and never emits plausible-but-wrong bytes.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum DecryptError {
90    /// The input is smaller than the 16-byte salt — not a `SQLCipher` file.
91    TooSmall,
92    /// No shipped profile's page-1 HMAC verified: the key is wrong, or the
93    /// database uses non-default cipher parameters this decryptor does not model.
94    KeyOrParametersMismatch,
95    /// A page past page 1 failed HMAC authentication after page 1 verified —
96    /// consistent with tampering or corruption of an otherwise-valid database.
97    /// Carries the 1-based page number (show-the-offending-value).
98    PageAuthFailed(u32),
99    /// The file holds more pages than a 32-bit page number can address.
100    TooLarge,
101}
102
103/// A decrypted database: the reconstructed plaintext bytes plus the profile that
104/// decrypted them.
105pub struct Decrypted {
106    /// A valid, standalone plaintext `SQLite` file — feed straight to
107    /// [`crate::Database::open`].
108    pub plaintext: Vec<u8>,
109    /// The `SQLCipher` profile that authenticated the pages.
110    pub version: SqlCipherVersion,
111    /// Logical page size in bytes.
112    pub page_size: u32,
113}
114
115/// The `PBKDF2`/HMAC digest a profile uses.
116#[derive(Clone, Copy)]
117enum Prf {
118    Sha1,
119    Sha512,
120}
121
122/// A fully-specified `SQLCipher` cipher configuration.
123struct Profile {
124    version: SqlCipherVersion,
125    page_size: usize,
126    kdf_iter: u32,
127    prf: Prf,
128    /// Bytes reserved at the end of each page for `IV || HMAC || padding`.
129    reserve: usize,
130    /// HMAC tag length (SHA-1 → 20, SHA-512 → 64).
131    hmac_len: usize,
132}
133
134/// The shipped default profiles, tried in order. v4 first (the modern default).
135const PROFILES: [Profile; 2] = [
136    Profile {
137        version: SqlCipherVersion::V4,
138        page_size: 4096,
139        kdf_iter: 256_000,
140        prf: Prf::Sha512,
141        reserve: 80,
142        hmac_len: 64,
143    },
144    Profile {
145        version: SqlCipherVersion::V3,
146        page_size: 1024,
147        kdf_iter: 64_000,
148        prf: Prf::Sha1,
149        reserve: 48,
150        hmac_len: 20,
151    },
152];
153
154/// `PBKDF2` into `out`, selecting the PRF digest. Infallible; `out` is any length.
155fn pbkdf2(prf: Prf, password: &[u8], salt: &[u8], rounds: u32, out: &mut [u8]) {
156    match prf {
157        Prf::Sha1 => pbkdf2::pbkdf2_hmac::<Sha1>(password, salt, rounds, out),
158        Prf::Sha512 => pbkdf2::pbkdf2_hmac::<Sha512>(password, salt, rounds, out),
159    }
160}
161
162/// Constant-time HMAC check of `data_a || data_b` against `tag`. Returns `false`
163/// (never panics) on any key/length issue.
164fn hmac_ok(prf: Prf, key: &[u8], data_a: &[u8], data_b: &[u8], tag: &[u8]) -> bool {
165    match prf {
166        Prf::Sha1 => {
167            let Ok(mut mac) = Hmac::<Sha1>::new_from_slice(key) else {
168                return false; // cov:unreachable: HMAC accepts any key length
169            };
170            mac.update(data_a);
171            mac.update(data_b);
172            mac.verify_slice(tag).is_ok()
173        }
174        Prf::Sha512 => {
175            let Ok(mut mac) = Hmac::<Sha512>::new_from_slice(key) else {
176                return false; // cov:unreachable: HMAC accepts any key length
177            };
178            mac.update(data_a);
179            mac.update(data_b);
180            mac.verify_slice(tag).is_ok()
181        }
182    }
183}
184
185/// The encryption key and HMAC key for one profile + supplied key + file salt.
186fn derive_keys(
187    profile: &Profile,
188    key: &SqlCipherKey,
189    salt: &[u8],
190) -> ([u8; KEY_LEN], [u8; KEY_LEN]) {
191    let mut enc = [0u8; KEY_LEN];
192    match key {
193        SqlCipherKey::Passphrase(pw) => pbkdf2(profile.prf, pw, salt, profile.kdf_iter, &mut enc),
194        SqlCipherKey::RawKey(k) => enc.copy_from_slice(k),
195    }
196    let mut hmac_salt = [0u8; SALT_LEN];
197    for (dst, &s) in hmac_salt.iter_mut().zip(salt.iter()) {
198        *dst = s ^ HMAC_SALT_MASK;
199    }
200    let mut hmac_key = [0u8; KEY_LEN];
201    pbkdf2(profile.prf, &enc, &hmac_salt, HMAC_KDF_ITER, &mut hmac_key);
202    (enc, hmac_key)
203}
204
205/// Byte spans within one on-disk page for a given profile and page number.
206/// `None` when the page is too short for its own reserve (crafted / truncated).
207struct PageLayout {
208    /// Where the encrypted region starts (16 on page 1 to skip the salt, else 0).
209    start: usize,
210    /// Where the IV starts (`page_size - reserve`).
211    iv_start: usize,
212}
213
214impl PageLayout {
215    fn for_page(profile: &Profile, pgno: u32) -> Option<Self> {
216        let iv_start = profile.page_size.checked_sub(profile.reserve)?;
217        let start = if pgno == 1 { SALT_LEN } else { 0 };
218        // Room for at least the ciphertext, the IV, and the HMAC tag.
219        if iv_start < start || iv_start.checked_add(IV_LEN + profile.hmac_len)? > profile.page_size
220        {
221            return None;
222        }
223        Some(Self { start, iv_start })
224    }
225}
226
227/// Verify one page's HMAC without decrypting it (used for version detection).
228fn page_hmac_ok(profile: &Profile, hmac_key: &[u8], page: &[u8], pgno: u32) -> bool {
229    let Some(layout) = PageLayout::for_page(profile, pgno) else {
230        return false;
231    };
232    let (Some(auth_region), Some(tag)) = (
233        page.get(layout.start..layout.iv_start + IV_LEN),
234        page.get(layout.iv_start + IV_LEN..layout.iv_start + IV_LEN + profile.hmac_len),
235    ) else {
236        return false; // cov:unreachable: PageLayout bounds already guarantee these
237    };
238    hmac_ok(profile.prf, hmac_key, auth_region, &pgno.to_le_bytes(), tag)
239}
240
241/// Authenticate and decrypt one page, returning the reconstructed plaintext page.
242/// `None` on any authentication or bounds failure (panic-free).
243fn decrypt_page(
244    profile: &Profile,
245    enc_key: &[u8; KEY_LEN],
246    hmac_key: &[u8],
247    page: &[u8],
248    pgno: u32,
249) -> Option<Vec<u8>> {
250    let layout = PageLayout::for_page(profile, pgno)?;
251    let iv = page.get(layout.iv_start..layout.iv_start + IV_LEN)?;
252    let ciphertext = page.get(layout.start..layout.iv_start)?;
253    let auth_region = page.get(layout.start..layout.iv_start + IV_LEN)?;
254    let tag = page.get(layout.iv_start + IV_LEN..layout.iv_start + IV_LEN + profile.hmac_len)?;
255    let tail = page.get(layout.iv_start..profile.page_size)?;
256
257    if !hmac_ok(profile.prf, hmac_key, auth_region, &pgno.to_le_bytes(), tag) {
258        return None;
259    }
260    if ciphertext.len() % IV_LEN != 0 {
261        return None; // cov:unreachable: a valid SQLCipher page is block-aligned
262    }
263
264    let dec = Aes256CbcDec::new_from_slices(enc_key, iv).ok()?;
265    let mut buf = ciphertext.to_vec();
266    let plain = dec.decrypt_padded_mut::<NoPadding>(&mut buf).ok()?;
267
268    let mut out = Vec::with_capacity(profile.page_size);
269    if pgno == 1 {
270        out.extend_from_slice(SQLITE_MAGIC);
271    }
272    out.extend_from_slice(plain);
273    out.extend_from_slice(tail);
274    Some(out)
275}
276
277/// Decrypt every page under an already-selected profile.
278fn decrypt_all(
279    profile: &Profile,
280    enc_key: &[u8; KEY_LEN],
281    hmac_key: &[u8],
282    ciphertext: &[u8],
283) -> Result<Decrypted, DecryptError> {
284    let page_count = ciphertext.len() / profile.page_size;
285    let mut out = Vec::with_capacity(page_count * profile.page_size);
286    for i in 0..page_count {
287        let pgno = u32::try_from(i + 1).map_err(|_| DecryptError::TooLarge)?;
288        let start = i * profile.page_size;
289        let end = start + profile.page_size;
290        let page = ciphertext
291            .get(start..end)
292            .ok_or(DecryptError::PageAuthFailed(pgno))?;
293        let plain = decrypt_page(profile, enc_key, hmac_key, page, pgno)
294            .ok_or(DecryptError::PageAuthFailed(pgno))?;
295        out.extend_from_slice(&plain);
296    }
297    Ok(Decrypted {
298        plaintext: out,
299        version: profile.version,
300        page_size: u32::try_from(profile.page_size).unwrap_or(u32::MAX),
301    })
302}
303
304/// Decrypt a `SQLCipher` database into a plaintext `SQLite` byte stream, detecting
305/// the cipher version by page-1 HMAC verification.
306///
307/// Returns [`DecryptError::KeyOrParametersMismatch`] if the key is wrong or the
308/// database uses cipher parameters outside the shipped v4/v3 defaults — a loud
309/// failure, never a silent wrong plaintext.
310pub fn decrypt(ciphertext: &[u8], key: &SqlCipherKey) -> Result<Decrypted, DecryptError> {
311    if ciphertext.len() < SALT_LEN {
312        return Err(DecryptError::TooSmall);
313    }
314    let salt = &ciphertext[..SALT_LEN];
315    for profile in &PROFILES {
316        if ciphertext.len() < profile.page_size || ciphertext.len() % profile.page_size != 0 {
317            continue;
318        }
319        let (enc_key, hmac_key) = derive_keys(profile, key, salt);
320        let Some(page1) = ciphertext.get(..profile.page_size) else {
321            continue; // cov:unreachable: length checked above
322        };
323        if page_hmac_ok(profile, &hmac_key, page1, 1) {
324            return decrypt_all(profile, &enc_key, &hmac_key, ciphertext);
325        }
326    }
327    Err(DecryptError::KeyOrParametersMismatch)
328}