From a7237b80eae240be1f0aa51a451aa57b0400876e Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Sat, 27 Sep 2025 17:49:42 +0530 Subject: [PATCH] add tests for checking encryption tampering --- .../query_processing/encryption.rs | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/tests/integration/query_processing/encryption.rs b/tests/integration/query_processing/encryption.rs index 2682d1104..f61c6f0bd 100644 --- a/tests/integration/query_processing/encryption.rs +++ b/tests/integration/query_processing/encryption.rs @@ -196,3 +196,270 @@ fn test_non_4k_page_size_encryption() -> anyhow::Result<()> { Ok(()) } + +#[test] +fn test_corruption_turso_magic_bytes() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let db_name = format!("test-corruption-magic-{}.db", rng().next_u32()); + let tmp_db = TempDatabase::new(&db_name, false); + let db_path = tmp_db.path.clone(); + + { + let conn = tmp_db.connect_limbo(); + run_query( + &tmp_db, + &conn, + "PRAGMA hexkey = 'b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327';", + )?; + run_query(&tmp_db, &conn, "PRAGMA cipher = 'aegis256';")?; + run_query( + &tmp_db, + &conn, + "CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT);", + )?; + run_query( + &tmp_db, + &conn, + "INSERT INTO test (value) VALUES ('Test corruption')", + )?; + run_query(&tmp_db, &conn, "PRAGMA wal_checkpoint(TRUNCATE);")?; + do_flush(&conn, &tmp_db)?; + } + + // corrupt the Turso magic bytes by changing "Turso" to "Vurso" (the db name as it was intended) + { + use std::fs::OpenOptions; + use std::io::{Seek, SeekFrom, Write}; + + let mut file = OpenOptions::new().write(true).open(&db_path)?; + + file.seek(SeekFrom::Start(0))?; + file.write_all(b"V")?; + } + + // try to connect to the corrupted database - this should fail + { + let uri = format!( + "file:{}?cipher=aegis256&hexkey=b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327", + db_path.to_str().unwrap() + ); + + let should_panic = panic::catch_unwind(panic::AssertUnwindSafe(|| { + let (_io, conn) = + turso_core::Connection::from_uri(&uri, true, false, false, false).unwrap(); + run_query_on_row(&tmp_db, &conn, "SELECT * FROM test", |_row: &Row| {}).unwrap(); + })); + + assert!( + should_panic.is_err(), + "should panic when accessing encrypted DB with corrupted Turso magic bytes" + ); + } + + Ok(()) +} + +#[test] +fn test_corruption_associated_data_bytes() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let db_name = format!("test-corruption-ad-{}.db", rng().next_u32()); + let tmp_db = TempDatabase::new(&db_name, false); + let db_path = tmp_db.path.clone(); + + { + let conn = tmp_db.connect_limbo(); + run_query( + &tmp_db, + &conn, + "PRAGMA hexkey = 'b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327';", + )?; + run_query(&tmp_db, &conn, "PRAGMA cipher = 'aegis256';")?; + run_query( + &tmp_db, + &conn, + "CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT);", + )?; + run_query( + &tmp_db, + &conn, + "INSERT INTO test (value) VALUES ('Test AD corruption')", + )?; + run_query(&tmp_db, &conn, "PRAGMA wal_checkpoint(TRUNCATE);")?; + do_flush(&conn, &tmp_db)?; + } + + // test corruption at different positions in the header (the first 100 bytes) + let corruption_positions = [3, 7, 16, 30, 50, 70, 99]; + + for &corrupt_pos in &corruption_positions { + let test_db_name = format!( + "test-corruption-ad-pos-{}-{}.db", + corrupt_pos, + rng().next_u32() + ); + let test_tmp_db = TempDatabase::new(&test_db_name, false); + let test_db_path = test_tmp_db.path.clone(); + std::fs::copy(&db_path, &test_db_path)?; + + // corrupt one byte + { + use std::fs::OpenOptions; + use std::io::{Read, Seek, SeekFrom, Write}; + + let mut file = OpenOptions::new() + .read(true) + .write(true) + .open(&test_db_path)?; + + file.seek(SeekFrom::Start(corrupt_pos as u64))?; + let mut original_byte = [0u8; 1]; + file.read_exact(&mut original_byte)?; + + // corrupt it by flipping all bits + let corrupted_byte = [!original_byte[0]]; + + file.seek(SeekFrom::Start(corrupt_pos as u64))?; + file.write_all(&corrupted_byte)?; + } + + // this should fail + { + let uri = format!( + "file:{}?cipher=aegis256&hexkey=b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327", + test_db_path.to_str().unwrap() + ); + + let should_panic = panic::catch_unwind(panic::AssertUnwindSafe(|| { + let (_io, conn) = + turso_core::Connection::from_uri(&uri, true, false, false, false).unwrap(); + run_query_on_row(&test_tmp_db, &conn, "SELECT * FROM test", |_row: &Row| {}) + .unwrap(); + })); + + assert!( + should_panic.is_err(), + "should panic when accessing encrypted DB with corrupted associated data at position {}", + corrupt_pos + ); + } + } + + Ok(()) +} + +#[test] +fn test_turso_header_structure() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + + let verify_header = + |db_path: &str, expected_cipher_id: u8, description: &str| -> anyhow::Result<()> { + use std::fs::File; + use std::io::{Read, Seek, SeekFrom}; + + let mut file = File::open(db_path)?; + let mut header = [0u8; 16]; + file.seek(SeekFrom::Start(0))?; + file.read_exact(&mut header)?; + + assert_eq!( + &header[0..5], + b"Turso", + "Magic bytes should be 'Turso' for {}", + description + ); + assert_eq!( + header[5], 0x00, + "Version should be 0x00 for {}", + description + ); + assert_eq!( + header[6], expected_cipher_id, + "Cipher ID should be {} for {}", + expected_cipher_id, description + ); + + // the unused bytes should be zeroed + for (i, &byte) in header[7..16].iter().enumerate() { + assert_eq!( + byte, + 0, + "Unused byte at position {} should be 0 for {}", + i + 7, + description + ); + } + + println!("Verified {} header: cipher ID = {}", description, header[6]); + Ok(()) + }; + + let test_cases = [ + ( + "aes128gcm", + 1, + "AES-128-GCM", + "b1bbfda4f589dc9daaf004fe21111e00", + ), + ( + "aes256gcm", + 2, + "AES-256-GCM", + "b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327", + ), + ( + "aegis256", + 3, + "AEGIS-256", + "b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327", + ), + ( + "aegis256x2", + 4, + "AEGIS-256X2", + "b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327", + ), + ( + "aegis128l", + 6, + "AEGIS-128L", + "b1bbfda4f589dc9daaf004fe21111e00", + ), + ( + "aegis128x2", + 7, + "AEGIS-128X2", + "b1bbfda4f589dc9daaf004fe21111e00", + ), + ( + "aegis128x4", + 8, + "AEGIS-128X4", + "b1bbfda4f589dc9daaf004fe21111e00", + ), + ]; + + for (cipher_name, expected_id, description, hexkey) in test_cases { + let db_name = format!("test-header-{}-{}.db", cipher_name, rng().next_u32()); + let tmp_db = TempDatabase::new(&db_name, false); + let db_path = tmp_db.path.clone(); + + { + let conn = tmp_db.connect_limbo(); + run_query(&tmp_db, &conn, &format!("PRAGMA hexkey = '{}';", hexkey))?; + run_query( + &tmp_db, + &conn, + &format!("PRAGMA cipher = '{}';", cipher_name), + )?; + run_query( + &tmp_db, + &conn, + "CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT);", + )?; + do_flush(&conn, &tmp_db)?; + } + + verify_header(&db_path.to_str().unwrap(), expected_id, description)?; + } + Ok(()) +}