From 556eaa133e9cfe7f4c8e542708119fdfd311357e Mon Sep 17 00:00:00 2001 From: Doan Luu Duc Tai Date: Fri, 30 Oct 2026 21:39:57 -0500 Subject: Implement experimental file-based encrypted private chat Add fixed-suite Rust/WASI identity files, signed challenge authentication, ciphertext-only SQLite storage, a minimal LibreJS-compatible interface, and consistent backup snapshots. Keep production unchanged and document the local workflow, limits, source distribution, and browser recovery tests. --- src/archive.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) (limited to 'src/archive.rs') diff --git a/src/archive.rs b/src/archive.rs index e7e6bc5..03ec643 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -103,6 +103,25 @@ fn snapshot(project: &Project, records: &[Value], stage: &Path) -> Result<()> { resume } +fn snapshot_chat(root: &Path, stage: &Path) -> Result<()> { + let source = root.join("var/data/chat/chat.sqlite"); + if !source.is_file() { + return Ok(()); + } + let directory = stage.join("var/data/chat"); + files::private_dir(&directory)?; + let database = + rusqlite::Connection::open_with_flags(source, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?; + database.busy_timeout(std::time::Duration::from_secs(5))?; + let destination = directory.join("chat.sqlite"); + database.backup("main", &destination, None::)?; + fs::set_permissions( + destination, + std::os::unix::fs::PermissionsExt::from_mode(0o600), + )?; + Ok(()) +} + fn snapshot_files(project: &Project, records: &[Value], stage: &Path) -> Result<()> { files::copy_source(&project.root, stage)?; files::copy_tree(&project.root.join(".env"), &stage.join(".env"))?; @@ -112,13 +131,14 @@ fn snapshot_files(project: &Project, records: &[Value], stage: &Path) -> Result< let data = project.root.join("var/data"); for item in fs::read_dir(&data)? { let entry = item?; - if entry.file_name() != "caddy" { + if entry.file_name() != "caddy" && entry.file_name() != "chat" { files::copy_tree( &entry.path(), &stage.join("var/data").join(entry.file_name()), )?; } } + snapshot_chat(&project.root, stage)?; let caddy = records .iter() .find(|record| { @@ -364,6 +384,32 @@ mod tests { use crate::files; use crate::project::{Mode, Project}; + #[test] + fn chat_snapshot_is_consistent_and_private() -> Result<()> { + let workspace = tempfile::tempdir()?; + let root = workspace.path().join("source"); + let stage = workspace.path().join("snapshot"); + files::private_dir(&root.join("var/data/chat"))?; + let connection = rusqlite::Connection::open(root.join("var/data/chat/chat.sqlite"))?; + connection.execute_batch( + "CREATE TABLE messages (body TEXT); INSERT INTO messages VALUES ('ciphertext-before');", + )?; + let transaction = connection.unchecked_transaction()?; + transaction.execute("INSERT INTO messages VALUES ('uncommitted')", [])?; + super::snapshot_chat(&root, &stage)?; + let snapshot = stage.join("var/data/chat/chat.sqlite"); + let restored = rusqlite::Connection::open(&snapshot)?; + let count: i64 = + restored.query_row("SELECT COUNT(*) FROM messages", [], |row| row.get(0))?; + assert_eq!(count, 1); + assert_eq!( + std::os::unix::fs::PermissionsExt::mode(&fs::metadata(snapshot)?.permissions()) & 0o777, + 0o600 + ); + transaction.rollback()?; + Ok(()) + } + #[test] fn restore_is_local_and_refuses_an_existing_destination() -> Result<()> { let workspace = tempfile::tempdir()?; -- cgit v1.2.3