mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-18 09:04:19 +01:00
add tests for checking encryption tampering
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user