From 1a6812c97facf3115823a632f0c1a235b9ac5575 Mon Sep 17 00:00:00 2001 From: Albert Hui Date: Tue, 28 Jul 2026 23:52:05 +0800 Subject: [PATCH 1/6] =?UTF-8?q?test(sqlcipher):=20RED=20=E2=80=94=20Tier-2?= =?UTF-8?q?=20oracle=20for=20SQLCipher=20decryption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Failing tests (API not yet implemented) that decrypt three real SQLCipher-4.17-CLI-minted fixtures (v4 defaults, v3 compatibility, and a raw 32-byte key) and read back the known rows through the native reader. The engine-authored ciphertext is the independent oracle: our RustCrypto decryptor must reproduce the plaintext OpenSSL produced. Co-Authored-By: Claude Fable 5 --- core/tests/sqlcipher_oracle.rs | 135 +++++++++++++++++++++++++++++ tests/data/sqlcipher/enc_rawkey.db | Bin 0 -> 8192 bytes tests/data/sqlcipher/enc_v3.db | Bin 0 -> 2048 bytes tests/data/sqlcipher/enc_v4.db | Bin 0 -> 12288 bytes 4 files changed, 135 insertions(+) create mode 100644 core/tests/sqlcipher_oracle.rs create mode 100644 tests/data/sqlcipher/enc_rawkey.db create mode 100644 tests/data/sqlcipher/enc_v3.db create mode 100644 tests/data/sqlcipher/enc_v4.db diff --git a/core/tests/sqlcipher_oracle.rs b/core/tests/sqlcipher_oracle.rs new file mode 100644 index 0000000..6ebeb17 --- /dev/null +++ b/core/tests/sqlcipher_oracle.rs @@ -0,0 +1,135 @@ +//! 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)] + +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(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(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(sqlcipher::DecryptError::TooSmall)); +} diff --git a/tests/data/sqlcipher/enc_rawkey.db b/tests/data/sqlcipher/enc_rawkey.db new file mode 100644 index 0000000000000000000000000000000000000000..cc2c9480a0f46d90096a17cf1cd664ccdab6b3a6 GIT binary patch literal 8192 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 literal 0 HcmV?d00001 diff --git a/tests/data/sqlcipher/enc_v4.db b/tests/data/sqlcipher/enc_v4.db new file mode 100644 index 0000000000000000000000000000000000000000..04745b8cda80668cefc47cf417e92c743f9b567f GIT binary patch literal 12288 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 aql~+7`Yd$virl$>8f1)BQqE~n`qWIAa_gM{ literal 0 HcmV?d00001 From b8487891586677a91f2b1448955c9897c00d4b4e Mon Sep 17 00:00:00 2001 From: Albert Hui Date: Wed, 29 Jul 2026 00:00:43 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(sqlcipher):=20GREEN=20=E2=80=94=20decr?= =?UTF-8?q?ypt=20SQLCipher=20pages=20into=20the=20reader's=20stream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `sqlite_core::sqlcipher` and `Database::open_encrypted(bytes, key)`: given a key, decrypt a SQLCipher database (PBKDF2 key derivation → AES-256-CBC per-page decrypt → per-page HMAC authentication) into a plaintext SQLite byte stream the existing reader consumes unchanged. - RustCrypto only (pbkdf2/hmac/sha1/sha2/aes/cbc/cipher), never hand-rolled; all low-MSRV so sqlite-core stays on rust-version 1.80. - Two typed key shapes (Passphrase / RawKey) — a raw key can never be silently PBKDF2-stretched as a passphrase (secure-by-design). - Version auto-detected by page-1 HMAC verification across the shipped SQLCipher v4 (default) and v3-compatibility profiles. - Fail loud: a wrong key/params is DecryptError::KeyOrParametersMismatch; a later page failing auth is PageAuthFailed(pgno). Panic-free on crafted/truncated input; no path emits plausible-but-wrong plaintext. Tier-2 validated by core/tests/sqlcipher_oracle.rs against three real SQLCipher-4.17-CLI fixtures (v4, v3-compat, raw key); the RustCrypto decryptor reproduces the engine's plaintext, read back to known rows. Provenance in tests/data/README.md; rationale in ADR 0010. Gates: cargo test --workspace (548) + clippy -D warnings + fmt --check. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 26 ++ core/Cargo.toml | 11 + core/src/lib.rs | 25 ++ core/src/sqlcipher.rs | 328 ++++++++++++++++++ core/tests/sqlcipher_oracle.rs | 15 +- .../0009-batteries-included-decode.md | 5 +- docs/decisions/0010-sqlcipher-decryption.md | 51 +++ tests/data/README.md | 50 +++ 8 files changed, 506 insertions(+), 5 deletions(-) create mode 100644 core/src/sqlcipher.rs create mode 100644 docs/decisions/0010-sqlcipher-decryption.md diff --git a/Cargo.lock b/Cargo.lock index 9c16969..05a6943 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", ] @@ -953,7 +972,14 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" name = "sqlite-core" version = "0.10.2" 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 index 6ebeb17..d9c530f 100644 --- a/core/tests/sqlcipher_oracle.rs +++ b/core/tests/sqlcipher_oracle.rs @@ -14,6 +14,9 @@ //! 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}; @@ -107,7 +110,10 @@ fn wrong_passphrase_is_a_clean_error() { ); let err = sqlcipher::decrypt(ENC_V4, &SqlCipherKey::Passphrase(b"wrong".to_vec())); - assert_eq!(err, Err(sqlcipher::DecryptError::KeyOrParametersMismatch)); + assert_eq!( + err.err(), + Some(sqlcipher::DecryptError::KeyOrParametersMismatch) + ); } #[test] @@ -115,7 +121,10 @@ 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(sqlcipher::DecryptError::KeyOrParametersMismatch)); + assert_eq!( + err.err(), + Some(sqlcipher::DecryptError::KeyOrParametersMismatch) + ); } #[test] @@ -131,5 +140,5 @@ fn truncated_ciphertext_never_panics() { #[test] fn empty_input_is_too_small() { let err = sqlcipher::decrypt(&[], &SqlCipherKey::RawKey(RAW_KEY)); - assert_eq!(err, Err(sqlcipher::DecryptError::TooSmall)); + 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/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. From b7691ca285646cdd92ef5f44d9ac0b28d2499a3e Mon Sep 17 00:00:00 2001 From: Albert Hui Date: Thu, 30 Jul 2026 21:04:09 +0800 Subject: [PATCH 3/6] chore(renovate): automerge lock-file maintenance lockFileMaintenance was enabled but not automerging, so refreshes opened PRs that then aged - producing exactly the lock lag the setting exists to prevent. Lock lag is the staleness that actually recurs in this fleet: a caret requirement that can reach a newer version while the committed lock sits behind it. rangeStrategy does not fix that; automerged lock maintenance does. Enabled only because this repo has an MSRV job, per ADR-0018: automerge is gated on the MSRV promise being protected by CI rather than by a reviewer noticing. Renovate also waits for checks to pass before automerging, so a bump that raises the minimum Rust version fails the MSRV job and cannot land. Repos without an MSRV job were deliberately skipped in this sweep. chore, not fix, so this does not cut a release: no crate code changed. Verified: renovate.json parses as JSON and lockFileMaintenance.automerge is true. Co-Authored-By: Claude Opus 5 (1M context) --- renovate.json | 1 + 1 file changed, 1 insertion(+) diff --git a/renovate.json b/renovate.json index 8eb060d..fe0fcb7 100644 --- a/renovate.json +++ b/renovate.json @@ -34,6 +34,7 @@ ], "lockFileMaintenance": { "enabled": true, + "automerge": true, "schedule": ["before 6am on Monday"] } } From c2f15593bd0fb1edf2f3b91625cc33aa67e0f3bb Mon Sep 17 00:00:00 2001 From: Albert Hui Date: Sat, 8 Aug 2026 22:10:42 -0700 Subject: [PATCH 4/6] chore: fix .gitignore target anchoring for nested cargo projects `/target/` is anchored to the repository root, so it does not ignore the build directories of nested cargo projects (fuzz/, bindings/python/). Those artifacts were being picked up by `git add -A` and committed. Unanchored `target/` matches at any depth, which is the intended behaviour. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/ From 4464c382122ae1b3e7c9c64ab216fa93ebe70aff Mon Sep 17 00:00:00 2001 From: Albert Hui Date: Sun, 9 Aug 2026 06:21:43 +0000 Subject: [PATCH 5/6] chore: mark byte-exact fixture trees -text The repo holds byte-exact fixtures and shipped no .gitattributes, so git's text/binary autodetection was the only thing standing between the fixtures and CRLF translation on a Windows checkout. Prophylactic, not a repair: the three SQLCipher fixtures added here each carry a NUL at byte 100 / 135 / 1044, inside git's 8000-byte sniff window, so git already classifies them binary and nothing is currently corrupted. The next fixture is not guaranteed that property -- a compressed payload whose first NUL falls past 8000 bytes is classified as text and rewritten. --- .gitattributes | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitattributes 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 From b9a0e1308622ff04464f46969bebbc6247ea4e2a Mon Sep 17 00:00:00 2001 From: Albert Hui Date: Sun, 9 Aug 2026 14:53:09 +0000 Subject: [PATCH 6/6] chore(vet): import the zcash and isrg audit sets; record the SQLCipher crypto deps SQLCipher brings six third-party RustCrypto crates into the graph. New crates entering the graph are a supply-chain decision, not the version-churn bookkeeping that a cache refresh covers, so this takes the strongest mechanism that applies before falling back to a weaker one. Imported two more aggregate audit sets rather than exempting everything: [imports.zcash] https://raw.githubusercontent.com/zcash/rust-ecosystem/... [imports.isrg] https://raw.githubusercontent.com/divviup/libprio-rs/... That is not paperwork. Exemptions drop 117 -> 103: SIXTEEN crates move from "nobody read this" to genuinely audited, including hmac and inout -- two of the six SQLCipher deps -- plus serde, sha2, subtle, getrandom, zlib-rs and others that were exempted before this PR. Four remain unaudited by us and by every imported set, and are exempted with a note saying exactly that: aes 0.8.4, block-padding 0.3.3, cbc 0.1.2, pbkdf2 0.12.2. The fleet rule forbids hand-rolled crypto and requires an audited ecosystem crate, so these are the correct dependencies; the exemption records that nobody has read these versions, which is true. aes and pbkdf2 were already exempted, but at criteria safe-to-run -- enough while they entered only through `zip` on dev/test paths. SQLCipher decryption puts them on the SHIPPED path, so both are raised to safe-to-deploy. The gate surfacing that change of exposure is the gate working. No certify, no --accept-all. Gate proven able to fail, with a well-formed mutation: re-pointing the cbc exemption to a version that does not exist gives exit 255 and `cbc:0.1.2 missing ["safe-to-deploy"]`; restoring it gives exit 0. (A first attempt that deleted the block outright only proved the store formatter works -- vet complained about blank lines, not about coverage.) --- supply-chain/config.toml | 78 +---- supply-chain/imports.lock | 647 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 661 insertions(+), 64 deletions(-) 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"