diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..03ca17b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Byte-exact forensic fixtures: git must never translate line endings in them. +# core.autocrlf defaults to TRUE on GitHub windows-latest runners, and git only +# sniffs the first 8000 bytes for a NUL before deciding a file is text. +tests/data/** -text +fuzz/corpus/** -text diff --git a/.gitignore b/.gitignore index a97bfc9..eba3ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +target/ # mkdocs build output (generated by `mkdocs build`; the Pages workflow builds it in CI) /site/ diff --git a/Cargo.lock b/Cargo.lock index 5d3db81..04a2d24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -192,6 +201,15 @@ dependencies = [ "zip 7.2.0", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.64" @@ -588,6 +606,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding", "generic-array", ] @@ -959,7 +978,14 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" name = "sqlite-core" version = "0.10.3" dependencies = [ + "aes", + "cbc", + "cipher", "forensicnomicon", + "hmac", + "pbkdf2", + "sha1", + "sha2", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml index 8230529..f4bf020 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,6 +17,17 @@ path = "src/lib.rs" [dependencies] # Consume KNOWLEDGE-layer format constants instead of re-hardcoding them. forensicnomicon = { workspace = true } +# SQLCipher decryption primitives — audited RustCrypto crates ONLY, never +# hand-rolled (CLAUDE.core.md: "Never hand-roll a cryptographic primitive"). +# PBKDF2 key derivation (HMAC-SHA1/SHA512), AES-256-CBC page decrypt, per-page +# HMAC authentication. All are low-MSRV and keep the reader on rust-version 1.80. +pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } +hmac = "0.12" +sha1 = "0.10" +sha2 = "0.10" +aes = "0.8" +cbc = "0.1" +cipher = "0.4" [lints] workspace = true diff --git a/core/src/lib.rs b/core/src/lib.rs index 7e7f3fb..22b9fea 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -24,6 +24,7 @@ pub mod attribution; pub mod rebuild; pub mod row_history; +pub mod sqlcipher; // The page-1 header field offsets are consumed from the KNOWLEDGE leaf // (forensicnomicon::sqlite ≥ 1.5.0); the previously-local duplicates were promoted @@ -70,6 +71,10 @@ pub enum Error { /// [`Database::open_path`], not a malformed database). Carries the /// [`std::io::ErrorKind`] (show-the-unrecognized-value). Io(std::io::ErrorKind), + /// `SQLCipher` decryption failed (wrong key, unsupported cipher parameters, or + /// a failed page authentication) via [`Database::open_encrypted`]. Carries + /// the underlying [`sqlcipher::DecryptError`] (show-the-unrecognized-value). + Decrypt(sqlcipher::DecryptError), } impl From for Error { @@ -78,6 +83,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: sqlcipher::DecryptError) -> Self { + Error::Decrypt(e) + } +} + /// A freed overflow-page chain could not be followed to a complete, trustworthy /// payload (task #73): a chain page that is not a freelist leaf (live / trunk / /// unreachable), a cycle, a premature terminator with bytes still owed, an @@ -657,6 +668,20 @@ impl Database { }) } + /// Decrypt a **`SQLCipher`** database with `key` and open the resulting + /// plaintext, detecting the cipher version automatically (see + /// [`sqlcipher::decrypt`]). The reader then consumes the decrypted byte + /// stream exactly as for a plaintext file — the encryption is transparent + /// past this call. + /// + /// Secure-by-default and read-only: a wrong key or unsupported cipher + /// parameters is a loud [`Error::Decrypt`], never a silently-misread + /// database; nothing is written back to the evidence file. + pub fn open_encrypted(bytes: &[u8], key: &sqlcipher::SqlCipherKey) -> Result { + let decrypted = sqlcipher::decrypt(bytes, key)?; + Self::open(decrypted.plaintext) + } + /// Open a database from a filesystem path with a **bounded-memory paged /// read** (roadmap §3.1): pages are streamed on demand through a small LRU /// cache instead of loading the whole file into a `Vec`, so a multi-GB diff --git a/core/src/sqlcipher.rs b/core/src/sqlcipher.rs new file mode 100644 index 0000000..ec3b4a9 --- /dev/null +++ b/core/src/sqlcipher.rs @@ -0,0 +1,328 @@ +//! `SQLCipher` at-rest decryption → a plaintext `SQLite` byte stream the reader +//! ([`crate::Database::open`]) consumes unchanged. +//! +//! # What `SQLCipher` does (and how we undo it) +//! +//! A `SQLCipher` database is an ordinary page-structured `SQLite` file whose every +//! page is encrypted with **AES-256-CBC** and authenticated with a per-page +//! **HMAC**. The first 16 bytes of the file are a random **salt** (in place of +//! the `SQLite format 3\0` magic). Key material is derived with **`PBKDF2`**: +//! +//! - encryption key: `PBKDF2(passphrase, salt, kdf_iter, 32)` — or a raw 32-byte +//! key used directly (`PRAGMA key = "x'<64 hex>'"`); +//! - HMAC key: `PBKDF2(encryption_key, salt ^ 0x3a, 2, 32)`. +//! +//! Each page's tail holds `[ IV(16) | HMAC | padding ]` occupying `reserve` +//! bytes. The HMAC authenticates `ciphertext || IV || page_no_le32`. Page 1's +//! first 16 bytes (the salt) are not encrypted; on decrypt we prepend the +//! standard magic to reconstruct a valid plaintext page 1. The plaintext header +//! carries `SQLCipher`'s own reserved-space byte, so the reader computes the +//! correct usable size with no further help. +//! +//! # Version detection +//! +//! The two shipped profiles are the `SQLCipher` v4 and v3 defaults; they differ in +//! `PBKDF2`/HMAC digest (SHA-512 vs SHA-1), iteration count, default page size, and +//! reserve. Because nothing in the header is readable before decryption, the +//! version is detected by **HMAC verification on page 1**: the first profile whose +//! page-1 tag matches the derived key is the correct one. A wrong key/parameters +//! matches no profile and fails loud ([`DecryptError::KeyOrParametersMismatch`]) — +//! never a silent wrong-output. +//! +//! # Crypto provenance +//! +//! Every primitive is an audited `RustCrypto` crate (`pbkdf2`, `hmac`, `sha1`, +//! `sha2`, `aes`, `cbc`). Nothing here is hand-rolled. + +use aes::Aes256; +use cipher::block_padding::NoPadding; +use cipher::{BlockDecryptMut, KeyIvInit}; +use hmac::{Hmac, Mac}; +use sha1::Sha1; +use sha2::Sha512; + +/// Per-file random salt length, and the length of page 1's plaintext magic. +const SALT_LEN: usize = 16; +/// AES-CBC initialization-vector length (one block). +const IV_LEN: usize = 16; +/// AES-256 key length. +const KEY_LEN: usize = 32; +/// XOR mask applied to the salt to derive the HMAC-key salt (`SQLCipher` +/// `HMAC_SALT_MASK`). +const HMAC_SALT_MASK: u8 = 0x3a; +/// `PBKDF2` iterations for the HMAC-key derivation (`SQLCipher` `FAST_PBKDF2`). +const HMAC_KDF_ITER: u32 = 2; +/// The 16-byte header every plaintext `SQLite` file begins with. +const SQLITE_MAGIC: &[u8; SALT_LEN] = b"SQLite format 3\x00"; + +type Aes256CbcDec = cbc::Decryptor; + +/// The key supplied by the caller. +/// +/// Secure-by-design: the two shapes are distinct types, so a raw key can never be +/// mistaken for a passphrase (which would silently `PBKDF2`-stretch 32 random bytes +/// and fail to decrypt). +#[derive(Clone)] +pub enum SqlCipherKey { + /// A user passphrase (`PRAGMA key = 'passphrase'`); the encryption key is + /// `PBKDF2`-derived from it and the database's per-file salt. + Passphrase(Vec), + /// A raw 32-byte key (`PRAGMA key = "x'<64 hex>'"`), used directly as the + /// AES-256 key. The salt for HMAC-key derivation still comes from the file. + RawKey([u8; KEY_LEN]), +} + +/// The `SQLCipher` default profile detected for a database. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SqlCipherVersion { + /// `SQLCipher` 4 defaults: `PBKDF2`/HMAC-SHA512, 256 000 iterations, 4096-byte + /// pages, 80-byte reserve. + V4, + /// `SQLCipher` 3 defaults (or `cipher_compatibility = 3`): `PBKDF2`/HMAC-SHA1, + /// 64 000 iterations, 1024-byte pages, 48-byte reserve. + V3, +} + +/// Why decryption could not proceed. Every variant is a loud, recoverable +/// failure — decryption never panics and never emits plausible-but-wrong bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DecryptError { + /// The input is smaller than the 16-byte salt — not a `SQLCipher` file. + TooSmall, + /// No shipped profile's page-1 HMAC verified: the key is wrong, or the + /// database uses non-default cipher parameters this decryptor does not model. + KeyOrParametersMismatch, + /// A page past page 1 failed HMAC authentication after page 1 verified — + /// consistent with tampering or corruption of an otherwise-valid database. + /// Carries the 1-based page number (show-the-offending-value). + PageAuthFailed(u32), + /// The file holds more pages than a 32-bit page number can address. + TooLarge, +} + +/// A decrypted database: the reconstructed plaintext bytes plus the profile that +/// decrypted them. +pub struct Decrypted { + /// A valid, standalone plaintext `SQLite` file — feed straight to + /// [`crate::Database::open`]. + pub plaintext: Vec, + /// The `SQLCipher` profile that authenticated the pages. + pub version: SqlCipherVersion, + /// Logical page size in bytes. + pub page_size: u32, +} + +/// The `PBKDF2`/HMAC digest a profile uses. +#[derive(Clone, Copy)] +enum Prf { + Sha1, + Sha512, +} + +/// A fully-specified `SQLCipher` cipher configuration. +struct Profile { + version: SqlCipherVersion, + page_size: usize, + kdf_iter: u32, + prf: Prf, + /// Bytes reserved at the end of each page for `IV || HMAC || padding`. + reserve: usize, + /// HMAC tag length (SHA-1 → 20, SHA-512 → 64). + hmac_len: usize, +} + +/// The shipped default profiles, tried in order. v4 first (the modern default). +const PROFILES: [Profile; 2] = [ + Profile { + version: SqlCipherVersion::V4, + page_size: 4096, + kdf_iter: 256_000, + prf: Prf::Sha512, + reserve: 80, + hmac_len: 64, + }, + Profile { + version: SqlCipherVersion::V3, + page_size: 1024, + kdf_iter: 64_000, + prf: Prf::Sha1, + reserve: 48, + hmac_len: 20, + }, +]; + +/// `PBKDF2` into `out`, selecting the PRF digest. Infallible; `out` is any length. +fn pbkdf2(prf: Prf, password: &[u8], salt: &[u8], rounds: u32, out: &mut [u8]) { + match prf { + Prf::Sha1 => pbkdf2::pbkdf2_hmac::(password, salt, rounds, out), + Prf::Sha512 => pbkdf2::pbkdf2_hmac::(password, salt, rounds, out), + } +} + +/// Constant-time HMAC check of `data_a || data_b` against `tag`. Returns `false` +/// (never panics) on any key/length issue. +fn hmac_ok(prf: Prf, key: &[u8], data_a: &[u8], data_b: &[u8], tag: &[u8]) -> bool { + match prf { + Prf::Sha1 => { + let Ok(mut mac) = Hmac::::new_from_slice(key) else { + return false; // cov:unreachable: HMAC accepts any key length + }; + mac.update(data_a); + mac.update(data_b); + mac.verify_slice(tag).is_ok() + } + Prf::Sha512 => { + let Ok(mut mac) = Hmac::::new_from_slice(key) else { + return false; // cov:unreachable: HMAC accepts any key length + }; + mac.update(data_a); + mac.update(data_b); + mac.verify_slice(tag).is_ok() + } + } +} + +/// The encryption key and HMAC key for one profile + supplied key + file salt. +fn derive_keys( + profile: &Profile, + key: &SqlCipherKey, + salt: &[u8], +) -> ([u8; KEY_LEN], [u8; KEY_LEN]) { + let mut enc = [0u8; KEY_LEN]; + match key { + SqlCipherKey::Passphrase(pw) => pbkdf2(profile.prf, pw, salt, profile.kdf_iter, &mut enc), + SqlCipherKey::RawKey(k) => enc.copy_from_slice(k), + } + let mut hmac_salt = [0u8; SALT_LEN]; + for (dst, &s) in hmac_salt.iter_mut().zip(salt.iter()) { + *dst = s ^ HMAC_SALT_MASK; + } + let mut hmac_key = [0u8; KEY_LEN]; + pbkdf2(profile.prf, &enc, &hmac_salt, HMAC_KDF_ITER, &mut hmac_key); + (enc, hmac_key) +} + +/// Byte spans within one on-disk page for a given profile and page number. +/// `None` when the page is too short for its own reserve (crafted / truncated). +struct PageLayout { + /// Where the encrypted region starts (16 on page 1 to skip the salt, else 0). + start: usize, + /// Where the IV starts (`page_size - reserve`). + iv_start: usize, +} + +impl PageLayout { + fn for_page(profile: &Profile, pgno: u32) -> Option { + let iv_start = profile.page_size.checked_sub(profile.reserve)?; + let start = if pgno == 1 { SALT_LEN } else { 0 }; + // Room for at least the ciphertext, the IV, and the HMAC tag. + if iv_start < start || iv_start.checked_add(IV_LEN + profile.hmac_len)? > profile.page_size + { + return None; + } + Some(Self { start, iv_start }) + } +} + +/// Verify one page's HMAC without decrypting it (used for version detection). +fn page_hmac_ok(profile: &Profile, hmac_key: &[u8], page: &[u8], pgno: u32) -> bool { + let Some(layout) = PageLayout::for_page(profile, pgno) else { + return false; + }; + let (Some(auth_region), Some(tag)) = ( + page.get(layout.start..layout.iv_start + IV_LEN), + page.get(layout.iv_start + IV_LEN..layout.iv_start + IV_LEN + profile.hmac_len), + ) else { + return false; // cov:unreachable: PageLayout bounds already guarantee these + }; + hmac_ok(profile.prf, hmac_key, auth_region, &pgno.to_le_bytes(), tag) +} + +/// Authenticate and decrypt one page, returning the reconstructed plaintext page. +/// `None` on any authentication or bounds failure (panic-free). +fn decrypt_page( + profile: &Profile, + enc_key: &[u8; KEY_LEN], + hmac_key: &[u8], + page: &[u8], + pgno: u32, +) -> Option> { + let layout = PageLayout::for_page(profile, pgno)?; + let iv = page.get(layout.iv_start..layout.iv_start + IV_LEN)?; + let ciphertext = page.get(layout.start..layout.iv_start)?; + let auth_region = page.get(layout.start..layout.iv_start + IV_LEN)?; + let tag = page.get(layout.iv_start + IV_LEN..layout.iv_start + IV_LEN + profile.hmac_len)?; + let tail = page.get(layout.iv_start..profile.page_size)?; + + if !hmac_ok(profile.prf, hmac_key, auth_region, &pgno.to_le_bytes(), tag) { + return None; + } + if ciphertext.len() % IV_LEN != 0 { + return None; // cov:unreachable: a valid SQLCipher page is block-aligned + } + + let dec = Aes256CbcDec::new_from_slices(enc_key, iv).ok()?; + let mut buf = ciphertext.to_vec(); + let plain = dec.decrypt_padded_mut::(&mut buf).ok()?; + + let mut out = Vec::with_capacity(profile.page_size); + if pgno == 1 { + out.extend_from_slice(SQLITE_MAGIC); + } + out.extend_from_slice(plain); + out.extend_from_slice(tail); + Some(out) +} + +/// Decrypt every page under an already-selected profile. +fn decrypt_all( + profile: &Profile, + enc_key: &[u8; KEY_LEN], + hmac_key: &[u8], + ciphertext: &[u8], +) -> Result { + let page_count = ciphertext.len() / profile.page_size; + let mut out = Vec::with_capacity(page_count * profile.page_size); + for i in 0..page_count { + let pgno = u32::try_from(i + 1).map_err(|_| DecryptError::TooLarge)?; + let start = i * profile.page_size; + let end = start + profile.page_size; + let page = ciphertext + .get(start..end) + .ok_or(DecryptError::PageAuthFailed(pgno))?; + let plain = decrypt_page(profile, enc_key, hmac_key, page, pgno) + .ok_or(DecryptError::PageAuthFailed(pgno))?; + out.extend_from_slice(&plain); + } + Ok(Decrypted { + plaintext: out, + version: profile.version, + page_size: u32::try_from(profile.page_size).unwrap_or(u32::MAX), + }) +} + +/// Decrypt a `SQLCipher` database into a plaintext `SQLite` byte stream, detecting +/// the cipher version by page-1 HMAC verification. +/// +/// Returns [`DecryptError::KeyOrParametersMismatch`] if the key is wrong or the +/// database uses cipher parameters outside the shipped v4/v3 defaults — a loud +/// failure, never a silent wrong plaintext. +pub fn decrypt(ciphertext: &[u8], key: &SqlCipherKey) -> Result { + if ciphertext.len() < SALT_LEN { + return Err(DecryptError::TooSmall); + } + let salt = &ciphertext[..SALT_LEN]; + for profile in &PROFILES { + if ciphertext.len() < profile.page_size || ciphertext.len() % profile.page_size != 0 { + continue; + } + let (enc_key, hmac_key) = derive_keys(profile, key, salt); + let Some(page1) = ciphertext.get(..profile.page_size) else { + continue; // cov:unreachable: length checked above + }; + if page_hmac_ok(profile, &hmac_key, page1, 1) { + return decrypt_all(profile, &enc_key, &hmac_key, ciphertext); + } + } + Err(DecryptError::KeyOrParametersMismatch) +} diff --git a/core/tests/sqlcipher_oracle.rs b/core/tests/sqlcipher_oracle.rs new file mode 100644 index 0000000..d9c530f --- /dev/null +++ b/core/tests/sqlcipher_oracle.rs @@ -0,0 +1,144 @@ +//! Tier-2 SQLCipher decryption validation against REAL SQLCipher-engine output. +//! +//! The three fixtures under `tests/data/sqlcipher/` were minted by the SQLCipher +//! 4.17 CLI (an independent implementation — the oracle) with the exact commands +//! recorded in `tests/data/README.md`: +//! +//! sqlcipher enc_v4.db -> PRAGMA key='correct horse battery staple'; +//! sqlcipher enc_v3.db -> + PRAGMA cipher_compatibility = 3; +//! sqlcipher enc_rawkey.db -> PRAGMA key="x'<64 hex>'"; (raw 32-byte key) +//! +//! into a table `t(id INTEGER PRIMARY KEY, name TEXT, val INTEGER)` with three +//! known rows (and a second table `notes` in the v4 fixture). Our RustCrypto +//! decryptor must reproduce the plaintext the OpenSSL-backed engine produced; +//! reading back the known rows through the native reader is the cross-check. + +#![allow(clippy::unwrap_used, clippy::expect_used)] +// The module doc embeds literal `sqlcipher` reproducer command lines and product +// names; backticking every token would mangle the reproducer (cf. real_db.rs). +#![allow(clippy::doc_markdown)] + +use sqlite_core::sqlcipher::{self, SqlCipherKey, SqlCipherVersion}; +use sqlite_core::{Database, Value}; + +const ENC_V4: &[u8] = include_bytes!("../../tests/data/sqlcipher/enc_v4.db"); +const ENC_V3: &[u8] = include_bytes!("../../tests/data/sqlcipher/enc_v3.db"); +const ENC_RAWKEY: &[u8] = include_bytes!("../../tests/data/sqlcipher/enc_rawkey.db"); + +const PASSPHRASE: &[u8] = b"correct horse battery staple"; +/// The raw key passed to `PRAGMA key = "x'...'"` when minting `enc_rawkey.db`. +const RAW_KEY: [u8; 32] = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, + 0x76, 0x2e, 0x71, 0x60, 0xf3, 0x8b, 0x4d, 0xa5, 0x6a, 0x78, 0x4d, 0x90, 0x45, 0x19, 0x0c, 0xfe, +]; + +/// The three rows inserted into `t` in every fixture. +fn assert_table_t(db: &Database) { + let rows = db.read_table(2, 3).expect("walk table t (root page 2)"); + assert_eq!(rows.len(), 3, "three inserted rows in t"); + + assert_eq!(rows[0].rowid, 1); + assert_eq!(rows[0].values[1], Value::Text("alpha".into())); + assert_eq!(rows[0].values[2], Value::Integer(100)); + + assert_eq!(rows[1].rowid, 2); + assert_eq!(rows[1].values[1], Value::Text("bravo".into())); + assert_eq!(rows[1].values[2], Value::Integer(200)); + + assert_eq!(rows[2].rowid, 3); + assert_eq!(rows[2].values[1], Value::Text("unicode-snow".into())); + assert_eq!(rows[2].values[2], Value::Integer(300)); +} + +#[test] +fn decrypts_v4_and_reads_known_rows() { + let db = Database::open_encrypted(ENC_V4, &SqlCipherKey::Passphrase(PASSPHRASE.to_vec())) + .expect("open_encrypted v4"); + assert_eq!(db.header().page_size, 4096); + // SQLCipher sets a non-zero reserved-space byte for its per-page IV+HMAC. + assert!( + db.header().reserved > 0, + "SQLCipher reserves per-page space" + ); + assert_table_t(&db); + + // Second table `notes` on root page 3, single column. + let notes = db.read_table(3, 1).expect("walk notes (root page 3)"); + assert_eq!(notes.len(), 1); + assert_eq!( + notes[0].values[0], + Value::Text("the quick brown fox".into()) + ); +} + +#[test] +fn detects_v4_version() { + let out = sqlcipher::decrypt(ENC_V4, &SqlCipherKey::Passphrase(PASSPHRASE.to_vec())) + .expect("decrypt v4"); + assert_eq!(out.version, SqlCipherVersion::V4); + assert_eq!(out.page_size, 4096); + // First 16 bytes of the reconstructed plaintext are the standard magic. + assert_eq!(&out.plaintext[..16], b"SQLite format 3\x00"); +} + +#[test] +fn detects_v3_compat_and_reads_known_rows() { + let out = sqlcipher::decrypt(ENC_V3, &SqlCipherKey::Passphrase(PASSPHRASE.to_vec())) + .expect("decrypt v3"); + assert_eq!(out.version, SqlCipherVersion::V3, "v3 auto-detected"); + assert_eq!(out.page_size, 1024); + + let db = Database::open_encrypted(ENC_V3, &SqlCipherKey::Passphrase(PASSPHRASE.to_vec())) + .expect("open_encrypted v3"); + assert_eq!(db.header().page_size, 1024); + assert_table_t(&db); +} + +#[test] +fn decrypts_raw_key_and_reads_known_rows() { + let db = Database::open_encrypted(ENC_RAWKEY, &SqlCipherKey::RawKey(RAW_KEY)) + .expect("open_encrypted raw key"); + assert_table_t(&db); +} + +#[test] +fn wrong_passphrase_is_a_clean_error() { + let err = Database::open_encrypted(ENC_V4, &SqlCipherKey::Passphrase(b"wrong".to_vec())); + assert!( + err.is_err(), + "a wrong key must fail loud, not panic or misread" + ); + + let err = sqlcipher::decrypt(ENC_V4, &SqlCipherKey::Passphrase(b"wrong".to_vec())); + assert_eq!( + err.err(), + Some(sqlcipher::DecryptError::KeyOrParametersMismatch) + ); +} + +#[test] +fn wrong_raw_key_is_a_clean_error() { + let mut bad = RAW_KEY; + bad[0] ^= 0xff; + let err = sqlcipher::decrypt(ENC_RAWKEY, &SqlCipherKey::RawKey(bad)); + assert_eq!( + err.err(), + Some(sqlcipher::DecryptError::KeyOrParametersMismatch) + ); +} + +#[test] +fn truncated_ciphertext_never_panics() { + for len in 0..ENC_V4.len().min(4200) { + let _ = sqlcipher::decrypt( + &ENC_V4[..len], + &SqlCipherKey::Passphrase(PASSPHRASE.to_vec()), + ); + } +} + +#[test] +fn empty_input_is_too_small() { + let err = sqlcipher::decrypt(&[], &SqlCipherKey::RawKey(RAW_KEY)); + assert_eq!(err.err(), Some(sqlcipher::DecryptError::TooSmall)); +} diff --git a/docs/decisions/0009-batteries-included-decode.md b/docs/decisions/0009-batteries-included-decode.md index dad2550..4adc3d4 100644 --- a/docs/decisions/0009-batteries-included-decode.md +++ b/docs/decisions/0009-batteries-included-decode.md @@ -41,5 +41,6 @@ The analysis layer hard-depends on its decode/enrichment stack, always on: - Decode output stays honest: an `interpreted` object carries `lossy` / `confidence` and sits *alongside* the raw base64 so the original bytes still round-trip (README "What you get"). -- Decryption stays out of scope — encrypted databases are detected and named, not - decrypted; recovering their records needs the key/VFS (README "Out of scope"). +- Decryption of a keyed database is now in scope — see ADR 0010: given a key, + `Database::open_encrypted` decrypts SQLCipher pages into the plaintext stream the + reader consumes. The reserved-space *naming* here stays the detection front door. diff --git a/docs/decisions/0010-sqlcipher-decryption.md b/docs/decisions/0010-sqlcipher-decryption.md new file mode 100644 index 0000000..881a809 --- /dev/null +++ b/docs/decisions/0010-sqlcipher-decryption.md @@ -0,0 +1,51 @@ +# 10. SQLCipher decryption as a reader capability + +Date: 2026-07-28 +Status: Accepted (supersedes the "decryption out of scope" consequence of ADR 0009) + +## Context + +ADR 0009 detected and *named* SQLCipher/SEE/checksum-VFS reserved space but left +encrypted databases unreadable. The DLEAPP workflow supplies a key (a passphrase, +or a raw 32-byte key extracted from a keychain), so the missing piece is turning +ciphertext + key into the plaintext byte stream the existing reader already +consumes — not a new parser. + +A SQLCipher file is an ordinary page-structured SQLite database whose every page +is AES-256-CBC encrypted and per-page HMAC-authenticated, with a random 16-byte +salt in place of the `SQLite format 3\0` magic and PBKDF2-derived keys. Undoing +that is a decrypt-to-stream (container-level) concern, not anomaly analysis. + +## Decision + +- **Home: `sqlite-core` (the reader), module `sqlcipher`.** Decryption produces a + standalone plaintext SQLite `Vec` that `Database::open` reads unchanged; the + idiomatic, secure-by-default seam is one call, `Database::open_encrypted(bytes, + key)`. A third-party consumer of the reader gets encrypted-DB support without the + analyzer. The decrypted plaintext carries SQLCipher's own reserved-space header + byte, so the reader computes usable size with no extra plumbing. +- **RustCrypto only, never hand-rolled** (`pbkdf2`/`hmac`/`sha1`/`sha2`/`aes`/`cbc` + + `cipher`), per the fleet crypto law. These are low-MSRV, keeping `sqlite-core` + on `rust-version = 1.80`. +- **Two typed key shapes** (`SqlCipherKey::Passphrase` / `RawKey`) so a raw key can + never be silently PBKDF2-stretched as a passphrase (secure-by-design). +- **Version by page-1 HMAC verification.** The shipped v4 and v3 default profiles + differ in PBKDF2/HMAC digest, iterations, page size, and reserve. Nothing in the + header is readable pre-decryption, so the correct profile is the one whose page-1 + HMAC tag verifies against the derived key — the same auto-detect real tools use. +- **Fail loud, never misread.** A wrong key / unsupported parameters matches no + profile and returns `DecryptError::KeyOrParametersMismatch`; a later page failing + authentication after page 1 verified returns `PageAuthFailed(pgno)`. No path emits + plausible-but-wrong plaintext, and the decryptor is panic-free on crafted input. + +## Consequences + +- Encrypted evidence databases are now readable end-to-end; the reserved-space + *naming* from ADR 0009 stays as the detection front door. +- Validation is Tier-2: the fixtures under `tests/data/sqlcipher/` are minted by the + independent SQLCipher 4.17 CLI, and `core/tests/sqlcipher_oracle.rs` requires our + RustCrypto output to reproduce that engine's plaintext, read back to known rows. +- Scope is the common v4 defaults + v3 compatibility. Non-default cipher settings + (custom `cipher_page_size`, `kdf_iter`, HMAC algorithm, or plaintext-header bytes) + are a loud mismatch, not a silent miss — an additive profile list extends coverage + without touching the seam. diff --git a/supply-chain/config.toml b/supply-chain/config.toml index b4b58c5..5ca9750 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -13,9 +13,15 @@ url = "https://raw.githubusercontent.com/EmbarkStudios/rust-ecosystem/main/audit [imports.google] url = "https://raw.githubusercontent.com/google/rust-crate-audits/main/audits.toml" +[imports.isrg] +url = "https://raw.githubusercontent.com/divviup/libprio-rs/main/supply-chain/audits.toml" + [imports.mozilla] url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml" +[imports.zcash] +url = "https://raw.githubusercontent.com/zcash/rust-ecosystem/main/supply-chain/audits.toml" + [policy.sqlite-core] audit-as-crates-io = false @@ -27,7 +33,7 @@ audit-as-crates-io = false [[exemptions.aes]] version = "0.8.4" -criteria = "safe-to-run" +criteria = "safe-to-deploy" [[exemptions.aho-corasick]] version = "1.1.4" @@ -65,8 +71,8 @@ criteria = "safe-to-deploy" version = "2.13.0" criteria = "safe-to-deploy" -[[exemptions.block-buffer]] -version = "0.10.4" +[[exemptions.block-padding]] +version = "0.3.3" criteria = "safe-to-deploy" [[exemptions.bytemuck]] @@ -85,14 +91,14 @@ criteria = "safe-to-run" version = "0.35.0" criteria = "safe-to-run" +[[exemptions.cbc]] +version = "0.1.2" +criteria = "safe-to-deploy" + [[exemptions.cc]] version = "1.2.64" criteria = "safe-to-run" -[[exemptions.cfg-if]] -version = "1.0.4" -criteria = "safe-to-deploy" - [[exemptions.clap]] version = "4.6.1" criteria = "safe-to-deploy" @@ -117,10 +123,6 @@ criteria = "safe-to-run" version = "1.0.5" criteria = "safe-to-deploy" -[[exemptions.constant_time_eq]] -version = "0.3.1" -criteria = "safe-to-run" - [[exemptions.cpufeatures]] version = "0.2.17" criteria = "safe-to-deploy" @@ -129,10 +131,6 @@ criteria = "safe-to-deploy" version = "1.5.0" criteria = "safe-to-deploy" -[[exemptions.crunchy]] -version = "0.2.4" -criteria = "safe-to-deploy" - [[exemptions.crypto-common]] version = "0.1.7" criteria = "safe-to-deploy" @@ -181,10 +179,6 @@ criteria = "safe-to-deploy" version = "0.14.7" criteria = "safe-to-deploy" -[[exemptions.getrandom]] -version = "0.3.4" -criteria = "safe-to-run" - [[exemptions.gif]] version = "0.14.2" criteria = "safe-to-deploy" @@ -197,10 +191,6 @@ criteria = "safe-to-deploy" version = "0.17.1" criteria = "safe-to-deploy" -[[exemptions.hmac]] -version = "0.12.1" -criteria = "safe-to-run" - [[exemptions.image]] version = "0.25.10" criteria = "safe-to-deploy" @@ -209,10 +199,6 @@ criteria = "safe-to-deploy" version = "0.2.4" criteria = "safe-to-deploy" -[[exemptions.inout]] -version = "0.1.4" -criteria = "safe-to-run" - [[exemptions.inventory]] version = "0.3.24" criteria = "safe-to-deploy" @@ -261,10 +247,6 @@ criteria = "safe-to-deploy" version = "0.8.1" criteria = "safe-to-deploy" -[[exemptions.num-conv]] -version = "0.2.2" -criteria = "safe-to-deploy" - [[exemptions.once_cell]] version = "1.21.4" criteria = "safe-to-deploy" @@ -275,7 +257,7 @@ criteria = "safe-to-deploy" [[exemptions.pbkdf2]] version = "0.12.2" -criteria = "safe-to-run" +criteria = "safe-to-deploy" [[exemptions.pin-project-lite]] version = "0.2.17" @@ -325,26 +307,10 @@ criteria = "safe-to-deploy" version = "1.0.23" criteria = "safe-to-deploy" -[[exemptions.serde]] -version = "1.0.228" -criteria = "safe-to-deploy" - -[[exemptions.serde_core]] -version = "1.0.228" -criteria = "safe-to-deploy" - -[[exemptions.serde_derive]] -version = "1.0.228" -criteria = "safe-to-deploy" - [[exemptions.serde_json]] version = "1.0.150" criteria = "safe-to-deploy" -[[exemptions.sha2]] -version = "0.10.9" -criteria = "safe-to-deploy" - [[exemptions.shlex]] version = "2.0.1" criteria = "safe-to-run" @@ -361,10 +327,6 @@ criteria = "safe-to-deploy" version = "1.1.1" criteria = "safe-to-deploy" -[[exemptions.subtle]] -version = "2.6.1" -criteria = "safe-to-deploy" - [[exemptions.syn]] version = "2.0.118" criteria = "safe-to-deploy" @@ -385,10 +347,6 @@ criteria = "safe-to-deploy" version = "0.3.49" criteria = "safe-to-deploy" -[[exemptions.time-core]] -version = "0.1.9" -criteria = "safe-to-deploy" - [[exemptions.time-macros]] version = "0.2.29" criteria = "safe-to-deploy" @@ -433,10 +391,6 @@ criteria = "safe-to-deploy" version = "0.1.12" criteria = "safe-to-deploy" -[[exemptions.windows-link]] -version = "0.2.1" -criteria = "safe-to-deploy" - [[exemptions.windows-sys]] version = "0.61.2" criteria = "safe-to-deploy" @@ -465,10 +419,6 @@ criteria = "safe-to-run" version = "7.2.0" criteria = "safe-to-deploy" -[[exemptions.zlib-rs]] -version = "0.6.3" -criteria = "safe-to-deploy" - [[exemptions.zopfli]] version = "0.8.3" criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index d3ce663..93c16ca 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -137,18 +137,41 @@ criteria = "safe-to-deploy" version = "2.0.0" notes = "Fork of the original `adler` crate, zero unsfae code, works in `no_std`, does what it says on th tin." +[[audits.bytecode-alliance.audits.block-buffer]] +who = "Benjamin Bouvier " +criteria = "safe-to-deploy" +delta = "0.9.0 -> 0.10.2" + +[[audits.bytecode-alliance.audits.cfg-if]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "I am the author of this crate." + [[audits.bytecode-alliance.audits.cipher]] who = "Andrew Brown " criteria = "safe-to-deploy" version = "0.4.4" notes = "Most unsafe is hidden by `inout` dependency; only remaining unsafe is raw-splitting a slice and an unreachable hint. Older versions of this regularly reach ~150k daily downloads." +[[audits.bytecode-alliance.audits.constant_time_eq]] +who = "Nick Fitzgerald " +criteria = "safe-to-deploy" +version = "0.2.4" +notes = "A few tiny blocks of `unsafe` but each of them is very obviously correct." + [[audits.bytecode-alliance.audits.heck]] who = "Alex Crichton " criteria = "safe-to-deploy" delta = "0.4.1 -> 0.5.0" notes = "Minor changes for a `no_std` upgrade but otherwise everything looks as expected." +[[audits.bytecode-alliance.audits.inout]] +who = "Andrew Brown " +criteria = "safe-to-deploy" +version = "0.1.3" +notes = "A part of RustCrypto/utils, this crate is designed to handle unsafe buffers and carefully documents the safety concerns throughout. Older versions of this tally up to ~130k daily downloads." + [[audits.bytecode-alliance.audits.miniz_oxide]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -185,6 +208,12 @@ criteria = "safe-to-deploy" delta = "0.8.5 -> 0.8.9" notes = "No new unsafe code, just refactorings." +[[audits.bytecode-alliance.audits.num-conv]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.2.1" +notes = "Minor update, nothing major" + [[audits.bytecode-alliance.audits.num-traits]] who = "Andrew Brown " criteria = "safe-to-deploy" @@ -267,6 +296,31 @@ criteria = "safe-to-deploy" delta = "0.3.6 -> 0.3.7" aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.getrandom]] +who = "Android Legacy" +criteria = "safe-to-run" +version = "0.2.2" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.getrandom]] +who = "David Koloski " +criteria = "safe-to-deploy" +delta = "0.2.2 -> 0.2.12" +notes = "Audited at https://fxrev.dev/932979" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.getrandom]] +who = "Adrian Taylor " +criteria = "safe-to-run" +delta = "0.2.12 -> 0.2.14" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.getrandom]] +who = "danakj " +criteria = "safe-to-run" +delta = "0.2.14 -> 0.2.15" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.heck]] who = "Lukasz Anforowicz " criteria = "safe-to-deploy" @@ -468,6 +522,241 @@ Still no `unsafe` anywhere. """ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.197" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'`. + +There were some hits for `net`, but they were related to serialization and +not actually opening any connections or anything like that. + +There were 2 hits of `unsafe` when grepping: +* In `fn as_str` in `impl Buf` +* In `fn serialize` in `impl Serialize for net::Ipv4Addr` + +Unsafe review comments can be found in https://crrev.com/c/5350573/2 (this +review also covered `serde_json_lenient`). + +Version 1.0.130 of the crate has been added to Chromium in +https://crrev.com/c/3265545. The CL description contains a link to a +(Google-internal, sorry) document with a mini security review. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.197 -> 1.0.198" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.198 -> 1.0.201" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.201 -> 1.0.202" +notes = "Trivial changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.202 -> 1.0.203" +notes = "s/doc_cfg/docsrs/ + tuple_impls/tuple_impl_body-related changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.203 -> 1.0.204" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.204 -> 1.0.207" +notes = "The small change in `src/private/ser.rs` should have no impact on `ub-risk-2`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.207 -> 1.0.209" +notes = """ +The delta carries fairly small changes in `src/private/de.rs` and +`src/private/ser.rs` (see https://crrev.com/c/5812194/2..5). AFAICT the +delta has no impact on the `unsafe`, `from_utf8_unchecked`-related parts +of the crate (in `src/de/format.rs` and `src/ser/impls.rs`). +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.209 -> 1.0.210" +notes = "Almost no new code - just feature rearrangement" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Liza Burakova " +criteria = "safe-to-deploy" +delta = "1.0.210 -> 1.0.213" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.213 -> 1.0.214" +notes = "No unsafe, no crypto" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.214 -> 1.0.215" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.215 -> 1.0.216" +notes = "The delta makes minor changes in `build.rs` - switching to the `?` syntax sugar." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.216 -> 1.0.217" +notes = "Minimal changes, nothing unsafe" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "1.0.217 -> 1.0.218" +notes = "No changes outside comments and documentation." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.218 -> 1.0.219" +notes = "Just allowing `clippy::elidable_lifetime_names`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.197" +notes = 'Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.197 -> 1.0.201" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.201 -> 1.0.202" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.202 -> 1.0.203" +notes = 'Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.203 -> 1.0.204" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.204 -> 1.0.207" +notes = 'Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.207 -> 1.0.209" +notes = ''' +There are no code changes in this delta - see https://crrev.com/c/5812194/2..5 + +I've neverthless also grepped for `-i cipher`, `-i crypto`, `\bfs\b`, +`\bnet\b`, and `\bunsafe\b`. There were no hits. +''' +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.209 -> 1.0.210" +notes = "Almost no new code - just feature rearrangement" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Liza Burakova " +criteria = "safe-to-deploy" +delta = "1.0.210 -> 1.0.213" +notes = "Grepped for 'unsafe', 'crypt', 'cipher', 'fs', 'net' - there were no hits" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.213 -> 1.0.214" +notes = "No changes to unsafe, no crypto" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.214 -> 1.0.215" +notes = "Minor changes should not impact UB risk" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.215 -> 1.0.216" +notes = "The delta adds `#[automatically_derived]` in a few places. Still no `unsafe`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.216 -> 1.0.217" +notes = "No changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "1.0.217 -> 1.0.218" +notes = "No changes outside comments and documentation." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.218 -> 1.0.219" +notes = "Minor changes (clippy tweaks, using `mem::take` instead of `mem::replace`)." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.sha1]] who = "David Koloski " criteria = "safe-to-deploy" @@ -486,6 +775,136 @@ Previously reviewed during security review and the audit is grandparented in. """ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.isrg.audits.block-buffer]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.9.0" + +[[audits.isrg.audits.cfg-if]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "1.0.0 -> 1.0.1" + +[[audits.isrg.audits.cfg-if]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.1 -> 1.0.3" + +[[audits.isrg.audits.cfg-if]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "1.0.3 -> 1.0.4" + +[[audits.isrg.audits.getrandom]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.3.3 -> 0.3.4" + +[[audits.isrg.audits.hmac]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.12.1" + +[[audits.isrg.audits.serde]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.219 -> 1.0.224" + +[[audits.isrg.audits.serde]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.224 -> 1.0.225" + +[[audits.isrg.audits.serde]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "1.0.225 -> 1.0.226" + +[[audits.isrg.audits.serde_core]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +version = "1.0.224" + +[[audits.isrg.audits.serde_core]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.224 -> 1.0.225" + +[[audits.isrg.audits.serde_core]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "1.0.225 -> 1.0.226" + +[[audits.isrg.audits.serde_derive]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.219 -> 1.0.224" + +[[audits.isrg.audits.serde_derive]] +who = "J.C. Jones " +criteria = "safe-to-deploy" +delta = "1.0.224 -> 1.0.225" + +[[audits.isrg.audits.serde_derive]] +who = "Tim Geoghegan " +criteria = "safe-to-deploy" +delta = "1.0.225 -> 1.0.226" + +[[audits.isrg.audits.sha2]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.10.2" + +[[audits.isrg.audits.sha2]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.10.8 -> 0.10.9" + +[[audits.isrg.audits.subtle]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "2.5.0 -> 2.6.1" + +[[audits.isrg.audits.zlib-rs]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = """ +zlib-rs uses unsafe Rust for invoking compiler intrinsics (i.e. SIMD), eschewing bounds checks, along the FFI boundary, and for interacting with pointers sourced from C. I have extensively reviewed and fuzzed the unsafe code. All findings from that work have been resolved as of version 0.4.0. To the best of my ability, I believe it's free of any serious security problems. + +zlib-rs does not require any external dependencies. +""" + +[[audits.isrg.audits.zlib-rs]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.4.1" + +[[audits.isrg.audits.zlib-rs]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +delta = "0.4.1 -> 0.4.2" + +[[audits.isrg.audits.zlib-rs]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +delta = "0.4.2 -> 0.5.0" + +[[audits.isrg.audits.zlib-rs]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 0.5.1" + +[[audits.isrg.audits.zlib-rs]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +delta = "0.5.1 -> 0.5.2" + +[[audits.isrg.audits.zlib-rs]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +delta = "0.5.2 -> 0.6.3" + [[audits.mozilla.wildcard-audits.encoding_rs]] who = "Henri Sivonen " criteria = "safe-to-deploy" @@ -501,6 +920,18 @@ criteria = "safe-to-deploy" delta = "2.0.0 -> 2.0.1" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.block-buffer]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.crunchy]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +version = "0.2.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.deranged]] who = "Alex Franchuk " criteria = "safe-to-deploy" @@ -525,6 +956,31 @@ delta = "0.4.0 -> 0.5.8" notes = "New unsafe code is properly guarded" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.getrandom]] +who = "Chris Martin " +criteria = "safe-to-deploy" +delta = "0.2.15 -> 0.3.1" +notes = """ +I've looked over all unsafe code, and it appears to be safe, fully initializing the rng buffers. +In addition, I've checked Linux, Windows, Mac, and Android more thoroughly against API +documentation. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.getrandom]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +delta = "0.3.1 -> 0.3.3" +notes = """ +Biggest non-trivial change is a new UEFI back-end, which looks reasonable to +the best of my ability: There's some trickiness on initialization but doesn't +look unsafe, at worse it leaks, and it might not if the relevant pointers are +static/non-owning. Other changes also look reasonable too: some tweaks to +inlining and a syscall-based linux back-end, whose relevant unsafe code looks +reasonable. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.hex]] who = "Simon Friedberger " criteria = "safe-to-deploy" @@ -544,6 +1000,23 @@ delta = "2.11.4 -> 2.14.0" notes = "Mostly internal refactorings. No new unsafe code." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.num-conv]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.1.0" +notes = """ +Very straightforward, simple crate. No dependencies, unsafe, extern, +side-effectful std functions, etc. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-conv]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.2.0" +notes = "Revision only removes code" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.powerfmt]] who = "Alex Franchuk " criteria = "safe-to-deploy" @@ -566,18 +1039,122 @@ criteria = "safe-to-deploy" delta = "1.0.40 -> 1.0.45" aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" +[[audits.mozilla.audits.serde]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.0.226 -> 1.0.227" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.serde]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.227 -> 1.0.228" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.serde_core]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.0.226 -> 1.0.227" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.serde_core]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.227 -> 1.0.228" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.serde_derive]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.0.226 -> 1.0.227" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.serde_derive]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.227 -> 1.0.228" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Jeff Muizelaar " +criteria = "safe-to-deploy" +delta = "0.10.6 -> 0.10.8" +notes = """ +The bulk of this is https://github.com/RustCrypto/hashes/pull/490 which adds aarch64 support along with another PR adding longson. +I didn't check the implementation thoroughly but there wasn't anything obviously nefarious. 0.10.8 has been out for more than a year +which suggests no one else has found anything either. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.strsim]] who = "Ben Dean-Kawamura " criteria = "safe-to-deploy" delta = "0.10.0 -> 0.11.1" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.subtle]] +who = "Simon Friedberger " +criteria = "safe-to-deploy" +version = "2.5.0" +notes = "The goal is to provide some constant-time correctness for cryptographic implementations. The approach is reasonable, it is known to be insufficient but this is pointed out in the documentation." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +version = "0.1.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.1.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.1.2 -> 0.1.4" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.8" +notes = "No unsafe code" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.utf8parse]] who = "Nika Layzell " criteria = "safe-to-deploy" delta = "0.2.1 -> 0.2.2" aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" +[[audits.mozilla.audits.windows-link]] +who = "Mark Hammond " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "A microsoft crate allowing unsafe calls to windows apis." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.windows-link]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.2.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.zmij]] who = "Benjamin VanderSloot " criteria = "safe-to-deploy" @@ -595,3 +1172,73 @@ criteria = "safe-to-deploy" delta = "1.0.20 -> 1.0.21" notes = "Almost no code changes. No new unsafe code." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.zcash.audits.block-buffer]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.10.3 -> 0.10.4" +notes = "Adds panics to prevent a block size of zero from causing unsoundness." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.constant_time_eq]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.4 -> 0.2.5" +notes = "No code changes." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.constant_time_eq]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.5 -> 0.2.6" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.constant_time_eq]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.6 -> 0.3.0" +notes = "Replaces some `unsafe` code by bumping MSRV to 1.66 (to access `core::hint::black_box`)." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.constant_time_eq]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.crunchy]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.3 -> 0.2.4" +notes = """ +Build script change is to fix a bug where a path separator for an included file +was being selected by the target OS instead of the host OS. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.inout]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.3 -> 0.1.4" +aggregated-from = "https://raw.githubusercontent.com/zcash/wallet/main/supply-chain/audits.toml" + +[[audits.zcash.audits.num-conv]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +delta = "0.2.1 -> 0.2.2" +notes = "No changes to unsafe code, straightforward refactoring and cleanup." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.time-core]] +who = "Kris Nuttycombe " +criteria = "safe-to-deploy" +delta = "0.1.8 -> 0.1.9" +notes = "No unsafe code; macro additions are straightforward refactoring changes." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.windows-link]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.2.1" +notes = "No code changes at all." +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" diff --git a/tests/data/README.md b/tests/data/README.md index e0b43ee..6a3b149 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -401,3 +401,53 @@ its own provenance README (source, NIST/author hashes, licence, ground truth): - **md5:** `6fe4622248008bf248eb367f37477c2c` — 536 bytes. - **Notable contents:** oversized spilled-payload cell; carving degrades to an empty/partial result rather than aborting. + +#### sqlcipher/ (REAL-engine SQLCipher ciphertext, Tier-2 decryption oracle) + +- **Source:** SYNTHETIC — minted by the **SQLCipher 4.17.0 CLI** + (`/opt/homebrew/bin/sqlcipher`, an independent OpenSSL-backed implementation — + the decryption oracle). Ground truth is derivable from the construction below. + The 16-byte per-file salt is random, so a re-mint yields different bytes; the + committed files are the pinned artifacts these md5s refer to. +- **Consumed by:** `core/tests/sqlcipher_oracle.rs` — our RustCrypto decryptor + (`sqlite_core::sqlcipher`) must reproduce the plaintext the engine produced, then + the native reader reads back the known rows. +- **Common schema** (all three): `t(id INTEGER PRIMARY KEY, name TEXT, val INTEGER)` + with rows `(1,'alpha',100) (2,'bravo',200) (3,'unicode-snow',300)`; + passphrase fixtures share the passphrase `correct horse battery staple`. +- **Generators:** + + ```sh + # enc_v4.db — SQLCipher 4 defaults (PBKDF2/HMAC-SHA512, 256000 iter, page 4096, reserve 80) + sqlcipher enc_v4.db <<'SQL' + PRAGMA key = 'correct horse battery staple'; + CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT, val INTEGER); + INSERT INTO t VALUES (1,'alpha',100),(2,'bravo',200),(3,'unicode-snow',300); + CREATE TABLE notes(body TEXT); + INSERT INTO notes VALUES ('the quick brown fox'); + SQL + + # enc_v3.db — SQLCipher 3 compatibility (PBKDF2/HMAC-SHA1, 64000 iter, page 1024, reserve 48) + sqlcipher enc_v3.db <<'SQL' + PRAGMA key = 'correct horse battery staple'; + PRAGMA cipher_compatibility = 3; + CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT, val INTEGER); + INSERT INTO t VALUES (1,'alpha',100),(2,'bravo',200),(3,'unicode-snow',300); + SQL + + # enc_rawkey.db — raw 32-byte key (no passphrase KDF for the encryption key) + sqlcipher enc_rawkey.db <<'SQL' + PRAGMA key = "x'2b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfe'"; + CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT, val INTEGER); + INSERT INTO t VALUES (1,'alpha',100),(2,'bravo',200),(3,'unicode-snow',300); + SQL + ``` + +- **md5:** + - `enc_v4.db` `dca83d44f81d66154b0417b1ef6a295d` — 12288 bytes (3 pages). + - `enc_v3.db` `e6a1cc04a264f67ce9169de82b749cc5` — 2048 bytes (2 pages). + - `enc_rawkey.db` `18ea1699a334c0667edad678bba08131` — 8192 bytes (2 pages). +- **Notable contents:** first 16 bytes are the random salt (NOT the `SQLite + format 3\0` magic); `enc_v4.db` additionally holds table `notes` (root page 3) + with one row `the quick brown fox`. Version is auto-detected by page-1 HMAC + verification; a wrong key matches no profile and fails loud. diff --git a/tests/data/sqlcipher/enc_rawkey.db b/tests/data/sqlcipher/enc_rawkey.db new file mode 100644 index 0000000..cc2c948 Binary files /dev/null and b/tests/data/sqlcipher/enc_rawkey.db differ diff --git a/tests/data/sqlcipher/enc_v3.db b/tests/data/sqlcipher/enc_v3.db new file mode 100644 index 0000000..8d6a0ab Binary files /dev/null and b/tests/data/sqlcipher/enc_v3.db differ diff --git a/tests/data/sqlcipher/enc_v4.db b/tests/data/sqlcipher/enc_v4.db new file mode 100644 index 0000000..04745b8 Binary files /dev/null and b/tests/data/sqlcipher/enc_v4.db differ