|
| 1 | +//! The encrypted files native and Wasm SQLite3MC hand each other, and the checks both sides run on them. |
| 2 | +//! The native `interop` binary and the Wasm tests include this file, each next to its own `ffi` module. |
| 3 | +
|
| 4 | +use super::ffi; |
| 5 | +use std::ffi::{c_int, CStr, CString}; |
| 6 | + |
| 7 | +const KEY: &str = "PRAGMA key = 'correct horse battery staple'"; |
| 8 | +const WRONG_KEY: &str = "PRAGMA key = 'wrong horse battery staple'"; |
| 9 | +/// Start of every row the native build writes. |
| 10 | +pub const NATIVE: &str = "written natively"; |
| 11 | +/// Start of every row the Wasm build writes. |
| 12 | +pub const WASM: &str = "written by Wasm"; |
| 13 | +/// Enough rows of about 100 bytes to fill several pages. |
| 14 | +const ROWS: u32 = 400; |
| 15 | +const SQLCIPHER: &str = "PRAGMA cipher = 'sqlcipher'"; |
| 16 | + |
| 17 | +/// Every cipher SQLite3MC ships, by file name, as the pragmas that select it before `PRAGMA key`. |
| 18 | +/// A reader that skips the last pragma of a cipher must fail, which proves the pragmas took effect. |
| 19 | +pub const CIPHERS: &[(&str, &[&str])] = &[ |
| 20 | + ("chacha20", &[]), |
| 21 | + ("aes128cbc", &["PRAGMA cipher = 'aes128cbc'"]), |
| 22 | + ("aes256cbc", &["PRAGMA cipher = 'aes256cbc'"]), |
| 23 | + ("sqlcipher1", &[SQLCIPHER, "PRAGMA legacy = 1"]), |
| 24 | + ("sqlcipher2", &[SQLCIPHER, "PRAGMA legacy = 2"]), |
| 25 | + ("sqlcipher3", &[SQLCIPHER, "PRAGMA legacy = 3"]), |
| 26 | + ("sqlcipher4", &[SQLCIPHER, "PRAGMA legacy = 4"]), |
| 27 | + ("rc4", &["PRAGMA cipher = 'rc4'"]), |
| 28 | + ("ascon128", &["PRAGMA cipher = 'ascon128'"]), |
| 29 | + ("aegis", &["PRAGMA cipher = 'aegis'"]), |
| 30 | +]; |
| 31 | + |
| 32 | +/// A connection to `name` in the side's default VFS, which fails the test on any SQLite error. |
| 33 | +pub struct Db(*mut ffi::sqlite3, String); |
| 34 | + |
| 35 | +impl Db { |
| 36 | + /// Opens `name` read-write, creating it if missing, and runs `pragmas`. |
| 37 | + /// |
| 38 | + /// # Panics |
| 39 | + /// |
| 40 | + /// If opening or a pragma fails. |
| 41 | + #[must_use] |
| 42 | + pub fn open(name: &str, pragmas: &[&str]) -> Self { |
| 43 | + let c_name = CString::new(name).unwrap(); |
| 44 | + let mut handle = std::ptr::null_mut(); |
| 45 | + let flags = ffi::SQLITE_OPEN_READWRITE | ffi::SQLITE_OPEN_CREATE; |
| 46 | + // `c_name` outlives the call, and SQLite copies it. |
| 47 | + let rc = |
| 48 | + unsafe { ffi::sqlite3_open_v2(c_name.as_ptr(), &mut handle, flags, std::ptr::null()) }; |
| 49 | + let db = Self(handle, name.to_owned()); |
| 50 | + assert_eq!(rc, ffi::SQLITE_OK, "{name}: {}", db.error()); |
| 51 | + for pragma in pragmas { |
| 52 | + db.exec(pragma); |
| 53 | + } |
| 54 | + db |
| 55 | + } |
| 56 | + |
| 57 | + /// Runs `sql` and returns SQLite's result code. |
| 58 | + fn status(&self, sql: &str) -> c_int { |
| 59 | + let sql = CString::new(sql).unwrap(); |
| 60 | + // `sql` outlives the call, and no callback or error pointer is passed. |
| 61 | + unsafe { |
| 62 | + let (arg, errmsg) = (std::ptr::null_mut(), std::ptr::null_mut()); |
| 63 | + ffi::sqlite3_exec(self.0, sql.as_ptr(), None, arg, errmsg) |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + fn exec(&self, sql: &str) { |
| 68 | + let rc = self.status(sql); |
| 69 | + assert_eq!(rc, ffi::SQLITE_OK, "{}: {sql}: {}", self.1, self.error()); |
| 70 | + } |
| 71 | + |
| 72 | + /// First column of the first row of `sql`, as text. |
| 73 | + /// |
| 74 | + /// # Panics |
| 75 | + /// |
| 76 | + /// If it fails or returns no text. |
| 77 | + #[must_use] |
| 78 | + pub fn text(&self, sql: &str) -> String { |
| 79 | + let c_sql = CString::new(sql).unwrap(); |
| 80 | + let mut stmt = std::ptr::null_mut(); |
| 81 | + // `c_sql` outlives the call, which reads it up to its NUL. |
| 82 | + let rc = unsafe { |
| 83 | + ffi::sqlite3_prepare_v2(self.0, c_sql.as_ptr(), -1, &mut stmt, std::ptr::null_mut()) |
| 84 | + }; |
| 85 | + assert_eq!(rc, ffi::SQLITE_OK, "{}: {sql}: {}", self.1, self.error()); |
| 86 | + // `stmt` was just prepared, and the text is copied before it is finalized. |
| 87 | + let text = unsafe { |
| 88 | + let value = if ffi::sqlite3_step(stmt) == ffi::SQLITE_ROW { |
| 89 | + ffi::sqlite3_column_text(stmt, 0) |
| 90 | + } else { |
| 91 | + std::ptr::null() |
| 92 | + }; |
| 93 | + let text = (!value.is_null()) |
| 94 | + .then(|| CStr::from_ptr(value.cast()).to_string_lossy().into_owned()); |
| 95 | + let error = self.error(); |
| 96 | + ffi::sqlite3_finalize(stmt); |
| 97 | + text.ok_or(error) |
| 98 | + }; |
| 99 | + text.unwrap_or_else(|error| panic!("{}: {sql}: {error}", self.1)) |
| 100 | + } |
| 101 | + |
| 102 | + fn error(&self) -> String { |
| 103 | + // SQLite returns a NUL-terminated message for any handle, even a null one. |
| 104 | + unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(self.0)) } |
| 105 | + .to_string_lossy() |
| 106 | + .into_owned() |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +impl Drop for Db { |
| 111 | + fn drop(&mut self) { |
| 112 | + // The handle came from `sqlite3_open_v2`, and every statement on it is finalized. |
| 113 | + unsafe { ffi::sqlite3_close(self.0) }; |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +/// Writes `name` in the format `pragmas` select, with rows starting with `marker`. |
| 118 | +pub fn write(name: &str, pragmas: &[&str], marker: &str) { |
| 119 | + Db::open(name, &[pragmas, &[KEY]].concat()).exec(&format!( |
| 120 | + "CREATE TABLE t(v TEXT); |
| 121 | + WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < {ROWS}) |
| 122 | + INSERT INTO t SELECT '{marker} ' || i || ' ' || hex(zeroblob(40)) FROM n;" |
| 123 | + )); |
| 124 | +} |
| 125 | + |
| 126 | +/// Asserts `bytes`, the file `name` holding rows that start with `marker`, is encrypted on every page, |
| 127 | +/// and that it opens with its key and pragmas, but not with a wrong key or without its last pragma. |
| 128 | +/// |
| 129 | +/// # Panics |
| 130 | +/// |
| 131 | +/// If any of that does not hold. |
| 132 | +pub fn check(name: &str, bytes: &[u8], pragmas: &[&str], marker: &str) { |
| 133 | + assert!(bytes.len() > 16 * 1024, "{name} is {} bytes", bytes.len()); |
| 134 | + for plain in [ |
| 135 | + b"SQLite format 3".as_slice(), |
| 136 | + b"CREATE TABLE", |
| 137 | + marker.as_bytes(), |
| 138 | + ] { |
| 139 | + let leak = bytes.windows(plain.len()).any(|w| w == plain); |
| 140 | + assert!(!leak, "{name} holds {:?}", String::from_utf8_lossy(plain)); |
| 141 | + } |
| 142 | + let db = Db::open(name, &[pragmas, &[KEY]].concat()); |
| 143 | + assert_eq!(db.text("PRAGMA quick_check"), "ok", "{name}"); |
| 144 | + let count = db.text(&format!("SELECT count(*) FROM t WHERE v LIKE '{marker} %'")); |
| 145 | + assert_eq!(count, ROWS.to_string(), "{name}"); |
| 146 | + let probe = "SELECT count(*) FROM sqlite_schema"; |
| 147 | + let rc = Db::open(name, &[pragmas, &[WRONG_KEY]].concat()).status(probe); |
| 148 | + assert_eq!(rc, ffi::SQLITE_NOTADB, "{name} opened with a wrong key"); |
| 149 | + if let Some((_, fewer)) = pragmas.split_last() { |
| 150 | + let rc = Db::open(name, &[fewer, &[KEY]].concat()).status(probe); |
| 151 | + assert_eq!( |
| 152 | + rc, |
| 153 | + ffi::SQLITE_NOTADB, |
| 154 | + "{name} opened without its last pragma" |
| 155 | + ); |
| 156 | + } |
| 157 | +} |
0 commit comments