diff --git a/README.md b/README.md index 7b01b77..8e53be7 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,4 @@ SQLite3MC is MIT licensed. The amalgamation also carries public-domain code (SQL A daily workflow in the [repository](https://github.com/LucaCappelletti94/sqlite3mc-src) opens a pull request for each new SQLite3MC release, taking the archive's checksum only from the release's Sigstore-signed `SHA256SUMS`. CI re-runs `upgrade.sh`, which checks that signature again before trusting the pinned checksum, to prove the vendored bytes match the pinned release. It also compiles the packaged sources natively and through `sqlite-wasm-rs`, and checks that each build opens the other's files in every cipher, under Node and in headless Chrome and Firefox, on OPFS too. -CI also runs SQLite3MC's own tests, a rekey of every cipher and SQLite's TCL suite through the shipped amalgamation, once plainly and once under the address and undefined-behaviour sanitizers, with every expected failure named and explained. Seven fuzz targets in `fuzz/` run on every pull request and daily through `ClusterFuzzLite`: a differential fuzzer comparing the amalgamation's `sqlcipher` scheme against [SQLCipher](https://www.zetetic.net/sqlcipher/) itself, a self-consistency check across the six native cipher schemes, and dedicated targets for rekeying, `journal_mode=WAL`, `ATTACH DATABASE`, the `sqlite3_backup_*` API, and shared-cache mode. +CI also runs SQLite3MC's own tests, a rekey of every cipher and SQLite's TCL suite through the shipped amalgamation, once plainly and once under the address and undefined-behaviour sanitizers, with every expected failure named and explained. Six fuzz targets in `fuzz/` run on every pull request and daily through `ClusterFuzzLite`: a self-consistency check across the six native cipher schemes, and dedicated targets for rekeying, `journal_mode=WAL`, `ATTACH DATABASE`, the `sqlite3_backup_*` API, and shared-cache mode. diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 79d7a71..8a6bca6 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -113,12 +113,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" -[[package]] -name = "sqlcipher-src" -version = "419.0.6+sqlcipher-4.19.0-sqlite-3.53.4-libtomcrypt-1.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab732d48ebf2a1449e94d071b26544f99845834c75c3c210e927ace097b49aa4" - [[package]] name = "sqlite3mc-src" version = "206.1.0+sqlite3mc-2.6.1-sqlite-3.54.0" @@ -130,7 +124,6 @@ dependencies = [ "arbitrary", "cc", "libfuzzer-sys", - "sqlcipher-src", "sqlite3mc-src", ] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 833d894..6fdd5f7 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -3,7 +3,7 @@ name = "sqlite3mc-src-fuzz" version = "0.0.0" edition = "2021" publish = false -description = "Fuzzing SQLite3MC against SQLCipher differentially, its own native ciphers for self-consistency, its rekey support, its WAL journal mode, its ATTACH DATABASE support, its backup API, and its shared-cache mode" +description = "Fuzzing SQLite3MC's own cipher schemes for self-consistency, its rekey support, its WAL journal mode, its ATTACH DATABASE support, its backup API, and its shared-cache mode" [package.metadata] cargo-fuzz = true @@ -14,16 +14,8 @@ libfuzzer-sys = "0.4.13" [build-dependencies] cc = "1.5.1" -sqlcipher-src = "419" sqlite3mc-src = { path = ".." } -[[bin]] -name = "differential" -path = "fuzz_targets/differential.rs" -test = false -doc = false -bench = false - [[bin]] name = "native" path = "fuzz_targets/native.rs" diff --git a/fuzz/build.rs b/fuzz/build.rs index e8385d7..2280a82 100644 --- a/fuzz/build.rs +++ b/fuzz/build.rs @@ -1,126 +1,41 @@ -//! Compiles SQLite3MC and SQLCipher into one binary, prefixing each object's globals apart. +//! Compiles the vendored amalgamation as a plain static library. -use std::{ - env, - fmt::Write as _, - fs, - path::{Path, PathBuf}, - process::Command, -}; - -struct Side<'a> { - prefix: &'a str, - source: PathBuf, - includes: &'a [&'a Path], - defines: &'a [(&'a str, Option<&'a str>)], -} +use std::{env, fs, path::PathBuf, process::Command}; fn main() { assert_eq!( env::var("CARGO_CFG_TARGET_OS").as_deref(), Ok("linux"), - "the symbol renaming needs ELF objects and GNU-compatible nm and objcopy, so the fuzz crate builds on Linux only" + "the fuzz crate builds on Linux only" ); let out = PathBuf::from(env::var_os("OUT_DIR").expect("cargo sets OUT_DIR")); let mc_dir = sqlite3mc_src::source_dir(); - let sc_dir = sqlcipher_src::source_dir(); - let common = [ + let mut cc = cc::Build::new(); + cc.file(mc_dir.join(sqlite3mc_src::SOURCE_FILE)) + .warnings(false); + cc.include(mc_dir); + for (name, value) in [ + ("SQLITE_CORE", None), ("SQLITE_TEMP_STORE", Some("2")), ("SQLITE_ENABLE_DBPAGE_VTAB", None), ("SQLITE_DEFAULT_MEMSTATUS", Some("0")), - ]; - let mc_defines = [common.as_slice(), &[("SQLITE_CORE", None)]].concat(); - build( - &Side { - prefix: "mc", - source: mc_dir.join(sqlite3mc_src::SOURCE_FILE), - includes: &[mc_dir], - defines: &mc_defines, - }, - &out, - ); - let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - build( - &Side { - prefix: "sc", - source: sqlcipher_unit(crate_dir, sc_dir, &out), - includes: &[ - sc_dir, - &sc_dir.join(sqlcipher_src::LIBTOMCRYPT_INCLUDE_DIR), - crate_dir, - ], - defines: &common, - }, - &out, - ); - println!("cargo:rustc-link-search=native={}", out.display()); - for lib in ["m", "pthread", "dl"] { - println!("cargo:rustc-link-lib={lib}"); - } - println!("cargo:rerun-if-changed=build.rs"); - for file in [ - "sqlcipher-wrapper.c", - "sqlcipher-ltc.h", - "sqlcipher-entropy.c", ] { - println!("cargo:rerun-if-changed={file}"); - } - println!("cargo:rerun-if-env-changed=NM"); - println!("cargo:rerun-if-env-changed=OBJCOPY"); -} - -fn build(side: &Side<'_>, out: &Path) { - let mut cc = cc::Build::new(); - cc.file(&side.source).warnings(false); - for include in side.includes { - println!("cargo:rerun-if-changed={}", include.display()); - cc.include(include); - } - for (name, value) in side.defines { - cc.define(name, *value); + cc.define(name, value); } instrument(&mut cc); let objects = cc.compile_intermediates(); let [object] = objects.as_slice() else { - panic!( - "{} compiled to {} objects", - side.source.display(), - objects.len() - ); + panic!("the amalgamation compiled to {} objects", objects.len()); }; - - let map = out.join(format!("{}.redefine", side.prefix)); - fs::write(&map, redefinitions(object, side.prefix)).expect("OUT_DIR is writable"); - let renamed = out.join(format!("{}.o", side.prefix)); - run(Command::new(tool("OBJCOPY", "objcopy")) - .arg(format!("--redefine-syms={}", map.display())) - .arg(object) - .arg(&renamed)); - - let archive = out.join(format!("lib{}.a", side.prefix)); + let archive = out.join("libmc.a"); let _ = fs::remove_file(&archive); - run(cc.get_archiver().arg("crs").arg(&archive).arg(&renamed)); - println!("cargo:rustc-link-lib=static={}", side.prefix); -} - -/// Writes the one translation unit SQLCipher builds from, the wrapper's configuration and SQLCipher, then every -/// libtomcrypt source in order, then the entropy hook, all sharing one configuration as upstream's single-file -/// libtomcrypt does. -fn sqlcipher_unit(crate_dir: &Path, sc_dir: &Path, out: &Path) -> PathBuf { - let sources = std::iter::once(crate_dir.join("sqlcipher-wrapper.c")) - .chain( - sqlcipher_src::LIBTOMCRYPT_SOURCES - .iter() - .map(|source| sc_dir.join(source)), - ) - .chain(std::iter::once(crate_dir.join("sqlcipher-entropy.c"))); - let unit = sources.fold(String::new(), |mut unit, source| { - let _ = writeln!(unit, "#include \"{}\"", source.display()); - unit - }); - let path = out.join("sqlcipher-unit.c"); - fs::write(&path, unit).expect("OUT_DIR is writable"); - path + run(cc.get_archiver().arg("crs").arg(&archive).arg(object)); + println!("cargo:rustc-link-search=native={}", out.display()); + println!("cargo:rustc-link-lib=static=mc"); + for lib in ["m", "pthread", "dl"] { + println!("cargo:rustc-link-lib={lib}"); + } + println!("cargo:rerun-if-changed=build.rs"); } /// Gives the C side the coverage and sanitizer instrumentation `cargo fuzz` gives the Rust side. @@ -139,25 +54,7 @@ fn instrument(cc: &mut cc::Build) { } } -/// One `old new` line per global symbol the object defines. -fn redefinitions(object: &Path, prefix: &str) -> String { - let listing = run(Command::new(tool("NM", "nm")) - .args(["--defined-only", "--extern-only", "--format=posix"]) - .arg(object)); - listing - .lines() - .filter_map(|line| line.split_whitespace().next()) - .fold(String::new(), |mut map, name| { - let _ = writeln!(map, "{name} {prefix}_{name}"); - map - }) -} - -fn tool(var: &str, default: &str) -> String { - env::var(var).unwrap_or_else(|_| default.to_owned()) -} - -fn run(command: &mut Command) -> String { +fn run(command: &mut Command) { let output = command .output() .unwrap_or_else(|e| panic!("cannot run {command:?}: {e}")); @@ -166,5 +63,4 @@ fn run(command: &mut Command) -> String { "{command:?} failed: {}", String::from_utf8_lossy(&output.stderr) ); - String::from_utf8(output.stdout).expect("tool output is UTF-8") } diff --git a/fuzz/corpus/attach/cross-copy b/fuzz/corpus/attach/cross-copy index 50b82ef..1a1f454 100644 Binary files a/fuzz/corpus/attach/cross-copy and b/fuzz/corpus/attach/cross-copy differ diff --git a/fuzz/corpus/attach/detach-reattach b/fuzz/corpus/attach/detach-reattach index d029202..27b6ebf 100644 Binary files a/fuzz/corpus/attach/detach-reattach and b/fuzz/corpus/attach/detach-reattach differ diff --git a/fuzz/corpus/attach/everything-at-once b/fuzz/corpus/attach/everything-at-once index 18d75e2..7e543f2 100644 --- a/fuzz/corpus/attach/everything-at-once +++ b/fuzz/corpus/attach/everything-at-once @@ -1,2 +1 @@ --ßCYæÔÃôÀ =wNí ¦�ת-Ï>Ï'l$(«¬Cú[ƒtòz}þ°1¾yÕ4pªÌ{Ò»÷úÏÕü0·¸ºïoUUW–ÕŠqoߦ -”Ks [œX” ´½.É`ךb‚ÚãæD¢®×w¸mh±¸Šn0®q(TZaôLX£ÐEØ¿ÞB* Í~,ÊÕÉ}�_AZÙÿ±£Ç©ùz‚7Èq®�u2=¼³u½µ#-«ô>[,F]ZÓÓXéöÞ‰Qn;]VÃb�ú˜Û?:ù½K4qº•†hΪô “òmÖö'>Å;o’CÀÂ=)ûۅûj4Uú®,L¹œx"diÅ9#»²­#VÌJ|wxëÅ4ÓˆªÇb-~/]´9È/ÉððÈþ|ÿÊ_jQΰ=¶üÜ2Ëÿ†ƒn©*¨s -ˆ \ No newline at end of file +üÃi“s=kmŠ"õ?6òÍǗϳ¨�H†E–Íi.úéd�u4¼3§8õµJ X7­'�(ŠÙqRpØ0\¨Ê¾Ó+1:O@aÄJPÑpo�:G¿yddßiUV®A½'â—+Ƨ»Ošéá3 ÀÞ¤[ 듥õ �ËŠAîÓ+ÅDMæW©éOãq„fîõ­,lOòeLÒÞ1!ÔŸ4ëD‰=’¬Ç3Þ×HŽjži}"SDâ¥áÀ•øÒà•Bai�ˆBfþKeÐöÁ½¯Æ�ù…Œã››«žONÛï›ãnø²Ù5êÓZ5?˜»äºT…T \ No newline at end of file diff --git a/fuzz/corpus/attach/one-attached-correct-key b/fuzz/corpus/attach/one-attached-correct-key index d5bfd1c..7c1aa17 100644 Binary files a/fuzz/corpus/attach/one-attached-correct-key and b/fuzz/corpus/attach/one-attached-correct-key differ diff --git a/fuzz/corpus/attach/one-attached-omitted-key b/fuzz/corpus/attach/one-attached-omitted-key index 91bab47..06d4e5d 100644 Binary files a/fuzz/corpus/attach/one-attached-omitted-key and b/fuzz/corpus/attach/one-attached-omitted-key differ diff --git a/fuzz/corpus/attach/rows-on-both-sides b/fuzz/corpus/attach/rows-on-both-sides index 99f0ab9..506066d 100644 Binary files a/fuzz/corpus/attach/rows-on-both-sides and b/fuzz/corpus/attach/rows-on-both-sides differ diff --git a/fuzz/corpus/attach/two-attached b/fuzz/corpus/attach/two-attached index db9a109..032ba82 100644 Binary files a/fuzz/corpus/attach/two-attached and b/fuzz/corpus/attach/two-attached differ diff --git a/fuzz/corpus/attach/vacuum-attached b/fuzz/corpus/attach/vacuum-attached index c49baa1..bedbed5 100644 Binary files a/fuzz/corpus/attach/vacuum-attached and b/fuzz/corpus/attach/vacuum-attached differ diff --git a/fuzz/corpus/backup/aes256-page-size-mismatch b/fuzz/corpus/backup/aes256-page-size-mismatch index da065d6..6cb1f7e 100644 Binary files a/fuzz/corpus/backup/aes256-page-size-mismatch and b/fuzz/corpus/backup/aes256-page-size-mismatch differ diff --git a/fuzz/corpus/backup/destination-has-content b/fuzz/corpus/backup/destination-has-content index 94ee58c..7222851 100644 Binary files a/fuzz/corpus/backup/destination-has-content and b/fuzz/corpus/backup/destination-has-content differ diff --git a/fuzz/corpus/backup/different-ciphers b/fuzz/corpus/backup/different-ciphers index ee55904..3e88749 100644 Binary files a/fuzz/corpus/backup/different-ciphers and b/fuzz/corpus/backup/different-ciphers differ diff --git a/fuzz/corpus/backup/empty-source b/fuzz/corpus/backup/empty-source index a4cf7a7..ea88887 100644 Binary files a/fuzz/corpus/backup/empty-source and b/fuzz/corpus/backup/empty-source differ diff --git a/fuzz/corpus/backup/no-page-size-set b/fuzz/corpus/backup/no-page-size-set index 3739595..11d3b78 100644 Binary files a/fuzz/corpus/backup/no-page-size-set and b/fuzz/corpus/backup/no-page-size-set differ diff --git a/fuzz/corpus/backup/same-cipher-mismatched-page-size b/fuzz/corpus/backup/same-cipher-mismatched-page-size index 6215e03..e57c338 100644 Binary files a/fuzz/corpus/backup/same-cipher-mismatched-page-size and b/fuzz/corpus/backup/same-cipher-mismatched-page-size differ diff --git a/fuzz/corpus/differential/compat-1 b/fuzz/corpus/differential/compat-1 deleted file mode 100644 index 7603f97..0000000 --- a/fuzz/corpus/differential/compat-1 +++ /dev/null @@ -1,2 +0,0 @@ -5O?즔„Ë´4êÔ2!»ˆrÛ>y -høó°iî�ð„N†‰SEЉ](«1¹º”ŠûoHXƒ\vŽçVs \ No newline at end of file diff --git a/fuzz/corpus/differential/compat-2 b/fuzz/corpus/differential/compat-2 deleted file mode 100644 index 6a04c10..0000000 Binary files a/fuzz/corpus/differential/compat-2 and /dev/null differ diff --git a/fuzz/corpus/differential/compat-3 b/fuzz/corpus/differential/compat-3 deleted file mode 100644 index 07ef936..0000000 --- a/fuzz/corpus/differential/compat-3 +++ /dev/null @@ -1 +0,0 @@ -i:Î ÖõLÔùnÆcÂ!Ío÷+²•Nc’:´Ãi“*+®9åZí4èl#µNT–  °o^€L§ÍiÕ–1æy�Ä 9u¸ªwYÙÉBö¾òGˆKÞåØŽ8˜%1懕-` 0_üYÜÏwFÇØÞ•J›•z�v”RGŸŠv%Óe =ø?l \ No newline at end of file diff --git a/fuzz/corpus/differential/compat-4 b/fuzz/corpus/differential/compat-4 deleted file mode 100644 index 61eaf99..0000000 --- a/fuzz/corpus/differential/compat-4 +++ /dev/null @@ -1,3 +0,0 @@ -…±qòØš­bãÞhþ›þ0� 4yîŽÄ -Ú(ÑF,»�Á¨’Q«Œ2UJrñö€ð€0®¸‘aõQ€åü¼"= -W }¼„ – ™Á®�¯W.þãÿ© ©wš�æåsˆžH&¯ñuwû-_Q-:>.ýÐÚS¼ökì?¨øˆü�±îº4¢Ò^B‰ä¤Üy]wÝptÜÙÈÂ?Ò�”îoÊŸÈmϪS(ã êõ ¬ \ No newline at end of file diff --git a/fuzz/corpus/differential/corruption-present b/fuzz/corpus/differential/corruption-present deleted file mode 100644 index fbea514..0000000 --- a/fuzz/corpus/differential/corruption-present +++ /dev/null @@ -1 +0,0 @@ -˜‹u�ñÛää´Ä×KÐ*sëÆ9GãÈ7÷µ/¶'{Ã�h�Àƒ Û"ëá}ëá£2%FÒtjÚ øb½¦$‰¦·Ïí?]IÙ�ÉOYEEÀ84ž=ubµD0x»@à lšãc/ƒ¡? CÍq¸�Ìà â>jhk¼r®ê=½¡¬!D,Ãõ×lræPAMŒXñš¼÷B±ÍôÉSýq½ · S[` \ No newline at end of file diff --git a/fuzz/corpus/differential/corruption-present-hmac-off b/fuzz/corpus/differential/corruption-present-hmac-off deleted file mode 100644 index e984363..0000000 Binary files a/fuzz/corpus/differential/corruption-present-hmac-off and /dev/null differ diff --git a/fuzz/corpus/differential/corruption-present-hmac-on b/fuzz/corpus/differential/corruption-present-hmac-on deleted file mode 100644 index db20eb5..0000000 Binary files a/fuzz/corpus/differential/corruption-present-hmac-on and /dev/null differ diff --git a/fuzz/corpus/differential/hmac-algorithm-sha1 b/fuzz/corpus/differential/hmac-algorithm-sha1 deleted file mode 100644 index 414977e..0000000 Binary files a/fuzz/corpus/differential/hmac-algorithm-sha1 and /dev/null differ diff --git a/fuzz/corpus/differential/hmac-algorithm-sha256 b/fuzz/corpus/differential/hmac-algorithm-sha256 deleted file mode 100644 index 1ec44a8..0000000 Binary files a/fuzz/corpus/differential/hmac-algorithm-sha256 and /dev/null differ diff --git a/fuzz/corpus/differential/hmac-algorithm-sha512 b/fuzz/corpus/differential/hmac-algorithm-sha512 deleted file mode 100644 index c3226ec..0000000 Binary files a/fuzz/corpus/differential/hmac-algorithm-sha512 and /dev/null differ diff --git a/fuzz/corpus/differential/hmac-default b/fuzz/corpus/differential/hmac-default deleted file mode 100644 index a8004d3..0000000 Binary files a/fuzz/corpus/differential/hmac-default and /dev/null differ diff --git a/fuzz/corpus/differential/hmac-off b/fuzz/corpus/differential/hmac-off deleted file mode 100644 index 7d1ab84..0000000 Binary files a/fuzz/corpus/differential/hmac-off and /dev/null differ diff --git a/fuzz/corpus/differential/hmac-on b/fuzz/corpus/differential/hmac-on deleted file mode 100644 index 00c2c22..0000000 Binary files a/fuzz/corpus/differential/hmac-on and /dev/null differ diff --git a/fuzz/corpus/differential/kdf-default b/fuzz/corpus/differential/kdf-default deleted file mode 100644 index ecb53e3..0000000 --- a/fuzz/corpus/differential/kdf-default +++ /dev/null @@ -1 +0,0 @@ -œìLùôÎê¬]…§(ÑŠG çóÂ×vf‰E‚øè3‹­ y0tfí§¤ÕBH>ø>7·²Fs»Ìk—wWu6£D\ÇÂS—½däµ d÷Ez£ñމG[–T\}§G \ No newline at end of file diff --git a/fuzz/corpus/differential/kdf-sha1 b/fuzz/corpus/differential/kdf-sha1 deleted file mode 100644 index b7e56a3..0000000 Binary files a/fuzz/corpus/differential/kdf-sha1 and /dev/null differ diff --git a/fuzz/corpus/differential/kdf-sha256 b/fuzz/corpus/differential/kdf-sha256 deleted file mode 100644 index 1cd5753..0000000 --- a/fuzz/corpus/differential/kdf-sha256 +++ /dev/null @@ -1 +0,0 @@ -þ7âe{j¡¡Ùþ–I1s—wZ�mº�Y¯›…³zO:Á 滎~…Ã;„a�·‡®ÅàñpN„"ÄòÅZ.ˆwQ \ No newline at end of file diff --git a/fuzz/corpus/differential/kdf-sha512 b/fuzz/corpus/differential/kdf-sha512 deleted file mode 100644 index df59321..0000000 --- a/fuzz/corpus/differential/kdf-sha512 +++ /dev/null @@ -1,3 +0,0 @@ - ƒ?äÿRSÔÖ$éáÅ ¦‰�Ï*ob>|^ïEÑxŸ~W -ߟgÙX49Ø—ÎRÜž—"¹ìdO§ÿa{–¤B: Zuî=H¼èNêïüþKäámi>—M³ô9ÿWPÒ®^—ZS¼�öÞ¢Ïä4xƒú²¼øfëy:é2 -¢7­2‹›Ûº{FìAg‡}�] \ No newline at end of file diff --git a/fuzz/corpus/differential/key-passphrase b/fuzz/corpus/differential/key-passphrase deleted file mode 100644 index 0013c8d..0000000 Binary files a/fuzz/corpus/differential/key-passphrase and /dev/null differ diff --git a/fuzz/corpus/differential/key-raw b/fuzz/corpus/differential/key-raw deleted file mode 100644 index 5248770..0000000 Binary files a/fuzz/corpus/differential/key-raw and /dev/null differ diff --git a/fuzz/corpus/differential/key-raw-with-salt-and-header b/fuzz/corpus/differential/key-raw-with-salt-and-header deleted file mode 100644 index a0f9122..0000000 Binary files a/fuzz/corpus/differential/key-raw-with-salt-and-header and /dev/null differ diff --git a/fuzz/corpus/differential/key-raw-with-salt-no-header b/fuzz/corpus/differential/key-raw-with-salt-no-header deleted file mode 100644 index fb540f2..0000000 Binary files a/fuzz/corpus/differential/key-raw-with-salt-no-header and /dev/null differ diff --git a/fuzz/corpus/differential/many-ops b/fuzz/corpus/differential/many-ops deleted file mode 100644 index 3816278..0000000 Binary files a/fuzz/corpus/differential/many-ops and /dev/null differ diff --git a/fuzz/corpus/differential/page-size-512 b/fuzz/corpus/differential/page-size-512 deleted file mode 100644 index fe32b76..0000000 Binary files a/fuzz/corpus/differential/page-size-512 and /dev/null differ diff --git a/fuzz/corpus/differential/page-size-65536 b/fuzz/corpus/differential/page-size-65536 deleted file mode 100644 index 3ff27b2..0000000 --- a/fuzz/corpus/differential/page-size-65536 +++ /dev/null @@ -1,2 +0,0 @@ -Ù+¹«_?ég)\Ýl‘¹¤ÉÜNý•¯dÞt/!jy–EòžHk*¡{ÖëYµu³ƒ5cáÎØ‚¥>.w�Ã3)O:so™mñO�5‹†Ï^2e3còy¦DJ4M6,·°b¥ŽóŒt„µ-¯‡×ÆFš?&37‘[;ênsT§ÙÅ ¼‚©…O¦ z}Ryßl…%Õzè)ÏÂT�ƒ€¿7:e4_g EoTøh±pVH*ÀúìÌG;~eD7öª“„ªÒ‘g²ÍRuÒ˜©µqÑ¥9xüí{³æJÄ•p²-õ ÚiEêÜ3Ú_õVµý<íê µ R£dˆhddQ1v]pÄ*õ -ÈË>.ˆ_¢˜OÀÕ„dá)Ñ \ No newline at end of file diff --git a/fuzz/corpus/differential/page-size-default b/fuzz/corpus/differential/page-size-default deleted file mode 100644 index b5b18a9..0000000 Binary files a/fuzz/corpus/differential/page-size-default and /dev/null differ diff --git a/fuzz/corpus/differential/writer-sqlcipher b/fuzz/corpus/differential/writer-sqlcipher deleted file mode 100644 index cd6575a..0000000 --- a/fuzz/corpus/differential/writer-sqlcipher +++ /dev/null @@ -1 +0,0 @@ -à2h�ѪìòÎD$E‘j&Œräí8;išÈ°©“VÔ܃ÖÞD±F'G÷øqš'#O¦œ<_\ƒƲ9�-Þj)1†¤k:°;¨Û´˜ÞåÁ—¾9ý¸ñË2KT+ a1¼Öu@':ÉÙIAÔ&i@J]…ïÆŸ//ÞxÒï@JòïÙo- –ñG@ \ No newline at end of file diff --git a/fuzz/corpus/differential/writer-sqlite3mc b/fuzz/corpus/differential/writer-sqlite3mc deleted file mode 100644 index ce5df11..0000000 Binary files a/fuzz/corpus/differential/writer-sqlite3mc and /dev/null differ diff --git a/fuzz/corpus/findings/backup-aes128-legacy-page-size-below-legacy-page-size b/fuzz/corpus/findings/backup-aes128-legacy-page-size-below-legacy-page-size deleted file mode 100644 index b148d50..0000000 Binary files a/fuzz/corpus/findings/backup-aes128-legacy-page-size-below-legacy-page-size and /dev/null differ diff --git a/fuzz/corpus/findings/backup-aes128-legacy-small-page-short-read b/fuzz/corpus/findings/backup-aes128-legacy-small-page-short-read deleted file mode 100644 index 04c9ac3..0000000 Binary files a/fuzz/corpus/findings/backup-aes128-legacy-small-page-short-read and /dev/null differ diff --git a/fuzz/corpus/findings/backup-guard-bypass-aes128-aegis b/fuzz/corpus/findings/backup-guard-bypass-aes128-aegis deleted file mode 100644 index b3dd33e..0000000 Binary files a/fuzz/corpus/findings/backup-guard-bypass-aes128-aegis and /dev/null differ diff --git a/fuzz/corpus/findings/backup-guard-bypass-cross-cipher b/fuzz/corpus/findings/backup-guard-bypass-cross-cipher deleted file mode 100644 index ca9273e..0000000 Binary files a/fuzz/corpus/findings/backup-guard-bypass-cross-cipher and /dev/null differ diff --git a/fuzz/corpus/findings/backup-rc4-small-page-short-read b/fuzz/corpus/findings/backup-rc4-small-page-short-read deleted file mode 100644 index 943b561..0000000 --- a/fuzz/corpus/findings/backup-rc4-small-page-short-read +++ /dev/null @@ -1,3 +0,0 @@ -@Â�·žÕjìxã¹––Ò ´á|Ž -1\Âì9h® &€4�áëê�Ì¢Nx ¥ƒ*‡nÂ�·žÕjìxã¹––Ò ´á|Ž -PJ[áº;iÑå2�'Z Ú .|¿W¦ŒF½z sýj�¦)yIÏúÛÜO$ÃéHªáìD \ No newline at end of file diff --git a/fuzz/corpus/findings/shared-cache-many-siblings b/fuzz/corpus/findings/shared-cache-many-siblings deleted file mode 100644 index 261a678..0000000 Binary files a/fuzz/corpus/findings/shared-cache-many-siblings and /dev/null differ diff --git a/fuzz/corpus/findings/shared-cache-one-sibling-wrong-key b/fuzz/corpus/findings/shared-cache-one-sibling-wrong-key deleted file mode 100644 index da7883b..0000000 --- a/fuzz/corpus/findings/shared-cache-one-sibling-wrong-key +++ /dev/null @@ -1,2 +0,0 @@ -õОL£ŒükfJ~–Ë»|(¦ óŸ‰ÎĈۓ‡o ÆI)pP<–�Ãoþ�•ºé ? ‰k™RÇ%à³2ª4§øŸ -•¼ »ZI‡'hjC˜ªŽÈêØ÷DkΊ¨P¸Mt4Ìä}�,óÐù’”fÙ�†d^1¹Œ/ 6ä_¹Õ@ô9I�_QL \ No newline at end of file diff --git a/fuzz/corpus/findings/shared-cache-sibling-has-ops b/fuzz/corpus/findings/shared-cache-sibling-has-ops deleted file mode 100644 index fe7f32a..0000000 Binary files a/fuzz/corpus/findings/shared-cache-sibling-has-ops and /dev/null differ diff --git a/fuzz/corpus/findings/shared-cache-wrong-key-sibling-then-vacuum b/fuzz/corpus/findings/shared-cache-wrong-key-sibling-then-vacuum deleted file mode 100644 index ccf5ad6..0000000 Binary files a/fuzz/corpus/findings/shared-cache-wrong-key-sibling-then-vacuum and /dev/null differ diff --git a/fuzz/corpus/findings/vacuum-65536-page-size-corrupt b/fuzz/corpus/findings/vacuum-65536-page-size-corrupt deleted file mode 100644 index 5fe991b..0000000 Binary files a/fuzz/corpus/findings/vacuum-65536-page-size-corrupt and /dev/null differ diff --git a/fuzz/corpus/findings/wal-aes256-legacy-vacuum b/fuzz/corpus/findings/wal-aes256-legacy-vacuum deleted file mode 100644 index 6849ab5..0000000 --- a/fuzz/corpus/findings/wal-aes256-legacy-vacuum +++ /dev/null @@ -1 +0,0 @@ -Œ³p8)Þ<ÕÄÿ–÷`è÷©Óer^ERçŒLš*”âÿ90]Óäe÷raw:sq4¢ g ÿÿÿd–/]†Ü(,Ë]]bZK¦ \ No newline at end of file diff --git a/fuzz/corpus/native/aegis-128 b/fuzz/corpus/native/aegis-128 index 1013b4f..ebefbb0 100644 Binary files a/fuzz/corpus/native/aegis-128 and b/fuzz/corpus/native/aegis-128 differ diff --git a/fuzz/corpus/native/aegis-256 b/fuzz/corpus/native/aegis-256 index 1482370..6e766d5 100644 --- a/fuzz/corpus/native/aegis-256 +++ b/fuzz/corpus/native/aegis-256 @@ -1,2 +1 @@ -þKþ¿s;¢5å“Y -¿qwÐ’/°…' ô}8séâ�¡õ›H/ă°¢J÷Z +¯P†£®›6W¡ŠZ15Œ¡,”¨¿=P]WÏãY9m{ƒ¼)Ø+™ØWÓ�ú@<ŠäWEú \ No newline at end of file +ÀÚÛó«ídç0µîëê  3…®0‰Tš­³´”ÿ‘a+Qy‹9ƒ&¦�­­À[Àgh\!ÁybROÊÛ¤MQô­pÒ©|¿ùθF¶ë$äÛsDKÖ‹,ñ½> .û3Ý9\ü‚¤d.¤È?ï®æ ‚Ýï'蘀¡Eçå>Sw}([å‚á \ No newline at end of file diff --git a/fuzz/corpus/native/cipher-aegis b/fuzz/corpus/native/cipher-aegis index c7b7a1f..57f78de 100644 Binary files a/fuzz/corpus/native/cipher-aegis and b/fuzz/corpus/native/cipher-aegis differ diff --git a/fuzz/corpus/native/cipher-aes256 b/fuzz/corpus/native/cipher-aes256 index 7efeac8..f28e58a 100644 Binary files a/fuzz/corpus/native/cipher-aes256 and b/fuzz/corpus/native/cipher-aes256 differ diff --git a/fuzz/corpus/native/cipher-ascon128 b/fuzz/corpus/native/cipher-ascon128 index a032cf7..ddb3310 100644 Binary files a/fuzz/corpus/native/cipher-ascon128 and b/fuzz/corpus/native/cipher-ascon128 differ diff --git a/fuzz/corpus/native/cipher-chacha20 b/fuzz/corpus/native/cipher-chacha20 index f6e585c..da65038 100644 Binary files a/fuzz/corpus/native/cipher-chacha20 and b/fuzz/corpus/native/cipher-chacha20 differ diff --git a/fuzz/corpus/native/cipher-rc4 b/fuzz/corpus/native/cipher-rc4 deleted file mode 100644 index c3a1dfd..0000000 Binary files a/fuzz/corpus/native/cipher-rc4 and /dev/null differ diff --git a/fuzz/corpus/native/cipher-sqlcipher b/fuzz/corpus/native/cipher-sqlcipher new file mode 100644 index 0000000..53b62ee Binary files /dev/null and b/fuzz/corpus/native/cipher-sqlcipher differ diff --git a/fuzz/corpus/native/damage-present-authenticated b/fuzz/corpus/native/damage-present-authenticated index 035f49d..e36493e 100644 Binary files a/fuzz/corpus/native/damage-present-authenticated and b/fuzz/corpus/native/damage-present-authenticated differ diff --git a/fuzz/corpus/native/damage-present-unauthenticated b/fuzz/corpus/native/damage-present-unauthenticated index 923995e..c3ae8f0 100644 Binary files a/fuzz/corpus/native/damage-present-unauthenticated and b/fuzz/corpus/native/damage-present-unauthenticated differ diff --git a/fuzz/corpus/native/key-passphrase b/fuzz/corpus/native/key-passphrase index 8bbe5d0..703ae15 100644 Binary files a/fuzz/corpus/native/key-passphrase and b/fuzz/corpus/native/key-passphrase differ diff --git a/fuzz/corpus/native/key-raw b/fuzz/corpus/native/key-raw index bbf5d8d..c8f7e03 100644 Binary files a/fuzz/corpus/native/key-raw and b/fuzz/corpus/native/key-raw differ diff --git a/fuzz/corpus/native/key-raw-with-salt b/fuzz/corpus/native/key-raw-with-salt index 1907dbd..153401c 100644 --- a/fuzz/corpus/native/key-raw-with-salt +++ b/fuzz/corpus/native/key-raw-with-salt @@ -1 +1,3 @@ -ŽÁÒ·÷º¯^#!ËÊí ‰n¤lÅ¢ HÚËD¢2™{�.‰¼»"µqkDõ9& ¡û³ìÁµ#ƒðA*W�f/’r7 ¬hžaâ¿Ö¿§¹]÷·ß–�%ÊGjòÎ(Z«° \ No newline at end of file +Oç.žT±4[8�:c¡[F˜×cÏôÈ»>D’Ük7§êÏ·€_Z™bÉ·rà4¡”8ïð +ÍñÍýÁÜykQc \ÄæQI/¥#»ça¦ö�˜†ˆeæ3Ý5Ó¦·ƒqΆšáâ¿“TO%€©œÿáäyØÐ*ðµåo­ÚaÖkÙ\„‰ÝþEðÂBêp=ÙDÌjyÓίé{»ïÐGé+Þš‡ó´Møg>XôÅE~CFWT)s™^Œ¯»_–µ¤ÔrYGï]ˆ,�Ãçý¾\"^”ªíˆ2úßyJT:9Jú1®1˜*vôã»í}æJ…ŽË9šf¥i#J7 Jú.g Œ€ÜÛé¶eÜ郷„‰ÌBõ= +AH(z ¨=‚De\\Û´ºyoBx_’µ(è°¡FªÏópÑ9yT9�Cc¢…K>m¹ô}àåi*޹¸¤ÕíMž;\ÈäQŠOÀH ±¯Òv­Š§¨ \ No newline at end of file diff --git a/fuzz/corpus/native/legacy-set b/fuzz/corpus/native/legacy-set deleted file mode 100644 index 0a2d2c6..0000000 Binary files a/fuzz/corpus/native/legacy-set and /dev/null differ diff --git a/fuzz/corpus/native/page-size-set b/fuzz/corpus/native/page-size-set deleted file mode 100644 index 538ff03..0000000 --- a/fuzz/corpus/native/page-size-set +++ /dev/null @@ -1 +0,0 @@ -ä3sÀƒ„0š{Ì BàdáGƒõü¶�ÛÌçÀGÚ‚ xíN²Â+o&?\«Ï±ßÄšø‘EÌCå>#+qDPóñ¤ÈÚײw`ó‹yYlQ·„ÊÂ�€>Â"”‹6y¢{¯â$‹ºÓØtxj û7²x&º%Y Ûû_PaCUÇmÒá®ÂùE‰/ôáÊrZxni'`_ˆFá°-mùŸºø§j¼… pÖÐ÷Ç–ñk1‡à…â)Iþ$dÇ�ïèÓÑÝÝIõñQh„®Ý}µ \ No newline at end of file diff --git a/fuzz/corpus/native/plaintext-header-present b/fuzz/corpus/native/plaintext-header-present index f88f36d..45e470c 100644 Binary files a/fuzz/corpus/native/plaintext-header-present and b/fuzz/corpus/native/plaintext-header-present differ diff --git a/fuzz/corpus/native/sqlcipher-hmac-off b/fuzz/corpus/native/sqlcipher-hmac-off new file mode 100644 index 0000000..30607fc Binary files /dev/null and b/fuzz/corpus/native/sqlcipher-hmac-off differ diff --git a/fuzz/corpus/native/sqlcipher-hmac-sha1 b/fuzz/corpus/native/sqlcipher-hmac-sha1 new file mode 100644 index 0000000..8959257 Binary files /dev/null and b/fuzz/corpus/native/sqlcipher-hmac-sha1 differ diff --git a/fuzz/corpus/native/sqlcipher-sha1-damage b/fuzz/corpus/native/sqlcipher-sha1-damage new file mode 100644 index 0000000..99f4f13 Binary files /dev/null and b/fuzz/corpus/native/sqlcipher-sha1-damage differ diff --git a/fuzz/corpus/rekey/change-key b/fuzz/corpus/rekey/change-key index afd6c65..3d707b7 100644 Binary files a/fuzz/corpus/rekey/change-key and b/fuzz/corpus/rekey/change-key differ diff --git a/fuzz/corpus/rekey/cipher-aegis b/fuzz/corpus/rekey/cipher-aegis index f51592e..1b16ec2 100644 --- a/fuzz/corpus/rekey/cipher-aegis +++ b/fuzz/corpus/rekey/cipher-aegis @@ -1 +1 @@ -JeEã<‹uéκm¡/ò·7¸8ºjîr#ãÅ¥óžÁÌzÝ‹šîH¢½’JñáO‡/ƒ©Ö¢8¿�m÷“h   \ No newline at end of file +4§8ëëÑÓ y°bC·|į+æêž¯wë¥� Ðs^3…  ˜§CNÀB%j…x'ÂqK¿ø°ŽTÇ¡lµ±ÄÒ$I}ü/mqUô€`û‰‹·Ô%‡Z‘—e&g¡1WÜ^sZ¦@� \ No newline at end of file diff --git a/fuzz/corpus/rekey/cipher-aes128 b/fuzz/corpus/rekey/cipher-aes128 index b9799f5..e443a50 100644 Binary files a/fuzz/corpus/rekey/cipher-aes128 and b/fuzz/corpus/rekey/cipher-aes128 differ diff --git a/fuzz/corpus/rekey/cipher-aes256 b/fuzz/corpus/rekey/cipher-aes256 index 3f85d74..295ef7c 100644 Binary files a/fuzz/corpus/rekey/cipher-aes256 and b/fuzz/corpus/rekey/cipher-aes256 differ diff --git a/fuzz/corpus/rekey/cipher-ascon128 b/fuzz/corpus/rekey/cipher-ascon128 index 58f37f7..28c3873 100644 --- a/fuzz/corpus/rekey/cipher-ascon128 +++ b/fuzz/corpus/rekey/cipher-ascon128 @@ -1 +1,2 @@ -+ÓsÒȽ]™% ¡:œ�<ÔÀt±e‹’5L~æˆõçñ�ǘJc.2Ô´r±�%h(×¥š»&‡T;zŒ¯¶Ð Àœ ±‚'ìD¬,XU…­ˆ+oÖŠ‘ž¬Áçß}$µ\vò»OBæ}­áý÷ÚTW, óÝ \ No newline at end of file +¨ÿ¾UQ‘Øc5E¡¢,ÿ“{ ¯Vñ Àì.Å#X9à¼Ç_þþé”0”Æ,øõ éhb›çÒÆ/6­P:ÔÉ^QÃÈ¡#¤+¶W]ϹxœbŽ2,ïýœLY>fç[ðüØš oÂ3È îï±ÊhŒ¹ÒRŸ«ž¯šœŸDÑ%û +tÇE>ê½ÎS7pžƒ�jPü±6•b«‘È0øƒ‚Þl÷·´ïW#Â�$53“ÁÀHäÀ-Ú”9½º7a¦}6°c½ÁýW}�àŸŽNû}ÿüŸ”DeHödžzæ�ߟâM!;Wá÷™1R櫜D�‘Ú-(¯Ú!~p ä”@^«W?÷dɲ“®ù¤Tä�ƒGÿlçØFt§Áp°|î‰UÅ èuˆ”G;¨p›@lás¨=îÎ]œ±lÃ6(À–<;qpÒrµÄ¾þ™Â5ý÷C °ö’aZ]$çÂ|íÄ@øˆì¦ùÏ€œ´H“0¦äw�wf»>R›ÃFàP,Ë~àk|l„€É.܈�Pb1ñK B’Ñ`X„ivår!è½A�ƒäUO@t‚ÆR 2Ö·Á£sd �|0âTÌ`!º–gŸõuKyuq™YÜwz 3ëù‰F+ƒ¤$†eq…èòÁ�¥�ÖËñš�¨þ»»„†q›E'º8ìñS‡9´5±ó>ÂTËyr}0†ìÏ\páŸÎ甘,7¹­S¶au~_™½÷Ö‘¨nŽî”«™vdõÖg>ÜÝ~Œ­2ë¼ò‡¬_Æ#c© '…T¶‰`×÷™¨àè�ÍÝ'— ¼_© hÿxw'¾Ürpcù1¯‘Òn“*{쫌ƒm¤¯ékm'ÁH” \ No newline at end of file diff --git a/fuzz/corpus/rekey/cipher-chacha20 b/fuzz/corpus/rekey/cipher-chacha20 index 19b8b30..a639b68 100644 Binary files a/fuzz/corpus/rekey/cipher-chacha20 and b/fuzz/corpus/rekey/cipher-chacha20 differ diff --git a/fuzz/corpus/rekey/cipher-rc4 b/fuzz/corpus/rekey/cipher-rc4 deleted file mode 100644 index 267f5d8..0000000 --- a/fuzz/corpus/rekey/cipher-rc4 +++ /dev/null @@ -1,3 +0,0 @@ -!¶Ó§âq¡+>pÆHÂ…ÿ€§‘á¦:ª-þ�Å9‹'½²ÂÁ–Œ/T!‡Ê³cüˆ�Þ`xó·(€Q6¶#ך©–ºe@�–"ž9C`¯Ap2­Vi­m2’T£Ó4E¡!#œÃTMÍ�‡dr«¸#\dV¦¿g:µZ¥¨\P¡4×Õ¶"€ïýíÍz¬eÅ-šg�–¥é:5Úo-ÆMºäŒ¬ê÷†™�OWzñ�6ºÐÁ¬ùÃa(g²Ûˆ% º UI›Gü|ãJ²¥ÂÙQð¹nšÔe)¥Ù0Øa•�ð=v?Æúd¤¡ |ÞÏ-ÛLV¿rÌnhgšl÷¾ð{b–�¨Ci*hwM%©—SfˆìPêw±Ât¦°gŽw�…Æ´$"rfñ #õÌö¸2·ö;§> ­ÛX+O¬[i"I1Ѷ|1óh!åEOp†î�¬FeƒúB×Ú…làe Œf©©»ØTfÓ\Wñ.š²ÒhLü¹¹È@ßA׊ô1ð{AÁ©­ÌQNȧ‡zUòâ#/ -0'¾Û¥�oû¿£æÁh:à/¯Q +ŠNd‰š7ñ%{_•J¿ò>ߺ]¬eÉÙˆ4ñç‰æ™ñNVz?�áÑÜæö1ÅÌ&Ù?1è#úšâ9jžJ·9ëR§V|»9ÚÓ¦Š}þÆIFdGN¡Ì4妰g¸«AT&½‡Ø‡Kœ�ÖFGZÔ(«'V‡Á­Vソ�†XJ]Ñ#),ð^FïuGs§ zKï~ܬ\…´Øß«£Ö¦ZÌ!^‘@&'V4G]…nÿD‘ÅN D@ož,‚œÉ!­‡òhÆÇ/Õæå+<�oÂE‹ ßöVÓìÐ-)Rã -ªèü(‹¨®xŸ@Á(a§Wè 4tv2�ž=ƒä¯�än(™èN¥XŠª­+ìêü‹æYZ^}g}±¿5“‡s/~.ÐCƌΕÆÓ¸¯ÈtzÊ€TNÞ�˺�Öu»k<Ý‹_:w=‚®xê¬5³IÚ˜‡‰�—²Þ´é�ZÍkŠëp‡ \ No newline at end of file diff --git a/fuzz/corpus/rekey/cipher-sqlcipher b/fuzz/corpus/rekey/cipher-sqlcipher new file mode 100644 index 0000000..bc3e212 --- /dev/null +++ b/fuzz/corpus/rekey/cipher-sqlcipher @@ -0,0 +1 @@ +ˆ�€ªÑã©&”îGטðY„üJU&É¹ÍæC¬Í›"õ›,Än HFãCzD@ØÚ¨ì·Õ^ïàšQºi(°¼’Þò�(çGðxYƒªÔ%E;ݵu«ûÂ>x†ÕR�),¯5"¾œ \ No newline at end of file diff --git a/fuzz/corpus/rekey/decrypt b/fuzz/corpus/rekey/decrypt index 4569bc0..5171acc 100644 --- a/fuzz/corpus/rekey/decrypt +++ b/fuzz/corpus/rekey/decrypt @@ -1 +1,2 @@ -ÿäüÖ»æÛWÌ|\ S›.nøŒJ²l.?> ßý—¢‡6™ö™ÈK­RþÏÝùKo%Ï�¾Ã…¡”R"Ú2ˆt3œA‚çõV|÷Ö;oöQݫ䜹ºt1`}Æ· ¡Oi÷G¯¯ \ No newline at end of file +î ³ &u3èŠç‡õj +œ,Ò(•ÏT“Jç"IG:ð2åG1¬»‹�ÍJÂf&Q8»;c¨ßL­ª"Ãlíï|òJÛ&/Ùqv:š™@(2ô/-øG¬EÐäs9�« Þ \ No newline at end of file diff --git a/fuzz/corpus/rekey/legacy-set b/fuzz/corpus/rekey/legacy-set deleted file mode 100644 index 45845ec..0000000 Binary files a/fuzz/corpus/rekey/legacy-set and /dev/null differ diff --git a/fuzz/corpus/rekey/many-ops-before b/fuzz/corpus/rekey/many-ops-before index 051d62f..b03ef39 100644 Binary files a/fuzz/corpus/rekey/many-ops-before and b/fuzz/corpus/rekey/many-ops-before differ diff --git a/fuzz/corpus/rekey/ops-after-empty b/fuzz/corpus/rekey/ops-after-empty index 2a793ec..29a0c1e 100644 Binary files a/fuzz/corpus/rekey/ops-after-empty and b/fuzz/corpus/rekey/ops-after-empty differ diff --git a/fuzz/corpus/rekey/ops-after-present b/fuzz/corpus/rekey/ops-after-present index 89fe11c..d016eeb 100644 Binary files a/fuzz/corpus/rekey/ops-after-present and b/fuzz/corpus/rekey/ops-after-present differ diff --git a/fuzz/corpus/rekey/plaintext-header-present b/fuzz/corpus/rekey/plaintext-header-present index f4f5b3f..0114177 100644 Binary files a/fuzz/corpus/rekey/plaintext-header-present and b/fuzz/corpus/rekey/plaintext-header-present differ diff --git a/fuzz/corpus/shared_cache/all-siblings-correct-key b/fuzz/corpus/shared_cache/all-siblings-correct-key index b4ecc8c..471ffcd 100644 Binary files a/fuzz/corpus/shared_cache/all-siblings-correct-key and b/fuzz/corpus/shared_cache/all-siblings-correct-key differ diff --git a/fuzz/corpus/shared_cache/many-siblings b/fuzz/corpus/shared_cache/many-siblings index 0299eb4..510ffd4 100644 Binary files a/fuzz/corpus/shared_cache/many-siblings and b/fuzz/corpus/shared_cache/many-siblings differ diff --git a/fuzz/corpus/shared_cache/mixed-correct-and-wrong b/fuzz/corpus/shared_cache/mixed-correct-and-wrong index cc9a9a8..c4d9adb 100644 Binary files a/fuzz/corpus/shared_cache/mixed-correct-and-wrong and b/fuzz/corpus/shared_cache/mixed-correct-and-wrong differ diff --git a/fuzz/corpus/shared_cache/no-siblings b/fuzz/corpus/shared_cache/no-siblings index 6e7caa0..f3c7c60 100644 --- a/fuzz/corpus/shared_cache/no-siblings +++ b/fuzz/corpus/shared_cache/no-siblings @@ -1 +1,2 @@ -ž]z¸×�§�8ǃ»¶#œ"´%;§@˜�€RPÒ4$«¡%Sf¹¡ûßLî«Ê˜Åi’ÕrБÜý‚½-5¹Ù¥åÄâÞºj ðZ{{"ÇÌc벇ˆ2¹”¥ü`¸ï”z&YïŒgɾ \ No newline at end of file +]ÄÙ +רèí]xÁr‰40Å~“�bÿ £)“N˜l “Aþ·Öäò@àt®tF§òÒ!Y«™°^‹·•PtÒù \ No newline at end of file diff --git a/fuzz/corpus/shared_cache/one-sibling-correct-key b/fuzz/corpus/shared_cache/one-sibling-correct-key index 18df2cb..4b4ffdb 100644 Binary files a/fuzz/corpus/shared_cache/one-sibling-correct-key and b/fuzz/corpus/shared_cache/one-sibling-correct-key differ diff --git a/fuzz/corpus/shared_cache/one-sibling-wrong-key b/fuzz/corpus/shared_cache/one-sibling-wrong-key index 75e9526..4226697 100644 Binary files a/fuzz/corpus/shared_cache/one-sibling-wrong-key and b/fuzz/corpus/shared_cache/one-sibling-wrong-key differ diff --git a/fuzz/corpus/shared_cache/sibling-has-ops b/fuzz/corpus/shared_cache/sibling-has-ops index 54b0126..da2da4f 100644 Binary files a/fuzz/corpus/shared_cache/sibling-has-ops and b/fuzz/corpus/shared_cache/sibling-has-ops differ diff --git a/fuzz/corpus/wal/checkpoint-full b/fuzz/corpus/wal/checkpoint-full index 709acb6..ae71b50 100644 Binary files a/fuzz/corpus/wal/checkpoint-full and b/fuzz/corpus/wal/checkpoint-full differ diff --git a/fuzz/corpus/wal/checkpoint-none b/fuzz/corpus/wal/checkpoint-none index f6afba1..9b01902 100644 Binary files a/fuzz/corpus/wal/checkpoint-none and b/fuzz/corpus/wal/checkpoint-none differ diff --git a/fuzz/corpus/wal/checkpoint-passive b/fuzz/corpus/wal/checkpoint-passive index d2bc0db..e55615d 100644 Binary files a/fuzz/corpus/wal/checkpoint-passive and b/fuzz/corpus/wal/checkpoint-passive differ diff --git a/fuzz/corpus/wal/checkpoint-restart b/fuzz/corpus/wal/checkpoint-restart index 861691c..41839bf 100644 Binary files a/fuzz/corpus/wal/checkpoint-restart and b/fuzz/corpus/wal/checkpoint-restart differ diff --git a/fuzz/corpus/wal/checkpoint-truncate b/fuzz/corpus/wal/checkpoint-truncate index 0d0ded2..e7f2bd2 100644 Binary files a/fuzz/corpus/wal/checkpoint-truncate and b/fuzz/corpus/wal/checkpoint-truncate differ diff --git a/fuzz/corpus/wal/cipher-aegis b/fuzz/corpus/wal/cipher-aegis index 8ea709f..5455ff7 100644 Binary files a/fuzz/corpus/wal/cipher-aegis and b/fuzz/corpus/wal/cipher-aegis differ diff --git a/fuzz/corpus/wal/cipher-aes128 b/fuzz/corpus/wal/cipher-aes128 index 292323f..dffc4f3 100644 Binary files a/fuzz/corpus/wal/cipher-aes128 and b/fuzz/corpus/wal/cipher-aes128 differ diff --git a/fuzz/corpus/wal/cipher-aes256 b/fuzz/corpus/wal/cipher-aes256 index d193f28..8989f91 100644 Binary files a/fuzz/corpus/wal/cipher-aes256 and b/fuzz/corpus/wal/cipher-aes256 differ diff --git a/fuzz/corpus/wal/cipher-ascon128 b/fuzz/corpus/wal/cipher-ascon128 index 8a95629..4ec9943 100644 --- a/fuzz/corpus/wal/cipher-ascon128 +++ b/fuzz/corpus/wal/cipher-ascon128 @@ -1 +1 @@ -Lପ/çVìß5P 훹ÂúךÜ&À õJt};ÊÅ͘�•ƒ¯XO‡’ð]‘Œ #tËs³ßM î³þü( \ No newline at end of file +«=XÁÛ~oÝQ§ÈI ¢¾‹s{̸N]ÆuùiÑV|=¬¾‚мSIšÃ»¢6•Ûó´±+óiTMÑ|hƒikH»Bw´óÂÇGø’òêí„ÉCƒÄã¯@*Nõˆ¤bÓxQ\D \ No newline at end of file diff --git a/fuzz/corpus/wal/cipher-chacha20 b/fuzz/corpus/wal/cipher-chacha20 index 26a5a69..86ca0e0 100644 Binary files a/fuzz/corpus/wal/cipher-chacha20 and b/fuzz/corpus/wal/cipher-chacha20 differ diff --git a/fuzz/corpus/wal/cipher-rc4 b/fuzz/corpus/wal/cipher-rc4 deleted file mode 100644 index 4c9f638..0000000 --- a/fuzz/corpus/wal/cipher-rc4 +++ /dev/null @@ -1,4 +0,0 @@ -ŸœxÇÚ¢ÀØ™ÿ¿ã„îþtaÖžf%ŽÆ=DÖí{ýß¼·²™’,Sp@ëßgHSFˆ·AÇCòÓ'â“f¶•Fùqx½9ímD‹©¢í¼7n‡Ø†MÅ4ôGô`¿‰Å7¨Ùc”éŸíÓê6Ñ¡4–ôY¹L·¾ÆŸ-W¥ñ æJíè³i…dæÄžCižÀcÖçëÌÍñD¦3(À,Â>7¹T™‹^XàxW Ú4;²’æ�-ݳ‰óÏñô¼)œ’Ó´Á“+Ë¥\1gn[¥Lû‡/FÚ¼hË<¶¬Ÿ¤gß ó¦lqÊ®“ž.|Je¾ÀœéðfR¡z÷€œ‰¥,ÔwE¥æ«™î%ù>7 6Xá†Í2í1¯þ\¿ -qhDŸQþ -Z<p޲&^ÿF êãé…aüî^°\B;B£öê››5gFÝЫ£ëxv\ó˜@èŒc€‰]ý)`úPµ?¥ç׿¶¸éT/z²õPT<ùåá -ê¥FE[ï–ïøØœµˆ…ÆCp ÔµÃØn;WOϕ唋2þÇ­ØSõù#†Ì@z¢=çkòFLÊ+·ÂÄ'¶ä?yÍ­�S¤Ž‰þ5ËŠ(§†B \ No newline at end of file diff --git a/fuzz/corpus/wal/cipher-sqlcipher b/fuzz/corpus/wal/cipher-sqlcipher new file mode 100644 index 0000000..6884e21 Binary files /dev/null and b/fuzz/corpus/wal/cipher-sqlcipher differ diff --git a/fuzz/corpus/wal/legacy-wal-and-truncation b/fuzz/corpus/wal/legacy-wal-and-truncation index 9775b0d..68acc3c 100644 Binary files a/fuzz/corpus/wal/legacy-wal-and-truncation and b/fuzz/corpus/wal/legacy-wal-and-truncation differ diff --git a/fuzz/corpus/wal/legacy-wal-false b/fuzz/corpus/wal/legacy-wal-false index 09b6eb2..487a26a 100644 Binary files a/fuzz/corpus/wal/legacy-wal-false and b/fuzz/corpus/wal/legacy-wal-false differ diff --git a/fuzz/corpus/wal/legacy-wal-true b/fuzz/corpus/wal/legacy-wal-true index e9cc797..d6675d0 100644 Binary files a/fuzz/corpus/wal/legacy-wal-true and b/fuzz/corpus/wal/legacy-wal-true differ diff --git a/fuzz/corpus/wal/many-ops b/fuzz/corpus/wal/many-ops index ac430fb..7847b60 100644 Binary files a/fuzz/corpus/wal/many-ops and b/fuzz/corpus/wal/many-ops differ diff --git a/fuzz/corpus/wal/wal-truncation-absent b/fuzz/corpus/wal/wal-truncation-absent index f43683b..2b7a439 100644 Binary files a/fuzz/corpus/wal/wal-truncation-absent and b/fuzz/corpus/wal/wal-truncation-absent differ diff --git a/fuzz/corpus/wal/wal-truncation-present b/fuzz/corpus/wal/wal-truncation-present index 5daeb8a..cc25128 100644 Binary files a/fuzz/corpus/wal/wal-truncation-present and b/fuzz/corpus/wal/wal-truncation-present differ diff --git a/fuzz/examples/gen_native_seeds.rs b/fuzz/examples/gen_native_seeds.rs index d198cfb..679e717 100644 --- a/fuzz/examples/gen_native_seeds.rs +++ b/fuzz/examples/gen_native_seeds.rs @@ -1,13 +1,13 @@ -//! Builds `corpus/native` the same way `gen_seeds` builds `corpus/differential`: rejection -//! sampling through the real `Arbitrary` impl, keeping the first draw that lands on each target -//! parameter family, after confirming the case runs without panicking. +//! Builds `corpus/native`: rejection sampling through the real `Arbitrary` impl, keeping the +//! first *clean* draw that lands on each target parameter family, after confirming the case +//! runs without panicking. use std::path::Path; use sqlite3mc_src_fuzz::{ run_native, seedgen::{run_search, DivergencePolicy, Family, Rng}, - Cipher, NativeCase, NativeKey, + Algorithm, Cipher, NativeCase, NativeKey, }; const fn has_ops(case: &NativeCase) -> bool { @@ -29,13 +29,22 @@ fn families() -> &'static [Family] { matches: |c| c.config.cipher == Cipher::ChaCha20 && has_ops(c), }, Family { - name: "cipher-rc4", - matches: |c| c.config.cipher == Cipher::Rc4 && has_ops(c), + name: "cipher-sqlcipher", + matches: |c| c.config.cipher == Cipher::SqlCipher && has_ops(c), }, Family { name: "cipher-ascon128", matches: |c| c.config.cipher == Cipher::Ascon128 && has_ops(c), }, + Family { + name: "sqlcipher-sha1-damage", + matches: |c| { + c.config.cipher == Cipher::SqlCipher + && c.config.hmac_algorithm == Some(Algorithm::Sha1) + && !c.damage.is_empty() + && has_ops(c) + }, + }, Family { name: "cipher-aegis", matches: |c| c.config.cipher == Cipher::Aegis && has_ops(c), @@ -57,12 +66,12 @@ fn families() -> &'static [Family] { matches: |c| c.config.plaintext_header_size.is_some_and(|n| n > 0) && has_ops(c), }, Family { - name: "legacy-set", - matches: |c| c.config.legacy.is_some() && has_ops(c), + name: "sqlcipher-hmac-off", + matches: |c| c.config.hmac_use == Some(false) && has_ops(c), }, Family { - name: "page-size-set", - matches: |c| c.config.page_size.is_some() && has_ops(c), + name: "sqlcipher-hmac-sha1", + matches: |c| c.config.hmac_algorithm == Some(Algorithm::Sha1) && has_ops(c), }, Family { name: "aegis-256", @@ -74,15 +83,11 @@ fn families() -> &'static [Family] { }, Family { name: "damage-present-authenticated", - matches: |c| c.config.cipher.authenticated() && !c.damage.is_empty() && has_ops(c), + matches: |c| c.config.authenticated() && !c.damage.is_empty() && has_ops(c), }, Family { name: "damage-present-unauthenticated", - matches: |c| !c.config.cipher.authenticated() && !c.damage.is_empty() && has_ops(c), - }, - Family { - name: "many-ops", - matches: |c| c.ops.len() >= 20, + matches: |c| !c.config.authenticated() && !c.damage.is_empty() && has_ops(c), }, ] } diff --git a/fuzz/examples/gen_rekey_seeds.rs b/fuzz/examples/gen_rekey_seeds.rs index c964ac6..7e5cec3 100644 --- a/fuzz/examples/gen_rekey_seeds.rs +++ b/fuzz/examples/gen_rekey_seeds.rs @@ -32,8 +32,8 @@ fn families() -> &'static [Family] { matches: |c| c.config.cipher == Cipher::ChaCha20 && has_ops(c), }, Family { - name: "cipher-rc4", - matches: |c| c.config.cipher == Cipher::Rc4 && has_ops(c), + name: "cipher-sqlcipher", + matches: |c| c.config.cipher == Cipher::SqlCipher && has_ops(c), }, Family { name: "cipher-ascon128", @@ -63,10 +63,6 @@ fn families() -> &'static [Family] { name: "plaintext-header-present", matches: |c| c.config.plaintext_header_size.is_some_and(|n| n > 0) && has_ops(c), }, - Family { - name: "legacy-set", - matches: |c| c.config.legacy.is_some() && has_ops(c), - }, Family { name: "many-ops-before", matches: |c| c.ops_before.len() >= 10, diff --git a/fuzz/examples/gen_seeds.rs b/fuzz/examples/gen_seeds.rs deleted file mode 100644 index 9dfa54b..0000000 --- a/fuzz/examples/gen_seeds.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Builds `corpus/differential` by rejection sampling: draws random bytes, decodes them through -//! the real `Arbitrary` impl exactly as libFuzzer would, and keeps the first draw that lands on -//! each target parameter family, after confirming the case runs without panicking. One committed -//! seed is therefore guaranteed to decode to a genuine, already-passing case, not a hand-built one -//! the harness never actually sees bytes for. A divergence is itself interesting input, so the -//! first panicking draw per family is kept too, in place, rather than searched past. - -use std::path::Path; - -use sqlite3mc_src_fuzz::{ - run, - seedgen::{run_search, DivergencePolicy, Family, Rng}, - Algorithm, Case, Key, Side, -}; - -const fn has_ops(case: &Case) -> bool { - !case.ops.is_empty() -} - -#[expect( - clippy::too_many_lines, - reason = "a flat table of Family entries, one per matcher, not control flow to split" -)] -fn families() -> &'static [Family] { - &[ - Family { - name: "writer-sqlcipher", - matches: |c| c.writer == Side::SqlCipher && has_ops(c), - }, - Family { - name: "writer-sqlite3mc", - matches: |c| c.writer == Side::Sqlite3mc && has_ops(c), - }, - Family { - name: "compat-1", - matches: |c| c.config.compatibility == 1 && has_ops(c), - }, - Family { - name: "compat-2", - matches: |c| c.config.compatibility == 2 && has_ops(c), - }, - Family { - name: "compat-3", - matches: |c| c.config.compatibility == 3 && has_ops(c), - }, - Family { - name: "compat-4", - matches: |c| c.config.compatibility == 4 && has_ops(c), - }, - Family { - name: "page-size-default", - matches: |c| c.config.page_size.is_none() && has_ops(c), - }, - Family { - name: "page-size-512", - matches: |c| c.config.page_size == Some(512) && has_ops(c), - }, - Family { - name: "page-size-65536", - matches: |c| c.config.page_size == Some(65536) && has_ops(c), - }, - Family { - name: "kdf-default", - matches: |c| c.config.kdf_algorithm.is_none() && has_ops(c), - }, - Family { - name: "kdf-sha1", - matches: |c| c.config.kdf_algorithm == Some(Algorithm::Sha1) && has_ops(c), - }, - Family { - name: "kdf-sha256", - matches: |c| c.config.kdf_algorithm == Some(Algorithm::Sha256) && has_ops(c), - }, - Family { - name: "kdf-sha512", - matches: |c| c.config.kdf_algorithm == Some(Algorithm::Sha512) && has_ops(c), - }, - Family { - name: "hmac-default", - matches: |c| c.config.hmac.is_none() && has_ops(c), - }, - Family { - name: "hmac-off", - matches: |c| c.config.hmac == Some(false) && has_ops(c), - }, - Family { - name: "hmac-on", - matches: |c| c.config.hmac == Some(true) && has_ops(c), - }, - Family { - name: "hmac-algorithm-sha1", - matches: |c| c.config.hmac_algorithm == Some(Algorithm::Sha1) && has_ops(c), - }, - Family { - name: "hmac-algorithm-sha256", - matches: |c| c.config.hmac_algorithm == Some(Algorithm::Sha256) && has_ops(c), - }, - Family { - name: "hmac-algorithm-sha512", - matches: |c| c.config.hmac_algorithm == Some(Algorithm::Sha512) && has_ops(c), - }, - Family { - name: "key-raw", - matches: |c| matches!(c.config.key, Key::Raw(_)) && has_ops(c), - }, - Family { - name: "key-raw-with-salt-no-header", - matches: |c| { - matches!( - c.config.key, - Key::RawWithSalt { - plaintext_header: 0, - .. - } - ) && has_ops(c) - }, - }, - Family { - name: "key-raw-with-salt-and-header", - matches: |c| { - matches!( - c.config.key, - Key::RawWithSalt { - plaintext_header, .. - } if plaintext_header > 0 - ) && has_ops(c) - }, - }, - Family { - name: "key-passphrase", - matches: |c| matches!(c.config.key, Key::Passphrase { .. }) && has_ops(c), - }, - Family { - name: "corruption-present", - matches: |c| !c.damage.is_empty() && has_ops(c), - }, - Family { - name: "corruption-present-hmac-off", - matches: |c| !c.damage.is_empty() && c.config.hmac == Some(false) && has_ops(c), - }, - Family { - name: "corruption-present-hmac-on", - matches: |c| !c.damage.is_empty() && c.config.hmac == Some(true) && has_ops(c), - }, - Family { - name: "many-ops", - matches: |c| c.ops.len() >= 20, - }, - ] -} - -fn main() { - let dir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/differential")); - run_search( - families(), - Rng::new(0x9e37_79b9_7f4a_7c15), - dir, - 6000, - 200_000, - run, - &DivergencePolicy::KeepFirstDraw, - ); -} diff --git a/fuzz/examples/gen_wal_seeds.rs b/fuzz/examples/gen_wal_seeds.rs index 0867cb4..e5d415c 100644 --- a/fuzz/examples/gen_wal_seeds.rs +++ b/fuzz/examples/gen_wal_seeds.rs @@ -1,8 +1,9 @@ //! Builds `corpus/wal` the same way `gen_rekey_seeds` builds `corpus/rekey`: rejection sampling //! through the real `Arbitrary` impl, keeping the first *clean* draw that lands on each target -//! parameter family. A draw that panics is never committed to `corpus/wal` (it would permanently -//! fail the replay test); the first one found per family is saved to `corpus/findings/` instead, -//! which nothing replays, and the search continues past it for a clean representative. +//! parameter family. A draw that panics is never committed to `corpus/wal` (it would +//! permanently fail the replay test); the first one found per family is saved to +//! `corpus/findings/` instead, which nothing replays, and the search continues past it for a +//! clean representative. use std::path::Path; @@ -31,8 +32,8 @@ fn families() -> &'static [Family] { matches: |c| c.config.cipher == Cipher::ChaCha20 && has_ops(c), }, Family { - name: "cipher-rc4", - matches: |c| c.config.cipher == Cipher::Rc4 && has_ops(c), + name: "cipher-sqlcipher", + matches: |c| c.config.cipher == Cipher::SqlCipher && has_ops(c), }, Family { name: "cipher-ascon128", diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs deleted file mode 100755 index 6035142..0000000 --- a/fuzz/fuzz_targets/differential.rs +++ /dev/null @@ -1,3 +0,0 @@ -#![no_main] - -libfuzzer_sys::fuzz_target!(|case: sqlite3mc_src_fuzz::Case| sqlite3mc_src_fuzz::run(&case)); diff --git a/fuzz/sqlcipher-entropy.c b/fuzz/sqlcipher-entropy.c deleted file mode 100644 index b1eaf4e..0000000 --- a/fuzz/sqlcipher-entropy.c +++ /dev/null @@ -1,14 +0,0 @@ -/* SQLCipher's entropy hook, giving its LibTomCrypt RNG the harness's deterministic getentropy. */ -long sqlcipher_wasm_no_clock(void) { abort(); } - -/* Fails closed so rng_get_bytes never falls through to the clock. */ -static unsigned long sqlcipher_wasm_rng(unsigned char *out, unsigned long len, void (*callback)(void)) { - (void)callback; - if (getentropy(out, len) != 0) abort(); - return len; -} - -int sqlcipher_wasm_extra_init(const char *arg) { - ltc_rng = sqlcipher_wasm_rng; - return sqlcipher_extra_init(arg); -} diff --git a/fuzz/sqlcipher-ltc.h b/fuzz/sqlcipher-ltc.h deleted file mode 100644 index 3857e89..0000000 --- a/fuzz/sqlcipher-ltc.h +++ /dev/null @@ -1,29 +0,0 @@ -/* The LibTomCrypt configuration SQLCipher and every libtomcrypt source in the unit share. */ -#ifndef SQLCIPHER_LTC_H -#define SQLCIPHER_LTC_H - -#define LTC_NOTHING -#define LTC_RIJNDAEL -#define LTC_CBC_MODE -#define LTC_SHA1 -#define LTC_SHA256 -#define LTC_SHA512 -#define LTC_HASH_HELPERS -#define LTC_HMAC -#define LTC_FORTUNA -#define LTC_PKCS_5 -#define LTC_RNG_GET_BYTES -#define LTC_PRNG_ENABLE_LTC_RNG -#define LTC_NO_TEST -#define LTC_NO_FILE -#define LTC_NO_ASM -#define LTC_NO_MATH -#define LTC_CLEAN_STACK -/* Bad arguments return CRYPT_INVALID_ARG instead of raising a signal, which the fuzzer would count as a crash. */ -#define ARGTYPE 4 -/* rng_get_bytes always ends in a clock-jitter generator, so reaching it must abort. */ -#define XCLOCK sqlcipher_wasm_no_clock -#define XCLOCKS_PER_SEC 1 -long sqlcipher_wasm_no_clock(void); - -#endif diff --git a/fuzz/sqlcipher-wrapper.c b/fuzz/sqlcipher-wrapper.c deleted file mode 100644 index 5d23bda..0000000 --- a/fuzz/sqlcipher-wrapper.c +++ /dev/null @@ -1,18 +0,0 @@ -/* SQLCipher's configuration, heading the one unit build.rs assembles with libtomcrypt and the entropy hook. */ -#define SQLITE_HAS_CODEC 1 -#define SQLCIPHER_CRYPTO_LIBTOMCRYPT 1 -/* Installs the entropy hook before SQLCipher's own init runs. */ -#define SQLITE_EXTRA_INIT sqlcipher_wasm_extra_init -#define SQLITE_EXTRA_SHUTDOWN sqlcipher_extra_shutdown -/* SQLCipher's log needs a timestamp and a device the harness does not keep. */ -#define SQLCIPHER_OMIT_LOG 1 -#define SQLCIPHER_OMIT_LOG_DEVICE 1 -#define OMIT_MEMLOCK 1 -/* SQLCipher refuses SQLITE_THREADSAFE=0, and the harness runs single-threaded, so no-op mutexes suffice. */ -#undef SQLITE_THREADSAFE -#define SQLITE_THREADSAFE 1 -#define SQLITE_MUTEX_NOOP 1 - -#define LTC_SOURCE -#include "sqlcipher-ltc.h" -#include "sqlcipher.c" diff --git a/fuzz/src/db.rs b/fuzz/src/db.rs index b65efa4..0ad724c 100644 --- a/fuzz/src/db.rs +++ b/fuzz/src/db.rs @@ -1,4 +1,5 @@ //! Safe owners for connections and statements, keeping every raw handle inside this module. +//! Each handle is released by exactly one matching amalgamation call, in its own `Drop`. use core::{ ffi::{c_char, c_int, c_void, CStr}, @@ -7,10 +8,7 @@ use core::{ slice, }; -use crate::ffi::{ - Api, Sqlite3, Stmt, SQLITE_BLOB, SQLITE_DONE, SQLITE_FLOAT, SQLITE_INTEGER, SQLITE_OK, - SQLITE_ROW, SQLITE_TEXT, -}; +use crate::ffi; /// An SQLite primary result code, the low byte of an extended one. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -23,7 +21,7 @@ impl Code { } const fn check(rc: c_int) -> Result<(), Code> { - if rc == SQLITE_OK { + if rc == ffi::SQLITE_OK { Ok(()) } else { Err(Code::of(rc)) @@ -55,37 +53,35 @@ pub(crate) enum Param<'a> { Blob(&'a [u8]), } -/// `sqlite3_enable_shared_cache`: process-wide, not tied to any one connection. Every connection -/// opened after this call defaults to shared-cache mode (`SQLITE_OPEN_PRIVATECACHE` overrides it -/// per connection, not used anywhere in this harness). -pub(crate) fn enable_shared_cache(api: &Api, enable: bool) -> Result<(), Code> { +/// `sqlite3_enable_shared_cache`: process-wide, so every connection opened after the call +/// defaults to shared-cache mode. +pub(crate) fn enable_shared_cache(enable: bool) -> Result<(), Code> { // SAFETY: takes a plain `c_int`, touches no pointers, safe to call at any time. - check(unsafe { (api.enable_shared_cache)(c_int::from(enable)) }) + check(unsafe { ffi::sqlite3_enable_shared_cache(c_int::from(enable)) }) } /// An open connection, closed on drop. -pub(crate) struct Connection<'a> { - api: &'a Api, - db: NonNull, +pub(crate) struct Connection { + db: NonNull, } -impl<'a> Connection<'a> { - pub(crate) fn open(api: &'a Api, path: &CStr, flags: c_int) -> Result { +impl Connection { + pub(crate) fn open(path: &CStr, flags: c_int) -> Result { let mut db = ptr::null_mut(); - // SAFETY: `path` is NUL-terminated and outlives the call, the VFS name is NULL for the - // default, and `db` is a valid place for SQLite to store the new handle or NULL. - let rc = unsafe { (api.open_v2)(path.as_ptr(), &raw mut db, flags, ptr::null()) }; + // SAFETY: `path` is NUL-terminated and outlives the call, the VFS name is NULL for + // the default, and `db` is a valid place for SQLite to store the new handle or NULL. + let rc = unsafe { ffi::sqlite3_open_v2(path.as_ptr(), &raw mut db, flags, ptr::null()) }; let db = NonNull::new(db); match (rc, db) { - (SQLITE_OK, Some(db)) => Ok(Self { api, db }), + (ffi::SQLITE_OK, Some(db)) => Ok(Self { db }), (_, db) => { if let Some(db) = db { // SAFETY: a failed open still returns a handle SQLite requires closing, and // nothing else holds it. - unsafe { (api.close_v2)(db.as_ptr()) }; + unsafe { ffi::sqlite3_close_v2(db.as_ptr()) }; } // SQLITE_ERROR stands in for an OK code that came without a handle. - Err(Code::of(if rc == SQLITE_OK { 1 } else { rc })) + Err(Code::of(if rc == ffi::SQLITE_OK { 1 } else { rc })) } } } @@ -94,7 +90,7 @@ impl<'a> Connection<'a> { let len = c_int::try_from(key.len()).expect("keys are short"); // SAFETY: `self.db` is open, and `key` is valid for `len` bytes, which SQLite copies // before returning. - check(unsafe { (self.api.key)(self.db.as_ptr(), key.as_ptr().cast(), len) }) + check(unsafe { ffi::sqlite3_key(self.db.as_ptr(), key.as_ptr().cast(), len) }) } /// Changes the key an already-open connection uses, empty bytes decrypting the database. @@ -102,21 +98,13 @@ impl<'a> Connection<'a> { let len = c_int::try_from(key.len()).expect("keys are short"); // SAFETY: `self.db` is open, and `key` is valid for `len` bytes, which SQLite copies // before returning. - check(unsafe { (self.api.rekey)(self.db.as_ptr(), key.as_ptr().cast(), len) }) - } - - const fn sqlite3mc(&self) -> &crate::ffi::Sqlite3mcApi { - self.api - .sqlite3mc - .as_ref() - .expect("only SQLite3MC has sqlite3mc_config") + check(unsafe { ffi::sqlite3_rekey(self.db.as_ptr(), key.as_ptr().cast(), len) }) } /// Sets a connection-wide SQLite3MC parameter, returning the value now in effect or -1. pub(crate) fn sqlite3mc_config(&self, param: &CStr, value: c_int) -> c_int { - let config = self.sqlite3mc().config; // SAFETY: `self.db` is open and `param` is NUL-terminated for the whole call. - unsafe { config(self.db.as_ptr(), param.as_ptr(), value) } + unsafe { ffi::sqlite3mc_config(self.db.as_ptr(), param.as_ptr(), value) } } /// Sets a cipher parameter, returning the value now in effect or -1. @@ -126,16 +114,16 @@ impl<'a> Connection<'a> { param: &CStr, value: c_int, ) -> c_int { - let config_cipher = self.sqlite3mc().config_cipher; // SAFETY: `self.db` is open and both names are NUL-terminated for the whole call. - unsafe { config_cipher(self.db.as_ptr(), cipher.as_ptr(), param.as_ptr(), value) } + unsafe { + ffi::sqlite3mc_config_cipher(self.db.as_ptr(), cipher.as_ptr(), param.as_ptr(), value) + } } /// The index `sqlite3mc_config` takes for the cipher named `name`. - pub(crate) fn sqlite3mc_cipher_index(&self, name: &CStr) -> c_int { - let cipher_index = self.sqlite3mc().cipher_index; + pub(crate) fn sqlite3mc_cipher_index(name: &CStr) -> c_int { // SAFETY: `name` is NUL-terminated for the whole call. - unsafe { cipher_index(name.as_ptr()) } + unsafe { ffi::sqlite3mc_cipher_index(name.as_ptr()) } } pub(crate) fn prepare(&self, sql: &CStr) -> Result, Code> { @@ -143,7 +131,7 @@ impl<'a> Connection<'a> { // SAFETY: `self.db` is open, `sql` is NUL-terminated (length -1 reads up to the NUL), // `stmt` is a valid place for the handle, and the tail pointer is not requested. let rc = unsafe { - (self.api.prepare_v2)( + ffi::sqlite3_prepare_v2( self.db.as_ptr(), sql.as_ptr(), -1, @@ -154,7 +142,6 @@ impl<'a> Connection<'a> { check(rc)?; let stmt = NonNull::new(stmt).expect("the harness never prepares empty SQL"); Ok(Statement { - api: self.api, stmt, _connection: PhantomData, }) @@ -165,82 +152,74 @@ impl<'a> Connection<'a> { self.prepare(sql)?.run(params) } - /// Starts a backup into `self` (as `dest_name`) from `src` (as `src_name`). `None` when the - /// pair is refused outright, SQLite's own built-in restrictions or SQLite3MC's - /// `sqlite3mcIsBackupSupported` compatibility guard alike. + /// Starts a backup into `self` from `src`, `None` when SQLite's own restrictions or + /// SQLite3MC's compatibility guard refuse the pair. pub(crate) fn backup_init( &self, dest_name: &CStr, src: &Self, src_name: &CStr, - ) -> Option> { + ) -> Option { // SAFETY: both connections are open for the call's duration, both names are // NUL-terminated, and the returned handle, if any, is owned by the new `BackupJob`. let handle = unsafe { - (self.api.backup_init)( + ffi::sqlite3_backup_init( self.db.as_ptr(), dest_name.as_ptr(), src.db.as_ptr(), src_name.as_ptr(), ) }; - NonNull::new(handle).map(|handle| BackupJob { - api: self.api, - handle, - }) + NonNull::new(handle).map(|handle| BackupJob { handle }) } } -/// A `sqlite3_backup` in progress. [`BackupJob::finish`] is the intended end; dropping one that -/// was never finished calls `sqlite3_backup_finish` anyway, so an early-abandoned backup is -/// still released. -pub(crate) struct BackupJob<'a> { - api: &'a Api, - handle: NonNull, +/// A `sqlite3_backup` in progress; dropping one never finished still calls +/// `sqlite3_backup_finish`. +pub(crate) struct BackupJob { + handle: NonNull, } -impl BackupJob<'_> { - /// Runs up to `n_page` pages (negative for "all remaining"), returning the raw result code: - /// callers need `SQLITE_DONE` (101) distinguished from a plain `SQLITE_OK` (0) success. +impl BackupJob { + /// Runs up to `n_page` pages (negative for all remaining), returning the raw code so callers + /// can tell `SQLITE_DONE` from plain success. pub(crate) fn step(&self, n_page: c_int) -> c_int { // SAFETY: the handle is valid until `finish`/drop, and `step` never invalidates it. - unsafe { (self.api.backup_step)(self.handle.as_ptr(), n_page) } + unsafe { ffi::sqlite3_backup_step(self.handle.as_ptr(), n_page) } } - /// Ends the backup. Finishes it immediately, then forgets `self` so [`BackupJob`]'s own - /// `Drop` never runs and finishes the same handle a second time. + /// Ends the backup, then forgets `self` so `Drop` never finishes the same handle twice. pub(crate) fn finish(self) -> Result<(), Code> { // SAFETY: `handle` came from a successful `backup_init`; `mem::forget` below is what // keeps this the only call that finishes it. - let result = check(unsafe { (self.api.backup_finish)(self.handle.as_ptr()) }); + let result = check(unsafe { ffi::sqlite3_backup_finish(self.handle.as_ptr()) }); core::mem::forget(self); result } } -impl Drop for BackupJob<'_> { +impl Drop for BackupJob { fn drop(&mut self) { // SAFETY: only reached when `finish` was never called, since `finish` forgets `self` // first; `self.handle` is still valid and this is its only release. unsafe { - (self.api.backup_finish)(self.handle.as_ptr()); + ffi::sqlite3_backup_finish(self.handle.as_ptr()); } } } -impl Drop for Connection<'_> { +impl Drop for Connection { fn drop(&mut self) { // SAFETY: `self.db` came from a successful open and is closed only here. Statements // borrow the connection, so all of them were finalized before this runs. - unsafe { (self.api.close_v2)(self.db.as_ptr()) }; + unsafe { ffi::sqlite3_close_v2(self.db.as_ptr()) }; } } /// A prepared statement, finalized on drop and unable to outlive its connection. pub(crate) struct Statement<'c> { - api: &'c Api, - stmt: NonNull, - _connection: PhantomData<&'c Connection<'c>>, + stmt: NonNull, + _connection: PhantomData<&'c Connection>, } impl Statement<'_> { @@ -248,48 +227,48 @@ impl Statement<'_> { pub(crate) fn run(&mut self, params: &[Param<'_>]) -> Result<(), Code> { let result = self.bind(params).and_then(|()| loop { match self.step() { - SQLITE_ROW => {} - SQLITE_DONE => break Ok(()), + ffi::SQLITE_ROW => {} + ffi::SQLITE_DONE => break Ok(()), rc => break Err(Code::of(rc)), } }); // SAFETY: `self.stmt` is a live statement owned by `self`. - unsafe { (self.api.reset)(self.stmt.as_ptr()) }; + unsafe { ffi::sqlite3_reset(self.stmt.as_ptr()) }; // SAFETY: as above. After this SQLite holds no pointer into `params`. - unsafe { (self.api.clear_bindings)(self.stmt.as_ptr()) }; + unsafe { ffi::sqlite3_clear_bindings(self.stmt.as_ptr()) }; result } fn step(&mut self) -> c_int { // SAFETY: `self.stmt` is a live statement, and every bound buffer outlives the step, // since `run` clears bindings before its borrows end. - unsafe { (self.api.step)(self.stmt.as_ptr()) } + unsafe { ffi::sqlite3_step(self.stmt.as_ptr()) } } - fn bind(&mut self, params: &[Param<'_>]) -> Result<(), Code> { + fn bind(&self, params: &[Param<'_>]) -> Result<(), Code> { let stmt = self.stmt.as_ptr(); for (index, param) in (1..).zip(params) { let rc = match *param { // SAFETY: `stmt` is live and `index` counts from 1 as SQLite expects. - Param::Null => unsafe { (self.api.bind_null)(stmt, index) }, + Param::Null => unsafe { ffi::sqlite3_bind_null(stmt, index) }, // SAFETY: as above. - Param::Integer(v) => unsafe { (self.api.bind_int64)(stmt, index, v) }, + Param::Integer(v) => unsafe { ffi::sqlite3_bind_int64(stmt, index, v) }, // SAFETY: as above. - Param::Real(v) => unsafe { (self.api.bind_double)(stmt, index, v) }, + Param::Real(v) => unsafe { ffi::sqlite3_bind_double(stmt, index, v) }, Param::Text(v) => { let len = c_int::try_from(v.len()).expect("text fits an int"); // SAFETY: `v` is valid for `len` bytes, and SQLITE_STATIC (the `None` // destructor) is sound because `run` clears the binding before `v`'s // borrow ends. unsafe { - (self.api.bind_text)(stmt, index, v.as_ptr().cast::(), len, None) + ffi::sqlite3_bind_text(stmt, index, v.as_ptr().cast::(), len, None) } } Param::Blob(v) => { let len = c_int::try_from(v.len()).expect("blob fits an int"); // SAFETY: as for text. unsafe { - (self.api.bind_blob)(stmt, index, v.as_ptr().cast::(), len, None) + ffi::sqlite3_bind_blob(stmt, index, v.as_ptr().cast::(), len, None) } } }; @@ -301,12 +280,12 @@ impl Statement<'_> { /// The next row, `None` once the statement is done. pub(crate) fn next_row(&mut self) -> Result>, Code> { match self.step() { - SQLITE_ROW => {} - SQLITE_DONE => return Ok(None), + ffi::SQLITE_ROW => {} + ffi::SQLITE_DONE => return Ok(None), rc => return Err(Code::of(rc)), } // SAFETY: the statement is live and positioned on a row. - let columns = unsafe { (self.api.column_count)(self.stmt.as_ptr()) }; + let columns = unsafe { ffi::sqlite3_column_count(self.stmt.as_ptr()) }; Ok(Some((0..columns).map(|i| self.column(i)).collect())) } @@ -315,20 +294,22 @@ impl Statement<'_> { let stmt = self.stmt.as_ptr(); // SAFETY: the statement is positioned on a row and `i` is in range, which holds for // every accessor below. - match unsafe { (self.api.column_type)(stmt, i) } { + match unsafe { ffi::sqlite3_column_type(stmt, i) } { // SAFETY: as above. - SQLITE_INTEGER => Value::Integer(unsafe { (self.api.column_int64)(stmt, i) }), + ffi::SQLITE_INTEGER => Value::Integer(unsafe { ffi::sqlite3_column_int64(stmt, i) }), // SAFETY: as above. - SQLITE_FLOAT => Value::Real(unsafe { (self.api.column_double)(stmt, i) }.to_bits()), - SQLITE_TEXT => { + ffi::SQLITE_FLOAT => { + Value::Real(unsafe { ffi::sqlite3_column_double(stmt, i) }.to_bits()) + } + ffi::SQLITE_TEXT => { // SAFETY: as above. The pointer is fetched before its length, the order SQLite // documents for a stable result. - let text = unsafe { (self.api.column_text)(stmt, i) }; + let text = unsafe { ffi::sqlite3_column_text(stmt, i) }; Value::Text(self.copy(text, i)) } - SQLITE_BLOB => { + ffi::SQLITE_BLOB => { // SAFETY: as for text. - let blob = unsafe { (self.api.column_blob)(stmt, i) }.cast::(); + let blob = unsafe { ffi::sqlite3_column_blob(stmt, i) }.cast::(); Value::Blob(self.copy(blob, i)) } _ => Value::Null, @@ -339,7 +320,7 @@ impl Statement<'_> { fn copy(&self, data: *const u8, i: c_int) -> Vec { // SAFETY: the statement is on a row and `i` is in range, and asking for the length // after the pointer leaves the pointer valid. - let len = unsafe { (self.api.column_bytes)(self.stmt.as_ptr(), i) }; + let len = unsafe { ffi::sqlite3_column_bytes(self.stmt.as_ptr(), i) }; let len = usize::try_from(len).expect("SQLite reports non-negative lengths"); if data.is_null() || len == 0 { return Vec::new(); @@ -353,6 +334,6 @@ impl Statement<'_> { impl Drop for Statement<'_> { fn drop(&mut self) { // SAFETY: `self.stmt` came from a successful prepare and is finalized only here. - unsafe { (self.api.finalize)(self.stmt.as_ptr()) }; + unsafe { ffi::sqlite3_finalize(self.stmt.as_ptr()) }; } } diff --git a/fuzz/src/entropy.rs b/fuzz/src/entropy.rs deleted file mode 100644 index a65a274..0000000 --- a/fuzz/src/entropy.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Replaces libc's `getentropy`, the call SQLCipher's RNG makes directly (`sqlcipher_wasm_rng` in -//! `sqlcipher-entropy.c`), with a deterministic stream reset once per [`run`](crate::run::run) call, so -//! the same [`Case`](crate::Case) produces the same salts and IVs on every repeat, not just the same -//! workload. SQLite3MC's own RNG reads `/dev/urandom` first and falls back to `getentropy` only if -//! that open fails, so this does not make SQLite3MC's own writes deterministic; it is enough for any -//! divergence whose randomness comes from SQLCipher's own side, which is where it is drawn from. - -use std::cell::Cell; - -thread_local! { - static STATE: Cell = const { Cell::new(0x9e37_79b9_7f4a_7c15) }; -} - -/// Reseeds the deterministic stream `getentropy` below draws from. Call once per [`run`] invocation, -/// before anything that might key or write a page, with a seed derived from the case itself so two -/// different cases draw different streams but the same case always draws the same one. -pub(crate) fn reset(seed: u64) { - STATE.with(|state| state.set(seed | 1)); -} - -/// xorshift64*, fast and good enough to stand in for real entropy in a replay-friendly harness. -fn next(state: &Cell) -> u64 { - let mut x = state.get(); - x ^= x << 13; - x ^= x >> 7; - x ^= x << 17; - state.set(x); - x.wrapping_mul(0x2545_f491_4f6c_dd1d) -} - -/// # Safety -/// `buf` must be valid for `len` writable bytes for the duration of this call, the same contract -/// libc's own `getentropy` has; the caller owns and allocates the buffer, this function only fills it. -#[unsafe(no_mangle)] -pub(crate) unsafe extern "C" fn getentropy(buf: *mut u8, len: usize) -> i32 { - STATE.with(|state| { - let mut filled = 0usize; - while filled < len { - let word = next(state).to_le_bytes(); - let take = word.len().min(len - filled); - // SAFETY: `buf..buf+len` is valid for writes for the whole call per this function's own - // caller contract, `u8` has no alignment requirement so `buf.add(filled)` needs no - // alignment check, `filled + take <= len` by construction so the destination range - // stays in bounds, `word` is a local stack array with no aliasing to `buf` possible, - // and `take <= word.len()` so the source read stays in bounds too. - unsafe { - std::ptr::copy_nonoverlapping(word.as_ptr(), buf.add(filled), take); - } - filled += take; - } - }); - 0 -} diff --git a/fuzz/src/ffi.rs b/fuzz/src/ffi.rs index 1ac6a48..ce60110 100644 --- a/fuzz/src/ffi.rs +++ b/fuzz/src/ffi.rs @@ -1,4 +1,8 @@ -//! The C API of both libraries, reached through the renamed symbols `build.rs` produces. +//! The C API of the vendored amalgamation, its `sqlite3mc_*` extensions included. +//! +//! Every handle is an opaque foreign allocation owned by SQLite3MC, moved through +//! [`crate::db`] as a `NonNull` and released by exactly one matching call. Every `&CStr` +//! parameter is NUL-terminated and must outlive the call; the amalgamation copies what it keeps. use core::ffi::{c_char, c_double, c_int, c_void}; @@ -34,157 +38,67 @@ pub(crate) const SQLITE_FLOAT: c_int = 2; pub(crate) const SQLITE_TEXT: c_int = 3; pub(crate) const SQLITE_BLOB: c_int = 4; -/// One library's entry points, so the harness drives either side through the same code. -pub(crate) struct Api { - pub(crate) open_v2: - unsafe extern "C" fn(*const c_char, *mut *mut Sqlite3, c_int, *const c_char) -> c_int, - pub(crate) close_v2: unsafe extern "C" fn(*mut Sqlite3) -> c_int, - pub(crate) key: unsafe extern "C" fn(*mut Sqlite3, *const c_void, c_int) -> c_int, - pub(crate) rekey: unsafe extern "C" fn(*mut Sqlite3, *const c_void, c_int) -> c_int, - pub(crate) prepare_v2: unsafe extern "C" fn( - *mut Sqlite3, - *const c_char, - c_int, - *mut *mut Stmt, - *mut *const c_char, - ) -> c_int, - pub(crate) step: unsafe extern "C" fn(*mut Stmt) -> c_int, - pub(crate) reset: unsafe extern "C" fn(*mut Stmt) -> c_int, - pub(crate) clear_bindings: unsafe extern "C" fn(*mut Stmt) -> c_int, - pub(crate) finalize: unsafe extern "C" fn(*mut Stmt) -> c_int, - pub(crate) bind_null: unsafe extern "C" fn(*mut Stmt, c_int) -> c_int, - pub(crate) bind_int64: unsafe extern "C" fn(*mut Stmt, c_int, i64) -> c_int, - pub(crate) bind_double: unsafe extern "C" fn(*mut Stmt, c_int, c_double) -> c_int, - pub(crate) bind_text: - unsafe extern "C" fn(*mut Stmt, c_int, *const c_char, c_int, Destructor) -> c_int, - pub(crate) bind_blob: - unsafe extern "C" fn(*mut Stmt, c_int, *const c_void, c_int, Destructor) -> c_int, - pub(crate) column_count: unsafe extern "C" fn(*mut Stmt) -> c_int, - pub(crate) column_type: unsafe extern "C" fn(*mut Stmt, c_int) -> c_int, - pub(crate) column_int64: unsafe extern "C" fn(*mut Stmt, c_int) -> i64, - pub(crate) column_double: unsafe extern "C" fn(*mut Stmt, c_int) -> c_double, - pub(crate) column_text: unsafe extern "C" fn(*mut Stmt, c_int) -> *const u8, - pub(crate) column_blob: unsafe extern "C" fn(*mut Stmt, c_int) -> *const c_void, - pub(crate) column_bytes: unsafe extern "C" fn(*mut Stmt, c_int) -> c_int, - pub(crate) backup_init: unsafe extern "C" fn( - *mut Sqlite3, - *const c_char, - *mut Sqlite3, - *const c_char, - ) -> *mut Backup, - pub(crate) backup_step: unsafe extern "C" fn(*mut Backup, c_int) -> c_int, - pub(crate) backup_finish: unsafe extern "C" fn(*mut Backup) -> c_int, - pub(crate) enable_shared_cache: unsafe extern "C" fn(c_int) -> c_int, - pub(crate) sqlite3mc: Option, +unsafe extern "C" { + pub(crate) fn sqlite3_open_v2( + dbfilename: *const c_char, + db: *mut *mut Sqlite3, + flags: c_int, + vfs: *const c_char, + ) -> c_int; + pub(crate) fn sqlite3_close_v2(db: *mut Sqlite3) -> c_int; + pub(crate) fn sqlite3_key(db: *mut Sqlite3, key: *const c_void, keylen: c_int) -> c_int; + pub(crate) fn sqlite3_rekey(db: *mut Sqlite3, key: *const c_void, keylen: c_int) -> c_int; + pub(crate) fn sqlite3_prepare_v2( + db: *mut Sqlite3, + zsql: *const c_char, + nbyte: c_int, + stmt: *mut *mut Stmt, + tail: *mut *const c_char, + ) -> c_int; + pub(crate) fn sqlite3_step(stmt: *mut Stmt) -> c_int; + pub(crate) fn sqlite3_reset(stmt: *mut Stmt) -> c_int; + pub(crate) fn sqlite3_clear_bindings(stmt: *mut Stmt) -> c_int; + pub(crate) fn sqlite3_finalize(stmt: *mut Stmt) -> c_int; + pub(crate) fn sqlite3_bind_null(stmt: *mut Stmt, index: c_int) -> c_int; + pub(crate) fn sqlite3_bind_int64(stmt: *mut Stmt, index: c_int, value: i64) -> c_int; + pub(crate) fn sqlite3_bind_double(stmt: *mut Stmt, index: c_int, value: c_double) -> c_int; + pub(crate) fn sqlite3_bind_text( + stmt: *mut Stmt, + index: c_int, + value: *const c_char, + nbyte: c_int, + destructor: Destructor, + ) -> c_int; + pub(crate) fn sqlite3_bind_blob( + stmt: *mut Stmt, + index: c_int, + value: *const c_void, + nbyte: c_int, + destructor: Destructor, + ) -> c_int; + pub(crate) fn sqlite3_column_count(stmt: *mut Stmt) -> c_int; + pub(crate) fn sqlite3_column_type(stmt: *mut Stmt, i: c_int) -> c_int; + pub(crate) fn sqlite3_column_int64(stmt: *mut Stmt, i: c_int) -> i64; + pub(crate) fn sqlite3_column_double(stmt: *mut Stmt, i: c_int) -> c_double; + pub(crate) fn sqlite3_column_text(stmt: *mut Stmt, i: c_int) -> *const u8; + pub(crate) fn sqlite3_column_blob(stmt: *mut Stmt, i: c_int) -> *const c_void; + pub(crate) fn sqlite3_column_bytes(stmt: *mut Stmt, i: c_int) -> c_int; + pub(crate) fn sqlite3_backup_init( + dest: *mut Sqlite3, + destname: *const c_char, + src: *mut Sqlite3, + srcname: *const c_char, + ) -> *mut Backup; + pub(crate) fn sqlite3_backup_step(p: *mut Backup, n_page: c_int) -> c_int; + pub(crate) fn sqlite3_backup_finish(p: *mut Backup) -> c_int; + pub(crate) fn sqlite3_enable_shared_cache(enable: c_int) -> c_int; + /// SQLite3MC's own entry points, absent from upstream SQLite. + pub(crate) fn sqlite3mc_config(db: *mut Sqlite3, param: *const c_char, value: c_int) -> c_int; + pub(crate) fn sqlite3mc_config_cipher( + db: *mut Sqlite3, + cipher: *const c_char, + param: *const c_char, + value: c_int, + ) -> c_int; + pub(crate) fn sqlite3mc_cipher_index(name: *const c_char) -> c_int; } - -/// SQLite3MC's configuration entry points, which SQLCipher lacks. -pub(crate) struct Sqlite3mcApi { - pub(crate) config: unsafe extern "C" fn(*mut Sqlite3, *const c_char, c_int) -> c_int, - pub(crate) config_cipher: - unsafe extern "C" fn(*mut Sqlite3, *const c_char, *const c_char, c_int) -> c_int, - pub(crate) cipher_index: unsafe extern "C" fn(*const c_char) -> c_int, -} - -macro_rules! bindings { - ($module:ident, $prefix:literal, { $($name:ident($($arg:ty),*) $(-> $ret:ty)?;)* }) => { - mod $module { - use super::{c_char, c_double, c_int, c_void, Backup, Destructor, Sqlite3, Stmt}; - - unsafe extern "C" { - $( - #[link_name = concat!($prefix, stringify!($name))] - pub(super) fn $name($(_: $arg),*) $(-> $ret)?; - )* - } - } - }; -} - -macro_rules! common { - ($module:ident, $prefix:literal $(, $extra:ident($($arg:ty),*) -> $ret:ty)*) => { - bindings!($module, $prefix, { - sqlite3_open_v2(*const c_char, *mut *mut Sqlite3, c_int, *const c_char) -> c_int; - sqlite3_close_v2(*mut Sqlite3) -> c_int; - sqlite3_key(*mut Sqlite3, *const c_void, c_int) -> c_int; - sqlite3_rekey(*mut Sqlite3, *const c_void, c_int) -> c_int; - sqlite3_prepare_v2(*mut Sqlite3, *const c_char, c_int, *mut *mut Stmt, *mut *const c_char) -> c_int; - sqlite3_step(*mut Stmt) -> c_int; - sqlite3_reset(*mut Stmt) -> c_int; - sqlite3_clear_bindings(*mut Stmt) -> c_int; - sqlite3_finalize(*mut Stmt) -> c_int; - sqlite3_bind_null(*mut Stmt, c_int) -> c_int; - sqlite3_bind_int64(*mut Stmt, c_int, i64) -> c_int; - sqlite3_bind_double(*mut Stmt, c_int, c_double) -> c_int; - sqlite3_bind_text(*mut Stmt, c_int, *const c_char, c_int, Destructor) -> c_int; - sqlite3_bind_blob(*mut Stmt, c_int, *const c_void, c_int, Destructor) -> c_int; - sqlite3_column_count(*mut Stmt) -> c_int; - sqlite3_column_type(*mut Stmt, c_int) -> c_int; - sqlite3_column_int64(*mut Stmt, c_int) -> i64; - sqlite3_column_double(*mut Stmt, c_int) -> c_double; - sqlite3_column_text(*mut Stmt, c_int) -> *const u8; - sqlite3_column_blob(*mut Stmt, c_int) -> *const c_void; - sqlite3_column_bytes(*mut Stmt, c_int) -> c_int; - sqlite3_backup_init(*mut Sqlite3, *const c_char, *mut Sqlite3, *const c_char) -> *mut Backup; - sqlite3_backup_step(*mut Backup, c_int) -> c_int; - sqlite3_backup_finish(*mut Backup) -> c_int; - sqlite3_enable_shared_cache(c_int) -> c_int; - $($extra($($arg),*) -> $ret;)* - }); - }; -} - -common!( - mc, - "mc_", - sqlite3mc_config(*mut Sqlite3, *const c_char, c_int) -> c_int, - sqlite3mc_config_cipher(*mut Sqlite3, *const c_char, *const c_char, c_int) -> c_int, - sqlite3mc_cipher_index(*const c_char) -> c_int -); -common!(sc, "sc_"); - -macro_rules! api { - ($module:ident, $sqlite3mc:expr) => { - Api { - open_v2: $module::sqlite3_open_v2, - close_v2: $module::sqlite3_close_v2, - key: $module::sqlite3_key, - rekey: $module::sqlite3_rekey, - prepare_v2: $module::sqlite3_prepare_v2, - step: $module::sqlite3_step, - reset: $module::sqlite3_reset, - clear_bindings: $module::sqlite3_clear_bindings, - finalize: $module::sqlite3_finalize, - bind_null: $module::sqlite3_bind_null, - bind_int64: $module::sqlite3_bind_int64, - bind_double: $module::sqlite3_bind_double, - bind_text: $module::sqlite3_bind_text, - bind_blob: $module::sqlite3_bind_blob, - column_count: $module::sqlite3_column_count, - column_type: $module::sqlite3_column_type, - column_int64: $module::sqlite3_column_int64, - column_double: $module::sqlite3_column_double, - column_text: $module::sqlite3_column_text, - column_blob: $module::sqlite3_column_blob, - column_bytes: $module::sqlite3_column_bytes, - backup_init: $module::sqlite3_backup_init, - backup_step: $module::sqlite3_backup_step, - backup_finish: $module::sqlite3_backup_finish, - enable_shared_cache: $module::sqlite3_enable_shared_cache, - sqlite3mc: $sqlite3mc, - } - }; -} - -/// SQLite3 Multiple Ciphers, from `sqlite3mc-src`. -pub(crate) static SQLITE3MC: Api = api!( - mc, - Some(Sqlite3mcApi { - config: mc::sqlite3mc_config, - config_cipher: mc::sqlite3mc_config_cipher, - cipher_index: mc::sqlite3mc_cipher_index, - }) -); - -/// SQLCipher with its libtomcrypt provider, from `sqlcipher-src`. -pub(crate) static SQLCIPHER: Api = api!(sc, None); diff --git a/fuzz/src/input.rs b/fuzz/src/input.rs index e64ea07..d8b9143 100644 --- a/fuzz/src/input.rs +++ b/fuzz/src/input.rs @@ -1,17 +1,8 @@ -//! What one execution does, decoded from fuzzer bytes into values both libraries accept. +//! What one execution does, decoded from fuzzer bytes into values SQLite3MC accepts. use arbitrary::{Arbitrary, Result, Unstructured}; -/// The library that writes the database. The other one reads it. -#[derive(Arbitrary, Debug, Clone, Copy, PartialEq, Eq)] -pub enum Side { - /// SQLCipher, the reference. - SqlCipher, - /// SQLite3MC's `sqlcipher` scheme. - Sqlite3mc, -} - -/// A hash both sides offer for key derivation and page authentication. +/// A hash the `sqlcipher` scheme takes for key derivation and page authentication. #[derive(Arbitrary, Debug, Clone, Copy, PartialEq, Eq)] pub enum Algorithm { /// SHA-1. @@ -22,129 +13,6 @@ pub enum Algorithm { Sha512, } -/// How the database is keyed. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Key { - /// A raw 32-byte key, which skips key derivation. - Raw([u8; 32]), - /// A raw key with an explicit 16-byte salt, which also lets page 1 keep a plaintext header. - RawWithSalt { - /// The key. - key: [u8; 32], - /// The salt. - salt: [u8; 16], - /// Bytes of page 1 left unencrypted, a multiple of 16 up to 96, or 0. - plaintext_header: u8, - }, - /// A passphrase run through PBKDF2 `kdf_iter` times. - Passphrase { - /// The passphrase, never empty. - bytes: Vec, - /// Iterations, 1 to 16. - kdf_iter: u8, - }, -} - -/// The cipher settings, as SQLCipher compatibility defaults plus optional overrides. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Config { - /// SQLCipher major version whose defaults apply, 1 to 4. - pub compatibility: u8, - /// Page size, a power of two from 512 to 65536, or the version's default. - pub page_size: Option, - /// Key derivation hash, or the version's default. - pub kdf_algorithm: Option, - /// Page authentication hash, or the version's default. - pub hmac_algorithm: Option, - /// Whether pages carry an HMAC, or the version's default. - pub hmac: Option, - /// The key. - pub key: Key, -} - -impl Config { - /// Whether the key names a salt that SQLite3MC replaces with the file's, lacking a plaintext header. - #[must_use] - pub const fn explicit_salt_unused_by_sqlite3mc(&self) -> bool { - matches!(self.key, Key::RawWithSalt { .. }) && self.plaintext_header() == 0 - } - - /// Page 1's plaintext header size in effect, which only compatibility 4 honours. - #[must_use] - pub const fn plaintext_header(&self) -> u8 { - match self.key { - Key::RawWithSalt { - plaintext_header, .. - } if self.compatibility == 4 => plaintext_header, - _ => 0, - } - } - - /// Bytes each page reserves for the IV plus, with HMAC on, the digest padded to 16 bytes. - #[must_use] - pub fn reserved(&self) -> u32 { - let hmac = self.hmac.unwrap_or(self.compatibility >= 2); - let algorithm = self.hmac_algorithm.unwrap_or(if self.compatibility == 4 { - Algorithm::Sha512 - } else { - Algorithm::Sha1 - }); - let digest: u32 = match algorithm { - Algorithm::Sha1 => 20, - Algorithm::Sha256 => 32, - Algorithm::Sha512 => 64, - }; - 16 + if hmac { digest.next_multiple_of(16) } else { 0 } - } -} - -impl<'a> Arbitrary<'a> for Config { - fn arbitrary(u: &mut Unstructured<'a>) -> Result { - let compatibility = u.int_in_range(1..=4)?; - let page_size = if u.arbitrary()? { - Some(1 << u.int_in_range(9..=16)?) - } else { - None - }; - let kdf_algorithm = u.arbitrary()?; - let hmac_algorithm = u.arbitrary()?; - let hmac = u.arbitrary()?; - let key = match u.int_in_range(0..=2)? { - 0 => Key::Raw(u.arbitrary()?), - 1 => Key::RawWithSalt { - key: u.arbitrary()?, - salt: u.arbitrary()?, - plaintext_header: 16 * u.int_in_range(0..=6)?, - }, - _ => { - let len = u.int_in_range(1..=64)?; - let mut bytes = u.bytes(len)?.to_vec(); - // SQLite3MC documents `raw:` as raw key syntax for every cipher, SQLCipher has none. - if bytes.starts_with(b"raw:") { - bytes[0] = b'R'; - } - Key::Passphrase { - bytes, - kdf_iter: u.int_in_range(1..=16)?, - } - } - }; - let mut config = Self { - compatibility, - page_size, - kdf_algorithm, - hmac_algorithm, - hmac, - key, - }; - // `sqlite3BtreeSetPageSize` enlarges 512-byte pages to 1024 when more than 32 bytes are reserved. - if config.page_size == Some(512) && config.reserved() > 32 { - config.page_size = Some(1024); - } - Ok(config) - } -} - /// One of the three tables a workload can touch. #[derive(Arbitrary, Debug, Clone, Copy, PartialEq, Eq)] pub enum Table { @@ -236,65 +104,16 @@ pub struct Flip { pub mask: u8, } -/// One execution. -#[derive(Debug, Clone, PartialEq)] -pub struct Case { - /// The library that writes the database. - pub writer: Side, - /// Cipher settings, identical on both sides. - pub config: Config, - /// Workload the writer runs, at most [`MAX_OPS`] steps of it. - pub ops: Vec, - /// Damage applied to a copy of the finished file, at most [`MAX_FLIPS`] flips of it. - pub damage: Vec, -} - -/// [`Case`] as decoded, before the exclusions in [`Case::from`]. -#[derive(Arbitrary)] -struct Draw { - writer: Side, - config: Config, - ops: Vec, - damage: Vec, -} - -impl From for Case { - fn from(draw: Draw) -> Self { - let mut config = draw.config; - // SQLCipher 4.19.0 cannot write 512-byte pages (upstream/sqlcipher-512-page-size.md). - if draw.writer == Side::SqlCipher && config.page_size == Some(512) { - config.page_size = Some(1024); - } - Self { - writer: draw.writer, - config, - ops: draw.ops, - damage: draw.damage, - } - } -} - -impl<'a> Arbitrary<'a> for Case { - fn arbitrary(u: &mut Unstructured<'a>) -> Result { - Draw::arbitrary(u).map(Self::from) - } - - fn arbitrary_take_rest(u: Unstructured<'a>) -> Result { - Draw::arbitrary_take_rest(u).map(Self::from) - } -} - -/// Workload steps beyond this are ignored, bounding one execution's time. Shared by every input -/// model that runs a workload, not just [`Case`]. +/// Workload steps beyond this are ignored, bounding one execution's time. Shared by every +/// input model that runs a workload. pub(crate) const MAX_OPS: usize = 64; /// Flips beyond this are ignored. Shared the same way as [`MAX_OPS`]. pub(crate) const MAX_FLIPS: usize = 16; -/// One of SQLite3MC's native cipher schemes, every one but `sqlcipher`. +/// One of SQLite3MC's own cipher schemes. /// -/// The differential fuzzer above already covers `sqlcipher` against a real second implementation. -/// These have none, so they are tested for self-consistency instead: round-trip, a wrong key never -/// succeeding, and, where the scheme actually authenticates its pages, tamper detection. +/// Each is tested for self-consistency: round-trip, a wrong key never succeeding, and, where +/// the scheme actually authenticates its pages, tamper detection. #[derive(Arbitrary, Debug, Clone, Copy, PartialEq, Eq)] pub enum Cipher { /// `aes128cbc`. No KDF parameter, no raw key (`GenerateKeyAES128Cipher` always pads and @@ -307,16 +126,16 @@ pub enum Cipher { /// `chacha20`. Settable PBKDF2 `kdf_iter`, a raw key with an optional embedded salt /// (`sqlite3mcExtractRawKey`), a plaintext header, a 16-byte per-page authentication tag. ChaCha20, - /// `rc4`. `legacy` is present but fixed at `RC4_LEGACY_DEFAULT` (min equals max), no raw key - /// (`GenerateKeyRC4Cipher` always SHA1-digests the given bytes as a passphrase), no plaintext - /// header, no page authentication (the weakest scheme on purpose, kept for legacy databases). - Rc4, - /// `ascon128`. No legacy variant. Settable PBKDF2 `kdf_iter`, a raw key with an optional + /// `sqlcipher`, SQLite3MC's own SQLCipher-compatible scheme. Settable PBKDF2 `kdf_iter` + /// (`kdf_algorithm` picks its hash), a raw key with an optional embedded salt, a plaintext + /// header, and, with `hmac_use` on, a page HMAC whose digest `hmac_algorithm` sizes. + SqlCipher, + /// `ascon128`. No `legacy` parameter. Settable PBKDF2 `kdf_iter`, a raw key with an optional /// embedded salt, a plaintext header, a 16-byte per-page authentication tag. Ascon128, - /// `aegis`. No legacy variant. Argon2id key derivation, a raw key with an optional embedded - /// salt, a plaintext header, AEAD by design. `algorithm` picks AEGIS-128 (16-byte key) or - /// AEGIS-256 (32-byte key). + /// `aegis`. No `legacy` parameter. Argon2id key derivation, a raw key with an optional + /// embedded salt, a plaintext header, AEAD by design. `algorithm` picks AEGIS-128 (16-byte + /// key) or AEGIS-256 (32-byte key). Aegis, } @@ -328,26 +147,19 @@ impl Cipher { Self::Aes128 => "aes128cbc", Self::Aes256 => "aes256cbc", Self::ChaCha20 => "chacha20", - Self::Rc4 => "rc4", + Self::SqlCipher => "sqlcipher", Self::Ascon128 => "ascon128", Self::Aegis => "aegis", } } - /// Whether `legacy` is actually settable, as opposed to merely present but fixed, as it is - /// for `Rc4` (`RC4_LEGACY_DEFAULT` is both its minimum and its maximum). - #[must_use] - pub const fn has_legacy(self) -> bool { - matches!(self, Self::Aes128 | Self::Aes256 | Self::ChaCha20) - } - - /// Whether `legacy_page_size` is one of this scheme's own parameters. Unlike - /// [`Cipher::has_legacy`], `Rc4` has this even though its own `legacy` is fixed. + /// Whether the scheme's own parameter table has `legacy`, which the harness pins to `0` + /// so a changed upstream default never re-enables legacy encryption. #[must_use] - pub const fn has_legacy_page_size(self) -> bool { + pub const fn has_legacy_param(self) -> bool { matches!( self, - Self::Aes128 | Self::Aes256 | Self::ChaCha20 | Self::Rc4 + Self::Aes128 | Self::Aes256 | Self::ChaCha20 | Self::SqlCipher ) } @@ -356,43 +168,45 @@ impl Cipher { /// its own, different, Argon2id parameters instead. #[must_use] pub const fn has_kdf_iter(self) -> bool { - matches!(self, Self::ChaCha20 | Self::Ascon128) + matches!(self, Self::ChaCha20 | Self::SqlCipher | Self::Ascon128) } - /// Whether this scheme's key setup ever calls `sqlite3mcExtractRawKey`, so a raw key, with or - /// without an embedded salt, bypasses derivation entirely. `Aes128`, `Aes256` and `Rc4` always - /// treat whatever bytes `key()` is given as a passphrase, so a hex string drawn to look like a - /// raw key would just be hashed as one. + /// Whether this scheme's key setup ever calls `sqlite3mcExtractRawKey`, so a raw key, with + /// or without an embedded salt, bypasses derivation entirely. `Aes128` and `Aes256` always + /// treat whatever bytes `key()` is given as a passphrase, so a hex string drawn to look like + /// a raw key would just be hashed as one. #[must_use] pub const fn supports_raw_key(self) -> bool { - matches!(self, Self::ChaCha20 | Self::Ascon128 | Self::Aegis) + matches!( + self, + Self::ChaCha20 | Self::SqlCipher | Self::Ascon128 | Self::Aegis + ) } - /// Whether `plaintext_header_size` is one of this scheme's own parameters. Every scheme that - /// has it also has [`Cipher::supports_raw_key`], not a coincidence: a plaintext header leaves - /// the file's own salt bytes unencrypted, so recovering the salt to rederive a passphrase key - /// on reopen needs it supplied back explicitly, which only a raw key with an embedded salt - /// does ([`NativeConfig::arbitrary`] only draws a plaintext header for that key form). + /// Whether `plaintext_header_size` is one of this scheme's own parameters. Every scheme + /// that has it also has [`Cipher::supports_raw_key`], not a coincidence: a plaintext header + /// leaves the file's own salt bytes unencrypted, so recovering the salt to rederive a + /// passphrase key on reopen needs it supplied back explicitly, which only a raw key with an + /// embedded salt does ([`NativeConfig::arbitrary`] only draws a plaintext header for that + /// key form). #[must_use] pub const fn has_plaintext_header(self) -> bool { - matches!(self, Self::ChaCha20 | Self::Ascon128 | Self::Aegis) - } - - /// Whether this scheme authenticates its pages, so tampering is expected to be caught. - #[must_use] - pub const fn authenticated(self) -> bool { - matches!(self, Self::ChaCha20 | Self::Ascon128 | Self::Aegis) + matches!( + self, + Self::ChaCha20 | Self::SqlCipher | Self::Ascon128 | Self::Aegis + ) } /// How many leading bytes of a passphrase this scheme's own `GenerateKey*Cipher` actually - /// reads, when it reads fewer than the whole thing. `Aes128` and `Aes256` both pad or truncate - /// through `sqlite3mcPadPassword`, which copies at most 32 bytes; every other scheme consumes - /// the full password (PBKDF2, Argon2id, or a plain hash all take an explicit length). + /// reads, when it reads fewer than the whole thing. `Aes128` and `Aes256` both pad or + /// truncate through `sqlite3mcPadPassword`, which copies at most 32 bytes; every other + /// scheme consumes the full password (PBKDF2, Argon2id, or a plain hash all take an + /// explicit length). #[must_use] pub const fn passphrase_prefix_len(self) -> Option { match self { Self::Aes128 | Self::Aes256 => Some(32), - Self::ChaCha20 | Self::Rc4 | Self::Ascon128 | Self::Aegis => None, + Self::ChaCha20 | Self::SqlCipher | Self::Ascon128 | Self::Aegis => None, } } @@ -400,8 +214,8 @@ impl Cipher { #[must_use] pub const fn key_len(self, aegis_algorithm_256: bool) -> usize { match self { - Self::Aes128 | Self::Rc4 => 16, - Self::Aes256 | Self::ChaCha20 | Self::Ascon128 => 32, + Self::Aes128 => 16, + Self::Aes256 | Self::ChaCha20 | Self::SqlCipher | Self::Ascon128 => 32, Self::Aegis => { if aegis_algorithm_256 { 32 @@ -415,9 +229,9 @@ impl Cipher { /// How a native-cipher database is keyed. /// -/// Unlike [`Key`], the raw forms are only ever drawn for a [`Cipher`] that -/// [`Cipher::supports_raw_key`] allows, [`NativeConfig::arbitrary`] enforces it; every other -/// scheme treats whatever bytes it is given as a passphrase. +/// The raw forms are only ever drawn for a [`Cipher`] that [`Cipher::supports_raw_key`] +/// allows, [`NativeConfig::arbitrary`] enforces it; every other scheme treats whatever bytes +/// it is given as a passphrase. #[derive(Debug, Clone, PartialEq, Eq)] pub enum NativeKey { /// A raw key, truncated to the scheme's own key length when applied. @@ -430,7 +244,7 @@ pub enum NativeKey { salt: [u8; 16], }, /// A passphrase, run through this scheme's own KDF when it has one, or hashed by its fixed - /// legacy scheme when it does not. + /// scheme when it does not. Passphrase { /// The passphrase, never empty. bytes: Vec, @@ -476,20 +290,22 @@ impl<'a> Arbitrary<'a> for NativeKey { } } -/// The cipher settings for one native-scheme execution, only the fields [`Cipher`] itself supports -/// ever drawn or applied. +/// The cipher settings for one native-scheme execution, only the fields [`Cipher`] itself +/// supports ever drawn or applied. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NativeConfig { /// Which scheme. pub cipher: Cipher, - /// `legacy`, when [`Cipher::has_legacy`]. - pub legacy: Option, - /// `legacy_page_size`, a power of two from 512 to 65536, when [`Cipher::has_legacy_page_size`]. - pub page_size: Option, + /// `kdf_algorithm`, `SqlCipher` only, or the scheme's default when `None`. + pub kdf_algorithm: Option, + /// `hmac_algorithm`, `SqlCipher` only, or the scheme's default when `None`. + pub hmac_algorithm: Option, + /// `hmac_use`, `SqlCipher` only, or the scheme's default when `None`. + pub hmac_use: Option, /// `plaintext_header_size`, when [`Cipher::has_plaintext_header`] and the key is - /// [`NativeKey::RawWithSalt`]. A passphrase key's salt would otherwise be lost on reopen, since - /// a plaintext header leaves no encrypted copy of it anywhere in the file (issue #209 on - /// `SQLite3MultipleCiphers`, which documents `cipher_salt` as the required workaround). + /// [`NativeKey::RawWithSalt`]. A passphrase key's salt would otherwise be lost on reopen, + /// since a plaintext header leaves no encrypted copy of it anywhere in the file (issue #209 + /// on `SQLite3MultipleCiphers`, which documents `cipher_salt` as the required workaround). pub plaintext_header_size: Option, /// `algorithm`, `Aegis` only: `true` picks AEGIS-256 (32-byte key), `false` AEGIS-128. pub aegis_algorithm_256: bool, @@ -500,11 +316,10 @@ pub struct NativeConfig { impl<'a> Arbitrary<'a> for NativeConfig { fn arbitrary(u: &mut Unstructured<'a>) -> Result { let cipher: Cipher = u.arbitrary()?; - let legacy = cipher.has_legacy().then(|| u.arbitrary()).transpose()?; - let page_size = if cipher.has_legacy_page_size() && u.arbitrary()? { - Some(1 << u.int_in_range(9..=16)?) + let (kdf_algorithm, hmac_algorithm, hmac_use) = if matches!(cipher, Cipher::SqlCipher) { + (u.arbitrary()?, u.arbitrary()?, u.arbitrary()?) } else { - None + (None, None, None) }; let aegis_algorithm_256 = matches!(cipher, Cipher::Aegis) && u.arbitrary()?; let key = NativeKey::arbitrary_for(u, cipher)?; @@ -516,8 +331,9 @@ impl<'a> Arbitrary<'a> for NativeConfig { }; Ok(Self { cipher, - legacy, - page_size, + kdf_algorithm, + hmac_algorithm, + hmac_use, plaintext_header_size, aegis_algorithm_256, key, @@ -531,30 +347,30 @@ impl NativeConfig { pub fn same_layout(&self, other: &Self) -> bool { let Self { cipher, - legacy, - page_size, + kdf_algorithm, + hmac_algorithm, + hmac_use, plaintext_header_size, aegis_algorithm_256, key: _, } = self; *cipher == other.cipher - && *legacy == other.legacy - && *page_size == other.page_size + && *kdf_algorithm == other.kdf_algorithm + && *hmac_algorithm == other.hmac_algorithm + && *hmac_use == other.hmac_use && *plaintext_header_size == other.plaintext_header_size && *aegis_algorithm_256 == other.aegis_algorithm_256 } - /// Whether this configuration encrypts page 1's header bytes 16 to 23, so nothing on disk - /// tells SQLite the page size before the key is set: `rc4` always, and `aes128cbc`, - /// `aes256cbc` and `chacha20` in `legacy` mode unless a plaintext header covers them. + /// Whether this configuration authenticates its pages, so tampering is expected to be + /// caught. `SqlCipher` does so only while `hmac_use` holds, its own default being on. #[must_use] - pub fn hides_page_size(&self) -> bool { - let whole_page_one = match self.cipher { - Cipher::Rc4 => true, - Cipher::Aes128 | Cipher::Aes256 | Cipher::ChaCha20 => self.legacy == Some(true), - Cipher::Ascon128 | Cipher::Aegis => false, - }; - whole_page_one && self.plaintext_header_size.unwrap_or(0) < 24 + pub fn authenticated(&self) -> bool { + match self.cipher { + Cipher::Aes128 | Cipher::Aes256 => false, + Cipher::ChaCha20 | Cipher::Ascon128 | Cipher::Aegis => true, + Cipher::SqlCipher => self.hmac_use.unwrap_or(true), + } } } @@ -566,7 +382,8 @@ pub struct NativeCase { /// Workload, at most [`MAX_OPS`] steps of it. pub ops: Vec, /// Damage applied to a copy of the finished file, at most [`MAX_FLIPS`] flips of it. Only - /// meaningful, and only applied, when [`Cipher::authenticated`]. + /// meaningful, and only applied, when the configuration [`NativeConfig::authenticated`] + /// authenticates its pages. pub damage: Vec, /// A key to reopen with after the workload, almost certainly not the one that wrote it. pub wrong_key: NativeKey, @@ -651,8 +468,8 @@ pub struct WalCase { /// The cipher and key. pub config: NativeConfig, /// `mc_legacy_wal`: `true` selects the older WAL frame encryption, which the project's own - /// source comments recommend only for recovering journals written before version 1.3.0, not - /// for general use. + /// source comments recommend only for recovering journals written before version 1.3.0, + /// not for general use. pub legacy_wal: bool, /// Workload, at most [`MAX_OPS`] steps of it. pub ops: Vec, @@ -712,11 +529,8 @@ pub struct AttachCase { pub detach_reattach: bool, } -/// A `PRAGMA page_size` value, a power of two from 512 to 65536. -/// -/// Distinct from [`NativeConfig::page_size`] (the cipher's own `legacy_page_size` parameter): -/// this is the ordinary SQLite btree page size, set through SQL rather than -/// `sqlite3mc_config_cipher`. +/// A `PRAGMA page_size` value, a power of two from 512 to 65536, the ordinary SQLite btree +/// page size, set through SQL rather than `sqlite3mc_config_cipher`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PageSize(u32); @@ -738,8 +552,8 @@ impl<'a> Arbitrary<'a> for PageSize { /// unrelated content, then runs `sqlite3_backup_init`/`_step`/`_finish` between them. /// /// SQLite3MC's own compatibility guard (`sqlite3mcIsBackupSupported`, called from -/// `sqlite3_backup_init`) lets pairs disagreeing on reserved bytes through, so a backup between them -/// corrupts the destination (`upstream/sqlite3mc-backup-guard-ignores-reserve.md`). +/// `sqlite3_backup_init`) lets pairs disagreeing on reserved bytes through, so a backup between +/// them corrupts the destination (`upstream/sqlite3mc-backup-guard-ignores-reserve.md`). /// /// Oracle: when [`BackupCase::src`] and [`BackupCase::dest`] are laid out identically, every /// cipher setting but the key and the `PRAGMA page_size` equal, and every step reports success, diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 7aef8e5..9248adf 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -1,6 +1,6 @@ -//! Fuzzing SQLite3MC against SQLCipher differentially, its own native ciphers for -//! self-consistency, its rekey support, its WAL journal mode, its `ATTACH DATABASE` support, its -//! `sqlite3_backup_*` API, and its shared-cache mode, on the same SQLite. +//! Fuzzing SQLite3MC's own cipher schemes for self-consistency, its rekey support, its WAL +//! journal mode, its `ATTACH DATABASE` support, its `sqlite3_backup_*` API, and its shared-cache +//! mode. #![expect( clippy::redundant_pub_crate, @@ -8,14 +8,13 @@ )] mod db; -mod entropy; mod ffi; mod input; mod run; pub mod seedgen; pub use input::{ - Algorithm, AttachCase, BackupCase, Case, Checkpoint, Cipher, Config, Key, NativeCase, - NativeKey, Op, RekeyAction, RekeyCase, SharedCacheCase, SiblingKey, Side, Table, WalCase, + Algorithm, AttachCase, BackupCase, Checkpoint, Cipher, NativeCase, NativeKey, Op, RekeyAction, + RekeyCase, SharedCacheCase, SiblingKey, Table, WalCase, }; -pub use run::{run, run_attach, run_backup, run_native, run_rekey, run_shared_cache, run_wal}; +pub use run::{run_attach, run_backup, run_native, run_rekey, run_shared_cache, run_wal}; diff --git a/fuzz/src/run.rs b/fuzz/src/run.rs index b45666e..0360ddd 100644 --- a/fuzz/src/run.rs +++ b/fuzz/src/run.rs @@ -1,9 +1,9 @@ -//! One execution: write with one side, read with both, damage a copy, read that with both. +//! One execution: write, read back, damage a copy, read that, through SQLite3MC's own ciphers. use core::ffi::{c_int, CStr}; use std::{ ffi::CString, - fmt::{Debug, Write as _}, + fmt::Write as _, fs, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, @@ -12,15 +12,11 @@ use std::{ use crate::{ db::{Code, Connection, Param, Value}, - ffi::{ - Api, SQLCIPHER, SQLITE3MC, SQLITE_DONE, SQLITE_OPEN_CREATE, SQLITE_OPEN_READONLY, - SQLITE_OPEN_READWRITE, - }, + ffi::{SQLITE_DONE, SQLITE_OPEN_CREATE, SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE}, input::{ - Algorithm, AttachCase, AttachedDb, BackupCase, Case, Cell, Cipher, Column, Config, Key, - NativeCase, NativeConfig, NativeKey, Op, PageSize, RekeyAction, RekeyCase, SharedCacheCase, - SiblingKey, Side, Table, WalCase, MAX_ATTACHED, MAX_ATTACH_ROWS, MAX_FLIPS, MAX_OPS, - MAX_SIBLINGS, + Algorithm, AttachCase, AttachedDb, BackupCase, Cell, Cipher, Column, NativeCase, + NativeConfig, NativeKey, Op, PageSize, RekeyAction, RekeyCase, SharedCacheCase, SiblingKey, + Table, WalCase, MAX_ATTACHED, MAX_ATTACH_ROWS, MAX_FLIPS, MAX_OPS, MAX_SIBLINGS, }, }; @@ -41,77 +37,8 @@ const TABLE_NAMES: [&str; 3] = ["t0", "t1", "t2"]; /// every native scheme leaves outside its AEAD coverage, independent of `plaintext_header_size`. const CIPHER_PAGE1_OFFSET: usize = 24; -/// Runs `case`, panicking when the libraries disagree or the writer cannot read its own file. -/// -/// # Panics -/// -/// On a divergence, or when a library refuses a setting or step the harness generates as valid. -pub fn run(case: &Case) { - static SERIAL: Mutex<()> = Mutex::new(()); - let _serial = begin_run(&SERIAL, case); - - let dir = WORKDIR.as_path(); - let clean = dir.join("clean.db"); - let damaged = dir.join("damaged.db"); - remove(&clean); - remove(&damaged); - - let writer = case.writer; - let reader = match writer { - Side::SqlCipher => Side::Sqlite3mc, - Side::Sqlite3mc => Side::SqlCipher, - }; - write(writer, &clean, case); - - let own = dump(api(writer), &clean, |c| configure(c, writer, &case.config)); - assert!( - clean_read(&own), - "{} cannot read back its own file for {case:#?}: {}", - name(writer), - describe(own.iter().rev().take(3)), - ); - compare( - case, - "the clean file", - writer, - &own, - reader, - &dump(api(reader), &clean, |c| configure(c, reader, &case.config)), - ); - - let mut bytes = fs::read(&clean).expect("the clean file exists"); - let flips = &case.damage[..case.damage.len().min(MAX_FLIPS)]; - if !bytes.is_empty() && !flips.is_empty() { - let len = u64::try_from(bytes.len()).expect("file length fits u64"); - for flip in flips { - let offset = - usize::try_from(u64::from(flip.offset) % len).expect("offset is below the length"); - // SQLite3MC reads this salt from the file (upstream/sqlite3mc-sqlcipher-explicit-salt.md). - if offset < 16 && case.config.explicit_salt_unused_by_sqlite3mc() { - continue; - } - bytes[offset] ^= flip.mask; - } - fs::write(&damaged, &bytes).expect("the work directory is writable"); - let by_writer = dump(api(writer), &damaged, |c| { - configure(c, writer, &case.config) - }); - let by_reader = dump(api(reader), &damaged, |c| { - configure(c, reader, &case.config) - }); - compare( - case, - "the damaged file", - writer, - &by_writer, - reader, - &by_reader, - ); - } - - remove(&clean); - remove(&damaged); -} +/// The page size the native targets see: they never set `PRAGMA page_size`. +const PAGE_SIZE: usize = 4096; /// Whether `log` is a full, successful dump: no failed step, ending in an `ok` integrity check. fn clean_read(log: &[Record]) -> bool { @@ -133,7 +60,7 @@ fn clean_read(log: &[Record]) -> bool { /// When SQLite3MC refuses a setting or step the harness generates as valid, or an oracle fails. pub fn run_native(case: &NativeCase) { static SERIAL: Mutex<()> = Mutex::new(()); - let _serial = begin_run(&SERIAL, case); + let _serial = begin_run(&SERIAL); let dir = WORKDIR.as_path(); let clean = dir.join("native-clean.db"); @@ -173,7 +100,7 @@ pub fn run_native(case: &NativeCase) { ); } - if cipher.authenticated() { + if case.config.authenticated() { let original = fs::read(&clean).expect("the clean file exists"); let mut bytes = original.clone(); let flips = &case.damage[..case.damage.len().min(MAX_FLIPS)]; @@ -184,12 +111,19 @@ pub fn run_native(case: &NativeCase) { // docs state); tampering it is never expected to be caught. let unauthenticated = usize::from(case.config.plaintext_header_size.unwrap_or(0)).max(CIPHER_PAGE1_OFFSET); + // `sqlcipher` with a SHA1 HMAC stores its digest in a 32-byte per-page slot but verifies + // only the first 20 bytes, so each page's last 12 slot bytes are unauthenticated. + let page_pad = usize::from( + cipher == Cipher::SqlCipher && case.config.hmac_algorithm == Some(Algorithm::Sha1), + ) * 12; if !bytes.is_empty() && !flips.is_empty() { let len = u64::try_from(bytes.len()).expect("file length fits u64"); for flip in flips { let offset = usize::try_from(u64::from(flip.offset) % len) .expect("offset is below the length"); - if offset < unauthenticated { + if offset < unauthenticated + || (page_pad > 0 && offset % PAGE_SIZE >= PAGE_SIZE - page_pad) + { continue; } bytes[offset] ^= flip.mask; @@ -215,7 +149,7 @@ pub fn run_native(case: &NativeCase) { /// Everything SQLite3MC can read back from `path` with `config`'s key, up to the first failure. fn dump_native(path: &Path, config: &NativeConfig) -> Vec { - dump(&SQLITE3MC, path, |c| configure_native(c, config)) + dump(path, |c| configure_native(c, config)) } /// Runs `case` against SQLite3MC's rekey support. @@ -230,7 +164,7 @@ fn dump_native(path: &Path, config: &NativeConfig) -> Vec { /// valid, or an oracle fails. pub fn run_rekey(case: &RekeyCase) { static SERIAL: Mutex<()> = Mutex::new(()); - let _serial = begin_run(&SERIAL, case); + let _serial = begin_run(&SERIAL); let dir = WORKDIR.as_path(); let clean = dir.join("rekey.db"); @@ -275,7 +209,7 @@ pub fn run_rekey(case: &RekeyCase) { RekeyAction::Decrypt => None, }; let own = after_config.as_ref().map_or_else( - || dump(&SQLITE3MC, &clean, |_| Ok(())), + || dump(&clean, |_| Ok(())), |config| dump_native(&clean, config), ); assert!( @@ -316,7 +250,7 @@ pub fn run_rekey(case: &RekeyCase) { /// When SQLite3MC refuses a setting or step the harness generates as valid, or the oracle fails. pub fn run_wal(case: &WalCase) { static SERIAL: Mutex<()> = Mutex::new(()); - let _serial = begin_run(&SERIAL, case); + let _serial = begin_run(&SERIAL); let dir = WORKDIR.as_path(); let clean = dir.join("wal.db"); @@ -389,7 +323,7 @@ pub fn run_wal(case: &WalCase) { /// file fails to read back cleanly afterward. pub fn run_attach(case: &AttachCase) { static SERIAL: Mutex<()> = Mutex::new(()); - let _serial = begin_run(&SERIAL, case); + let _serial = begin_run(&SERIAL); let dir = WORKDIR.as_path(); let main_path = dir.join("attach-main.db"); @@ -514,7 +448,7 @@ fn write_native(path: &Path, config: &NativeConfig, rows: usize) { /// after a failed `ATTACH` is skipped; every other step is tolerated to fail, a wrong or /// inherited key means they are expected to. fn exercise_attached( - connection: &Connection<'_>, + connection: &Connection, index: usize, attached: &AttachedDb, cross_copy: bool, @@ -557,10 +491,7 @@ fn exercise_attached( /// identically, every cipher setting but the key and the `PRAGMA page_size` equal, has to reopen /// cleanly with the destination's own true key after the backup reports full success. A failure /// there is the defect `utelle/SQLite3MultipleCiphers` issue #158 described, and exactly what this -/// harness exists to catch a recurrence of. The one identically laid out pair exempt is a -/// destination that hides its page size and has `PRAGMA page_size` below the page size SQLite3MC -/// assumes on reopen, `legacy_page_size` or 4096 bytes, which it cannot reopen at all, backup or -/// not (`upstream/sqlite3mc-rc4-short-read-skips-decryption.md`). +/// harness exists to catch a recurrence of. /// /// # Panics /// @@ -568,7 +499,7 @@ fn exercise_attached( /// two identically laid out databases reported full success and left an unreadable destination. pub fn run_backup(case: &BackupCase) { static SERIAL: Mutex<()> = Mutex::new(()); - let _serial = begin_run(&SERIAL, case); + let _serial = begin_run(&SERIAL); let dir = WORKDIR.as_path(); let src_path = dir.join("backup-src.db"); @@ -586,10 +517,6 @@ pub fn run_backup(case: &BackupCase) { && finish_result.is_ok() && case.src_page_size == case.dest_page_size && case.src.same_layout(&case.dest) - && !(case.dest.hides_page_size() - && case - .dest_page_size - .is_some_and(|size| size.get() < case.dest.page_size.unwrap_or(4096))) { drop(src); drop(dest); @@ -611,12 +538,12 @@ pub fn run_backup(case: &BackupCase) { /// Opens a new SQLite3MC file at `path`, stages and applies `config`'s cipher and key, sets /// `page_size` through `PRAGMA page_size` first when given, then runs `ops`. -fn open_for_backup<'a>( +fn open_for_backup( path: &Path, config: &NativeConfig, page_size: Option, ops: &[Op], -) -> Connection<'a> { +) -> Connection { let connection = open_native(path, config, "file"); if let Some(page_size) = page_size { let text = sql(&format!("PRAGMA page_size={}", page_size.get())); @@ -636,7 +563,7 @@ fn open_for_backup<'a>( /// key when given, omitting the `KEY` clause entirely otherwise (which SQLite3MC resolves by /// copying the main database's own current codec verbatim). Returns whether it succeeded. fn attach( - connection: &Connection<'_>, + connection: &Connection, db_name: &str, path_str: &str, attach_key: Option<&NativeConfig>, @@ -679,10 +606,9 @@ fn attach( /// codec (`utelle/SQLite3MultipleCiphers` issue #281), so such a run skips the file check. pub fn run_shared_cache(case: &SharedCacheCase) { static SERIAL: Mutex<()> = Mutex::new(()); - let _serial = begin_run(&SERIAL, case); + let _serial = begin_run(&SERIAL); - crate::db::enable_shared_cache(&SQLITE3MC, true) - .expect("SQLite3MC accepts sqlite3_enable_shared_cache(1)"); + crate::db::enable_shared_cache(true).expect("SQLite3MC accepts sqlite3_enable_shared_cache(1)"); let dir = WORKDIR.as_path(); let path = dir.join("shared-cache.db"); @@ -702,11 +628,9 @@ pub fn run_shared_cache(case: &SharedCacheCase) { let mut one_key = true; let mut foreign_write = false; for sibling in case.siblings.iter().take(MAX_SIBLINGS) { - let Ok(connection) = Connection::open( - &SQLITE3MC, - &c_path(&path), - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, - ) else { + let Ok(connection) = + Connection::open(&c_path(&path), SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) + else { continue; }; let config = match &sibling.key { @@ -750,20 +674,6 @@ pub fn run_shared_cache(case: &SharedCacheCase) { remove(&path); } -const fn api(side: Side) -> &'static Api { - match side { - Side::SqlCipher => &SQLCIPHER, - Side::Sqlite3mc => &SQLITE3MC, - } -} - -const fn name(side: Side) -> &'static str { - match side { - Side::SqlCipher => "SQLCipher", - Side::Sqlite3mc => "SQLite3MC", - } -} - /// A per-process directory on tmpfs where there is one. static WORKDIR: LazyLock = LazyLock::new(|| { let shm = Path::new("/dev/shm"); @@ -777,17 +687,12 @@ static WORKDIR: LazyLock = LazyLock::new(|| { dir }); -/// Locks `serial` and reseeds the deterministic entropy stream from `case`, so the same case -/// always draws the same salts and IVs on replay, but different cases are not forced to start -/// from the identical stream. The returned guard must stay alive for the whole run, SQLCipher is -/// compiled with no-op mutexes and every execution reuses one directory. -fn begin_run<'a>(serial: &'a Mutex<()>, case: &impl Debug) -> MutexGuard<'a, ()> { +/// Locks `serial` so the fixed file names in the shared work directory see one run at a time. +/// The guard must stay alive for the whole run. +fn begin_run(serial: &Mutex<()>) -> MutexGuard<'_, ()> { let guard = serial .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - std::hash::Hash::hash(&format!("{case:?}"), &mut hasher); - crate::entropy::reset(std::hash::Hasher::finish(&hasher)); guard } @@ -813,33 +718,6 @@ fn hex(bytes: &[u8], out: &mut String) { } } -fn key_bytes(key: &Key) -> Vec { - match key { - Key::Raw(key) => { - let mut text = String::from("x'"); - hex(key, &mut text); - text.push('\''); - text.into_bytes() - } - Key::RawWithSalt { key, salt, .. } => { - let mut text = String::from("x'"); - hex(key, &mut text); - hex(salt, &mut text); - text.push('\''); - text.into_bytes() - } - Key::Passphrase { bytes, .. } => bytes.clone(), - } -} - -/// Applies `config` and the key, returning only a refused key as an observation. -fn configure(connection: &Connection<'_>, side: Side, config: &Config) -> Result<(), Code> { - match side { - Side::Sqlite3mc => configure_sqlite3mc(connection, config), - Side::SqlCipher => configure_sqlcipher(connection, config), - } -} - const fn sqlite3mc_algorithm(algorithm: Algorithm) -> c_int { match algorithm { Algorithm::Sha1 => 0, @@ -848,95 +726,6 @@ const fn sqlite3mc_algorithm(algorithm: Algorithm) -> c_int { } } -fn configure_sqlite3mc(connection: &Connection<'_>, config: &Config) -> Result<(), Code> { - let cipher = connection.sqlite3mc_cipher_index(c"sqlcipher"); - assert_eq!( - connection.sqlite3mc_config(c"cipher", cipher), - cipher, - "SQLite3MC refused its sqlcipher scheme" - ); - let set = |param: &CStr, value: c_int| { - let set = connection.sqlite3mc_config_cipher(c"sqlcipher", param, value); - assert_eq!(set, value, "SQLite3MC refused {param:?} = {value}"); - }; - // `legacy` first, since it resets the other parameters to that version's defaults. - set(c"legacy", c_int::from(config.compatibility)); - if let Some(page_size) = config.page_size { - set( - c"legacy_page_size", - c_int::try_from(page_size).expect("page sizes fit an int"), - ); - } - if let Some(algorithm) = config.kdf_algorithm { - set(c"kdf_algorithm", sqlite3mc_algorithm(algorithm)); - } - if let Some(algorithm) = config.hmac_algorithm { - set(c"hmac_algorithm", sqlite3mc_algorithm(algorithm)); - } - if let Some(hmac) = config.hmac { - set(c"hmac_use", c_int::from(hmac)); - } - if let Key::Passphrase { kdf_iter, .. } = config.key { - set(c"kdf_iter", c_int::from(kdf_iter)); - } - if config.plaintext_header() > 0 { - set( - c"plaintext_header_size", - c_int::from(config.plaintext_header()), - ); - } - connection.key(&key_bytes(&config.key)) -} - -fn configure_sqlcipher(connection: &Connection<'_>, config: &Config) -> Result<(), Code> { - // SQLCipher ignores cipher pragmas until the key has created the codec. - connection.key(&key_bytes(&config.key))?; - let pragma = |text: String| { - let result = connection.execute(&sql(&text), &[]); - assert_eq!(result, Ok(()), "SQLCipher refused {text}"); - }; - pragma(format!( - "PRAGMA cipher_compatibility = {}", - config.compatibility - )); - if let Some(page_size) = config.page_size { - pragma(format!("PRAGMA cipher_page_size = {page_size}")); - } - let hash = |algorithm| match algorithm { - Algorithm::Sha1 => "SHA1", - Algorithm::Sha256 => "SHA256", - Algorithm::Sha512 => "SHA512", - }; - if let Some(algorithm) = config.kdf_algorithm { - pragma(format!( - "PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_{}", - hash(algorithm) - )); - } - if let Some(algorithm) = config.hmac_algorithm { - pragma(format!( - "PRAGMA cipher_hmac_algorithm = HMAC_{}", - hash(algorithm) - )); - } - if let Some(hmac) = config.hmac { - pragma(format!( - "PRAGMA cipher_use_hmac = {}", - if hmac { "ON" } else { "OFF" } - )); - } - if let Key::Passphrase { kdf_iter, .. } = config.key { - pragma(format!("PRAGMA kdf_iter = {kdf_iter}")); - } - if config.plaintext_header() > 0 { - pragma(format!( - "PRAGMA cipher_plaintext_header_size = {}", - config.plaintext_header() - )); - } - Ok(()) -} - /// `key`'s bytes, truncated to `key_len` where that is shorter than the form's own natural /// length, matching what [`Cipher::key_len`] says the scheme actually consumes. fn native_key_bytes(key: &NativeKey, key_len: usize) -> Vec { @@ -1008,9 +797,9 @@ const fn aegis_algorithm(select_256: bool) -> c_int { /// Sets every SQLite3MC cipher parameter `config` carries, without touching the key. The /// resulting pending state is consumed by whichever key-setup call runs next on this /// connection: [`Connection::key`], `ATTACH ... KEY`, or [`Connection::rekey`]. -fn stage_cipher(connection: &Connection<'_>, config: &NativeConfig) { +fn stage_cipher(connection: &Connection, config: &NativeConfig) { let cipher_name = CString::new(config.cipher.name()).expect("cipher names have no NUL"); - let index = connection.sqlite3mc_cipher_index(&cipher_name); + let index = Connection::sqlite3mc_cipher_index(&cipher_name); assert_eq!( connection.sqlite3mc_config(c"cipher", index), index, @@ -1026,14 +815,20 @@ fn stage_cipher(connection: &Connection<'_>, config: &NativeConfig) { config.cipher.name() ); }; - if let Some(legacy) = config.legacy { - set(c"legacy", c_int::from(legacy)); + // `legacy` first, since setting it resets the other parameters to that version's defaults. + if config.cipher.has_legacy_param() { + set(c"legacy", 0); } - if let Some(page_size) = config.page_size { - set( - c"legacy_page_size", - c_int::try_from(page_size).expect("page sizes fit an int"), - ); + if matches!(config.cipher, Cipher::SqlCipher) { + if let Some(algorithm) = config.kdf_algorithm { + set(c"kdf_algorithm", sqlite3mc_algorithm(algorithm)); + } + if let Some(algorithm) = config.hmac_algorithm { + set(c"hmac_algorithm", sqlite3mc_algorithm(algorithm)); + } + if let Some(hmac_use) = config.hmac_use { + set(c"hmac_use", c_int::from(hmac_use)); + } } if matches!(config.cipher, Cipher::Aegis) { set(c"algorithm", aegis_algorithm(config.aegis_algorithm_256)); @@ -1050,7 +845,7 @@ fn stage_cipher(connection: &Connection<'_>, config: &NativeConfig) { /// Applies `config` and the key to SQLite3MC's own native scheme, returning only a refused key as /// an observation. -fn configure_native(connection: &Connection<'_>, config: &NativeConfig) -> Result<(), Code> { +fn configure_native(connection: &Connection, config: &NativeConfig) -> Result<(), Code> { stage_cipher(connection, config); connection.key(&native_key_bytes( &config.key, @@ -1061,19 +856,15 @@ fn configure_native(connection: &Connection<'_>, config: &NativeConfig) -> Resul /// Opens a new SQLite3MC file at `path` and applies `config`'s cipher and key, panicking with /// `what` (e.g. `"file"`, `"main database"`) naming the file in the message if SQLite3MC refuses /// to create or key it. -fn open_native<'a>(path: &Path, config: &NativeConfig, what: &str) -> Connection<'a> { +fn open_native(path: &Path, config: &NativeConfig, what: &str) -> Connection { let cipher = config.cipher; - let connection = Connection::open( - &SQLITE3MC, - &c_path(path), - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, - ) - .unwrap_or_else(|code| { - panic!( - "SQLite3MC cannot create a {} {what}: {code:?}", - cipher.name() - ) - }); + let connection = Connection::open(&c_path(path), SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) + .unwrap_or_else(|code| { + panic!( + "SQLite3MC cannot create a {} {what}: {code:?}", + cipher.name() + ) + }); if let Err(code) = configure_native(&connection, config) { panic!( "SQLite3MC refused the key of a new {} {what}: {code:?}", @@ -1131,25 +922,10 @@ impl<'a> Owned<'a> { } } -/// Creates the database and runs the workload, whose steps are all valid SQL. -fn write(side: Side, path: &Path, case: &Case) { - let connection = Connection::open( - api(side), - &c_path(path), - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, - ) - .unwrap_or_else(|code| panic!("{} cannot create a database: {code:?}", name(side))); - if let Err(code) = configure(&connection, side, &case.config) { - panic!("{} refused the key of a new database: {code:?}", name(side)); - } - run_workload(&connection, name(side), &case.ops, case); -} - /// Runs `ops` against an already-open, already-configured connection, panicking with `label` and -/// `context`'s own Debug output naming what failed. Shared by the differential writer and any -/// other input model that drives a workload against one connection it expects to always work. +/// `context`'s own Debug output naming what failed. pub(crate) fn run_workload( - connection: &Connection<'_>, + connection: &Connection, label: &str, ops: &[Op], context: &D, @@ -1167,7 +943,7 @@ pub(crate) fn run_workload( /// Runs `ops` against an already-open, already-configured connection, silently stopping at the /// first statement that fails instead of panicking. For a connection whose own key or cipher may /// be wrong on purpose, where failure is expected and not itself a defect. -pub(crate) fn run_workload_tolerant(connection: &Connection<'_>, ops: &[Op]) { +pub(crate) fn run_workload_tolerant(connection: &Connection, ops: &[Op]) { run_workload_with(connection, ops, |connection, text, params, _op| { connection.execute(&sql(text), params).is_ok() }); @@ -1177,9 +953,9 @@ pub(crate) fn run_workload_tolerant(connection: &Connection<'_>, ops: &[Op]) { /// `run` reports whether its statement succeeded, which decides whether dependent state (a table /// now existing, a transaction now open) advances or the op is treated as never having happened. fn run_workload_with( - connection: &Connection<'_>, + connection: &Connection, ops: &[Op], - mut run: impl FnMut(&Connection<'_>, &str, &[Param<'_>], &Op) -> bool, + mut run: impl FnMut(&Connection, &str, &[Param<'_>], &Op) -> bool, ) { let mut created = [false; 3]; // Tables created inside a transaction disappear again on rollback. @@ -1247,15 +1023,11 @@ fn run_workload_with( } } -/// Everything `api` can read from the file at `path` with the key `configure` applies, up to the -/// first failure. -fn dump( - api: &Api, - path: &Path, - configure: impl FnOnce(&Connection<'_>) -> Result<(), Code>, -) -> Vec { +/// Everything SQLite3MC can read from the file at `path` with the key `configure` applies, up +/// to the first failure. +fn dump(path: &Path, configure: impl FnOnce(&Connection) -> Result<(), Code>) -> Vec { let mut log = Vec::new(); - let connection = match Connection::open(api, &c_path(path), SQLITE_OPEN_READONLY) { + let connection = match Connection::open(&c_path(path), SQLITE_OPEN_READONLY) { Ok(connection) => connection, Err(code) => { log.push(Record::Failed("open", code)); @@ -1287,12 +1059,7 @@ fn dump( type Step = Result<(), (&'static str, Code)>; -fn rows( - connection: &Connection<'_>, - stage: &'static str, - query: &CStr, - log: &mut Vec, -) -> Step { +fn rows(connection: &Connection, stage: &'static str, query: &CStr, log: &mut Vec) -> Step { let mut statement = connection.prepare(query).map_err(|code| (stage, code))?; while let Some(row) = statement.next_row().map_err(|code| (stage, code))? { log.push(Record::Row(stage, row)); @@ -1301,7 +1068,7 @@ fn rows( } /// Logs every page's content area, cutting the reserved bytes page 1's header byte 20 names. -fn pages(connection: &Connection<'_>, log: &mut Vec) -> Step { +fn pages(connection: &Connection, log: &mut Vec) -> Step { const STAGE: &str = "sqlite_dbpage"; let mut statement = connection .prepare(c"SELECT pgno, data FROM sqlite_dbpage ORDER BY pgno") @@ -1322,58 +1089,6 @@ fn pages(connection: &Connection<'_>, log: &mut Vec) -> Step { Ok(()) } -fn compare(case: &Case, file: &str, a: Side, a_log: &[Record], b: Side, b_log: &[Record]) { - if a_log == b_log { - return; - } - let at = a_log.iter().zip(b_log).take_while(|(x, y)| x == y).count(); - let (sqlcipher, sqlite3mc) = if a == Side::SqlCipher { - (a_log, b_log) - } else { - (b_log, a_log) - }; - if case.config.reserved() > 16 - && swallowed_hmac_failure( - at, - case.config.plaintext_header() > 0, - sqlcipher.get(at), - sqlite3mc.get(at), - ) - { - return; - } - panic!( - "{} and {} disagree on {file} from record {at} (of {} and {}).\n{}:{}\n{}:{}\nfor {case:#?}", - name(a), - name(b), - a_log.len(), - b_log.len(), - name(a), - describe(&a_log[at..]), - name(b), - describe(&b_log[at..]), - ); -} - -/// SQLCipher 4.19.0 zero-fills a page that fails its HMAC check instead of reporting the failure. -fn swallowed_hmac_failure( - at: usize, - plaintext_header: bool, - sqlcipher: Option<&Record>, - sqlite3mc: Option<&Record>, -) -> bool { - match (sqlcipher, sqlite3mc) { - (Some(Record::Page(_, data)), Some(Record::Failed("sqlite_dbpage", Code(11)))) => { - data.iter().all(|&b| b == 0) - } - ( - Some(Record::Failed("sqlite_dbpage", Code(11))), - Some(Record::Failed("sqlite_dbpage", Code(26))), - ) => at == 0 && plaintext_header, - _ => false, - } -} - /// Up to six records, one per line, each cut to a readable length. fn describe<'r>(records: impl IntoIterator) -> String { let mut out = String::new(); diff --git a/fuzz/tests/replay.rs b/fuzz/tests/replay.rs index 49015c3..ee64f37 100644 --- a/fuzz/tests/replay.rs +++ b/fuzz/tests/replay.rs @@ -1,33 +1,13 @@ -//! Replays the committed corpora, differential against both libraries, native for -//! self-consistency, rekey for its own round-trip, WAL mode for its own crash tolerance, ATTACH -//! for cross-database isolation, the backup API for its own compatibility guard, and -//! shared-cache mode for not corrupting a sibling connection's live session, without libFuzzer. +//! Replays the committed corpora for self-consistency across the native cipher schemes, rekey +//! round-trips, WAL-mode crash tolerance, `ATTACH DATABASE` cross-database isolation, the backup +//! API's compatibility guard, and shared-cache mode, without libFuzzer. use arbitrary::{Arbitrary, Unstructured}; use sqlite3mc_src_fuzz::{ - run, run_attach, run_backup, run_native, run_rekey, run_shared_cache, run_wal, AttachCase, - BackupCase, Case, NativeCase, RekeyCase, SharedCacheCase, WalCase, + run_attach, run_backup, run_native, run_rekey, run_shared_cache, run_wal, AttachCase, + BackupCase, NativeCase, RekeyCase, SharedCacheCase, WalCase, }; -#[test] -fn committed_corpus_agrees_with_sqlcipher() { - let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/differential"); - let mut seeds: Vec<_> = std::fs::read_dir(dir) - .expect("the committed corpus exists") - .map(|entry| entry.expect("the corpus is readable").path()) - .collect(); - seeds.sort(); - assert!(!seeds.is_empty(), "{dir} holds no seeds"); - for seed in seeds { - let bytes = std::fs::read(&seed).expect("seeds are readable"); - // libFuzzer's `fuzz_target!` decodes the same way, taking the rest of the input last. - let case = Case::arbitrary_take_rest(Unstructured::new(&bytes)) - .unwrap_or_else(|e| panic!("{} does not decode: {e}", seed.display())); - eprintln!("{}", seed.display()); - run(&case); - } -} - #[test] fn committed_native_corpus_is_self_consistent() { let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/native");