Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Byte-exact forensic fixtures: git must never translate line endings in them.
# core.autocrlf defaults to TRUE on GitHub windows-latest runners, and git only
# sniffs the first 8000 bytes for a NUL before deciding a file is text.
tests/data/** -text
fuzz/corpus/** -text
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/target
target/

# mkdocs build output (generated by `mkdocs build`; the Pages workflow builds it in CI)
/site/
Expand Down
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ path = "src/lib.rs"
[dependencies]
# Consume KNOWLEDGE-layer format constants instead of re-hardcoding them.
forensicnomicon = { workspace = true }
# SQLCipher decryption primitives — audited RustCrypto crates ONLY, never
# hand-rolled (CLAUDE.core.md: "Never hand-roll a cryptographic primitive").
# PBKDF2 key derivation (HMAC-SHA1/SHA512), AES-256-CBC page decrypt, per-page
# HMAC authentication. All are low-MSRV and keep the reader on rust-version 1.80.
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
hmac = "0.12"
sha1 = "0.10"
sha2 = "0.10"
aes = "0.8"
cbc = "0.1"
cipher = "0.4"

[lints]
workspace = true
25 changes: 25 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
pub mod attribution;
pub mod rebuild;
pub mod row_history;
pub mod sqlcipher;

// The page-1 header field offsets are consumed from the KNOWLEDGE leaf
// (forensicnomicon::sqlite ≥ 1.5.0); the previously-local duplicates were promoted
Expand Down Expand Up @@ -70,6 +71,10 @@ pub enum Error {
/// [`Database::open_path`], not a malformed database). Carries the
/// [`std::io::ErrorKind`] (show-the-unrecognized-value).
Io(std::io::ErrorKind),
/// `SQLCipher` decryption failed (wrong key, unsupported cipher parameters, or
/// a failed page authentication) via [`Database::open_encrypted`]. Carries
/// the underlying [`sqlcipher::DecryptError`] (show-the-unrecognized-value).
Decrypt(sqlcipher::DecryptError),
}

impl From<std::io::Error> for Error {
Expand All @@ -78,6 +83,12 @@ impl From<std::io::Error> for Error {
}
}

impl From<sqlcipher::DecryptError> for Error {
fn from(e: sqlcipher::DecryptError) -> Self {
Error::Decrypt(e)
}
}

/// A freed overflow-page chain could not be followed to a complete, trustworthy
/// payload (task #73): a chain page that is not a freelist leaf (live / trunk /
/// unreachable), a cycle, a premature terminator with bytes still owed, an
Expand Down Expand Up @@ -657,6 +668,20 @@ impl Database {
})
}

/// Decrypt a **`SQLCipher`** database with `key` and open the resulting
/// plaintext, detecting the cipher version automatically (see
/// [`sqlcipher::decrypt`]). The reader then consumes the decrypted byte
/// stream exactly as for a plaintext file — the encryption is transparent
/// past this call.
///
/// Secure-by-default and read-only: a wrong key or unsupported cipher
/// parameters is a loud [`Error::Decrypt`], never a silently-misread
/// database; nothing is written back to the evidence file.
pub fn open_encrypted(bytes: &[u8], key: &sqlcipher::SqlCipherKey) -> Result<Self, Error> {
let decrypted = sqlcipher::decrypt(bytes, key)?;
Self::open(decrypted.plaintext)
}

/// Open a database from a filesystem path with a **bounded-memory paged
/// read** (roadmap §3.1): pages are streamed on demand through a small LRU
/// cache instead of loading the whole file into a `Vec<u8>`, so a multi-GB
Expand Down
Loading
Loading