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}