From 100a0d8e97a2e49a9dd3ef08d5a70b962b711236 Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Tue, 12 Aug 2025 21:40:30 +0530 Subject: [PATCH 01/10] Add encryption module Let's add an encryption module, hard coded to use AES 256 GCM. Other required parameters are also hard coded and will be made configurable in the future PRs. The module is behind a `encryption` feature flag. --- Cargo.lock | 119 +++++++++++++++++++++++ core/Cargo.toml | 3 + core/storage/encryption.rs | 187 +++++++++++++++++++++++++++++++++++++ core/storage/mod.rs | 2 + 4 files changed, 311 insertions(+) create mode 100644 core/storage/encryption.rs diff --git a/Cargo.lock b/Cargo.lock index 4a3170a1b..1d8b43d55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.11" @@ -449,6 +484,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.32" @@ -607,6 +652,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -732,6 +786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -772,6 +827,15 @@ version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "ctrlc" version = "3.4.5" @@ -1335,6 +1399,16 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1681,6 +1755,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "io-uring" version = "0.7.6" @@ -2435,6 +2518,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "option-ext" version = "0.2.0" @@ -2582,6 +2671,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.0" @@ -3362,6 +3463,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "3.0.2" @@ -3850,6 +3957,8 @@ dependencies = [ name = "turso_core" version = "0.1.4-pre.10" dependencies = [ + "aes", + "aes-gcm", "antithesis_sdk", "bitflags 2.9.0", "built", @@ -4101,6 +4210,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index f51f7c810..cd9632931 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -27,6 +27,7 @@ omit_autovacuum = [] simulator = ["fuzz", "serde"] serde = ["dep:serde"] series = [] +encryption = ["dep:aes-gcm", "dep:aes"] [target.'cfg(target_os = "linux")'.dependencies] io-uring = { version = "0.7.5", optional = true } @@ -73,6 +74,8 @@ uuid = { version = "1.11.0", features = ["v4", "v7"], optional = true } tempfile = "3.8.0" pack1 = { version = "1.0.0", features = ["bytemuck"] } bytemuck = "1.23.1" +aes-gcm = { version = "0.10.3", optional = true } +aes = { version = "0.8.4", optional = true } [build-dependencies] chrono = { version = "0.4.38", default-features = false } diff --git a/core/storage/encryption.rs b/core/storage/encryption.rs new file mode 100644 index 000000000..b308096fa --- /dev/null +++ b/core/storage/encryption.rs @@ -0,0 +1,187 @@ +use crate::Result; +use aes_gcm::{ + aead::{Aead, AeadCore, KeyInit, OsRng}, + Aes256Gcm, Key, Nonce, +}; +use std::ops::Deref; + +pub const ENCRYPTION_METADATA_SIZE: usize = 28; +pub const ENCRYPTED_PAGE_SIZE: usize = 4096; +pub const ENCRYPTION_NONCE_SIZE: usize = 12; + +#[repr(transparent)] +#[derive(Clone)] +pub struct EncryptionKey([u8; 32]); + +impl EncryptionKey { + pub fn new(key: [u8; 32]) -> Self { + Self(key) + } + + pub fn from_string(s: &str) -> Self { + let mut key = [0u8; 32]; + let bytes = s.as_bytes(); + let len = bytes.len().min(32); + key[..len].copy_from_slice(&bytes[..len]); + Self(key) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn as_slice(&self) -> &[u8] { + &self.0 + } +} + +impl Deref for EncryptionKey { + type Target = [u8; 32]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8; 32]> for EncryptionKey { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +impl std::fmt::Debug for EncryptionKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EncryptionKey") + .field("key", &"") + .finish() + } +} + +impl Drop for EncryptionKey { + fn drop(&mut self) { + // securely zero out the key bytes before dropping + for byte in self.0.iter_mut() { + unsafe { + std::ptr::write_volatile(byte, 0); + } + } + } +} + +pub fn encrypt_page(page: &[u8], page_id: usize, key: &EncryptionKey) -> Result> { + if page_id == 1 { + tracing::debug!("skipping encryption for page 1 (database header)"); + return Ok(page.to_vec()); + } + tracing::debug!("encrypting page {}", page_id); + assert_eq!( + page.len(), + ENCRYPTED_PAGE_SIZE, + "Page data must be exactly {ENCRYPTED_PAGE_SIZE} bytes" + ); + let reserved_bytes = &page[ENCRYPTED_PAGE_SIZE - ENCRYPTION_METADATA_SIZE..]; + let reserved_bytes_zeroed = reserved_bytes.iter().all(|&b| b == 0); + assert!( + reserved_bytes_zeroed, + "last reserved bytes must be empty/zero, but found non-zero bytes" + ); + let payload = &page[..ENCRYPTED_PAGE_SIZE - ENCRYPTION_METADATA_SIZE]; + let (encrypted, nonce) = encrypt(payload, key)?; + assert_eq!( + encrypted.len(), + ENCRYPTED_PAGE_SIZE - nonce.len(), + "Encrypted page must be exactly {} bytes", + ENCRYPTED_PAGE_SIZE - nonce.len() + ); + let mut result = Vec::with_capacity(ENCRYPTED_PAGE_SIZE); + result.extend_from_slice(&encrypted); + result.extend_from_slice(&nonce); + assert_eq!( + result.len(), + ENCRYPTED_PAGE_SIZE, + "Encrypted page must be exactly {ENCRYPTED_PAGE_SIZE} bytes" + ); + Ok(result) +} + +pub fn decrypt_page(encrypted_page: &[u8], page_id: usize, key: &EncryptionKey) -> Result> { + if page_id == 1 { + tracing::debug!("skipping decryption for page 1 (database header)"); + return Ok(encrypted_page.to_vec()); + } + tracing::debug!("decrypting page {}", page_id); + assert_eq!( + encrypted_page.len(), + ENCRYPTED_PAGE_SIZE, + "Encrypted page data must be exactly {ENCRYPTED_PAGE_SIZE} bytes" + ); + + let nonce_start = encrypted_page.len() - ENCRYPTION_NONCE_SIZE; + let payload = &encrypted_page[..nonce_start]; + let nonce = &encrypted_page[nonce_start..]; + + let decrypted_data = decrypt(payload, nonce, key)?; + assert_eq!( + decrypted_data.len(), + ENCRYPTED_PAGE_SIZE - ENCRYPTION_METADATA_SIZE, + "Decrypted page data must be exactly {} bytes", + ENCRYPTED_PAGE_SIZE - ENCRYPTION_METADATA_SIZE + ); + let mut result = Vec::with_capacity(ENCRYPTED_PAGE_SIZE); + result.extend_from_slice(&decrypted_data); + result.resize(ENCRYPTED_PAGE_SIZE, 0); + assert_eq!( + result.len(), + ENCRYPTED_PAGE_SIZE, + "Decrypted page data must be exactly {ENCRYPTED_PAGE_SIZE} bytes" + ); + Ok(result) +} + +fn encrypt(plaintext: &[u8], key: &EncryptionKey) -> Result<(Vec, Vec)> { + let key: &Key = key.as_ref().into(); + let cipher = Aes256Gcm::new(key); + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let ciphertext = cipher.encrypt(&nonce, plaintext).unwrap(); + Ok((ciphertext, nonce.to_vec())) +} + +fn decrypt(ciphertext: &[u8], nonce: &[u8], key: &EncryptionKey) -> Result> { + let key: &Key = key.as_ref().into(); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(nonce); + let plaintext = cipher.decrypt(nonce, ciphertext).unwrap(); + Ok(plaintext) +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::Rng; + + #[test] + fn test_encrypt_decrypt_round_trip() { + let mut rng = rand::thread_rng(); + let data_size = ENCRYPTED_PAGE_SIZE - ENCRYPTION_METADATA_SIZE; + + let page_data = { + let mut page = vec![0u8; ENCRYPTED_PAGE_SIZE]; + page.iter_mut() + .take(data_size) + .for_each(|byte| *byte = rng.gen()); + page + }; + + let key = EncryptionKey::from_string("alice and bob use encryption on database"); + + let page_id = 42; + let encrypted = encrypt_page(&page_data, page_id, &key).unwrap(); + assert_eq!(encrypted.len(), ENCRYPTED_PAGE_SIZE); + assert_ne!(&encrypted[..data_size], &page_data[..data_size]); + assert_ne!(&encrypted[..], &page_data[..]); + + let decrypted = decrypt_page(&encrypted, page_id, &key).unwrap(); + assert_eq!(decrypted.len(), ENCRYPTED_PAGE_SIZE); + assert_eq!(decrypted, page_data); + } +} diff --git a/core/storage/mod.rs b/core/storage/mod.rs index 944f192fc..bcba83d5a 100644 --- a/core/storage/mod.rs +++ b/core/storage/mod.rs @@ -13,6 +13,8 @@ pub(crate) mod btree; pub(crate) mod buffer_pool; pub(crate) mod database; +#[cfg(feature = "encryption")] +pub(crate) mod encryption; pub(crate) mod page_cache; #[allow(clippy::arc_with_non_send_sync)] pub(crate) mod pager; From 93774ffc3b8cb0b0a87f10cc18072827e2b886dc Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Tue, 12 Aug 2025 21:44:25 +0530 Subject: [PATCH 02/10] Add `PRAGMA key` to set the encryption key If set, set the key for the connection --- core/lib.rs | 14 ++++++++ core/pragma.rs | 4 +++ core/translate/pragma.rs | 35 ++++++++++++++++--- vendored/sqlite3-parser/src/parser/ast/mod.rs | 5 +++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/core/lib.rs b/core/lib.rs index e1e5c6ae4..0d0c737ab 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -76,6 +76,8 @@ use std::{ }; #[cfg(feature = "fs")] use storage::database::DatabaseFile; +#[cfg(feature = "encryption")] +pub use storage::encryption::EncryptionKey; use storage::page_cache::DumbLruPageCache; use storage::pager::{AtomicDbState, DbState}; use storage::sqlite3_ondisk::PageSize; @@ -425,6 +427,8 @@ impl Database { view_transaction_states: RefCell::new(HashMap::new()), metrics: RefCell::new(ConnectionMetrics::new()), is_nested_stmt: Cell::new(false), + #[cfg(feature = "encryption")] + encryption_key: RefCell::new(None), }); let builtin_syms = self.builtin_syms.borrow(); // add built-in extensions symbols to the connection to prevent having to load each time @@ -852,6 +856,8 @@ pub struct Connection { /// Whether the connection is executing a statement initiated by another statement. /// Generally this is only true for ParseSchema. is_nested_stmt: Cell, + #[cfg(feature = "encryption")] + encryption_key: RefCell>, } impl Connection { @@ -1925,6 +1931,14 @@ impl Connection { pub fn get_syms_vtab_mods(&self) -> std::collections::HashSet { self.syms.borrow().vtab_modules.keys().cloned().collect() } + + #[cfg(feature = "encryption")] + pub fn set_encryption_key(&self, key: Option) { + tracing::trace!("setting encryption key for connection"); + *self.encryption_key.borrow_mut() = key.clone(); + let pager = self.pager.borrow(); + pager.set_encryption_key(key); + } } pub struct Statement { diff --git a/core/pragma.rs b/core/pragma.rs index f4b4b3f44..62be7f313 100644 --- a/core/pragma.rs +++ b/core/pragma.rs @@ -107,6 +107,10 @@ pub fn pragma_for(pragma: &PragmaName) -> Pragma { &["query_only"], ), FreelistCount => Pragma::new(PragmaFlags::Result0, &["freelist_count"]), + EncryptionKey => Pragma::new( + PragmaFlags::Result0 | PragmaFlags::SchemaReq | PragmaFlags::NoColumns1, + &["key"], + ), } } diff --git a/core/translate/pragma.rs b/core/translate/pragma.rs index bac0309cd..2af63a977 100644 --- a/core/translate/pragma.rs +++ b/core/translate/pragma.rs @@ -7,11 +7,16 @@ use std::sync::Arc; use turso_sqlite3_parser::ast::{self, ColumnDefinition, Expr, Literal, Name}; use turso_sqlite3_parser::ast::{PragmaName, QualifiedName}; +use super::integrity_check::translate_integrity_check; use crate::pragma::pragma_for; use crate::schema::Schema; +#[cfg(feature = "encryption")] +use crate::storage::encryption::EncryptionKey; use crate::storage::pager::AutoVacuumMode; +use crate::storage::pager::Pager; use crate::storage::sqlite3_ondisk::CacheSize; use crate::storage::wal::CheckpointMode; +use crate::translate::emitter::TransactionMode; use crate::translate::schema::translate_create_table; use crate::util::{normalize_ident, parse_signed_number, parse_string, IOExt as _}; use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts}; @@ -20,10 +25,6 @@ use crate::{bail_parse_error, CaptureDataChangesMode, LimboError, SymbolTable, V use std::str::FromStr; use strum::IntoEnumIterator; -use super::integrity_check::translate_integrity_check; -use crate::storage::pager::Pager; -use crate::translate::emitter::TransactionMode; - fn list_pragmas(program: &mut ProgramBuilder) { for x in PragmaName::iter() { let register = program.emit_string8_new_reg(x.to_string()); @@ -309,6 +310,15 @@ fn update_pragma( connection, program, ), + PragmaName::EncryptionKey => { + #[cfg(feature = "encryption")] + { + let value = parse_string(&value)?; + let key = EncryptionKey::from_string(&value); + connection.set_encryption_key(Some(key)); + } + Ok((program, TransactionMode::None)) + } } } @@ -566,6 +576,23 @@ fn query_pragma( program.add_pragma_result_column(pragma.to_string()); Ok((program, TransactionMode::None)) } + PragmaName::EncryptionKey => { + #[cfg(feature = "encryption")] + { + let msg = { + if connection.encryption_key.borrow().is_some() { + "encryption key is set for this session" + } else { + "encryption key is not set for this session" + } + }; + let register = program.alloc_register(); + program.emit_string8(msg.to_string(), register); + program.emit_result_row(register, 1); + program.add_pragma_result_column(pragma.to_string()); + } + Ok((program, TransactionMode::None)) + } } } diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index e8d755799..de096827f 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -1829,6 +1829,11 @@ pub enum PragmaName { IntegrityCheck, /// `journal_mode` pragma JournalMode, + /// encryption key for encrypted databases. This is just called `key` because most + /// extensions use this name instead of `encryption_key`. + #[strum(serialize = "key")] + #[cfg_attr(feature = "serde", serde(rename = "key"))] + EncryptionKey, /// Noop as per SQLite docs LegacyFileFormat, /// Set or get the maximum number of pages in the database file. From a6e9237c94b162691dd14bb0213c2641367c341f Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Tue, 12 Aug 2025 21:53:43 +0530 Subject: [PATCH 03/10] Set encryption key in pager and WAL --- core/storage/pager.rs | 16 ++++++++++++++++ core/storage/sqlite3_ondisk.rs | 2 ++ core/storage/wal.rs | 27 +++++++++++++++++++++------ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/core/storage/pager.rs b/core/storage/pager.rs index 18bbc1d23..7f000c80b 100644 --- a/core/storage/pager.rs +++ b/core/storage/pager.rs @@ -32,6 +32,8 @@ use super::wal::CheckpointMode; /// SQLite's default maximum page count const DEFAULT_MAX_PAGE_COUNT: u32 = 0xfffffffe; +#[cfg(feature = "encryption")] +use crate::storage::encryption::EncryptionKey; #[cfg(not(feature = "omit_autovacuum"))] use ptrmap::*; @@ -425,6 +427,8 @@ pub struct Pager { header_ref_state: RefCell, #[cfg(not(feature = "omit_autovacuum"))] btree_create_vacuum_full_state: Cell, + #[cfg(feature = "encryption")] + pub(crate) encryption_key: RefCell>, } #[derive(Debug, Clone)] @@ -526,6 +530,8 @@ impl Pager { header_ref_state: RefCell::new(HeaderRefState::Start), #[cfg(not(feature = "omit_autovacuum"))] btree_create_vacuum_full_state: Cell::new(BtreeCreateVacuumFullState::Start), + #[cfg(feature = "encryption")] + encryption_key: RefCell::new(None), }) } @@ -1045,6 +1051,7 @@ impl Pager { page_idx: usize, page: PageRef, allow_empty_read: bool, + #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, ) -> Result { sqlite3_ondisk::begin_read_page( self.db_file.clone(), @@ -1052,6 +1059,8 @@ impl Pager { page, page_idx, allow_empty_read, + #[cfg(feature = "encryption")] + encryption_key, ) } @@ -1957,6 +1966,13 @@ impl Pager { let header = header_ref.borrow_mut(); Ok(IOResult::Done(f(header))) } + + #[cfg(feature = "encryption")] + pub fn set_encryption_key(&self, key: Option) { + self.encryption_key.replace(key.clone()); + let Some(wal) = self.wal.as_ref() else { return }; + wal.borrow_mut().set_encryption_key(key) + } } pub fn allocate_new_page(page_id: usize, buffer_pool: &Arc, offset: usize) -> PageRef { diff --git a/core/storage/sqlite3_ondisk.rs b/core/storage/sqlite3_ondisk.rs index 381498d5b..2bd0dbbf6 100644 --- a/core/storage/sqlite3_ondisk.rs +++ b/core/storage/sqlite3_ondisk.rs @@ -59,6 +59,8 @@ use crate::storage::btree::offset::{ use crate::storage::btree::{payload_overflow_threshold_max, payload_overflow_threshold_min}; use crate::storage::buffer_pool::BufferPool; use crate::storage::database::DatabaseStorage; +#[cfg(feature = "encryption")] +use crate::storage::encryption::EncryptionKey; use crate::storage::pager::Pager; use crate::storage::wal::{PendingFlush, READMARK_NOT_USED}; use crate::types::{RawSlice, RefValue, SerialType, SerialTypeKind, TextRef, TextSubtype}; diff --git a/core/storage/wal.rs b/core/storage/wal.rs index b4ba856df..9db5bc267 100644 --- a/core/storage/wal.rs +++ b/core/storage/wal.rs @@ -11,9 +11,17 @@ use std::fmt::{Debug, Formatter}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::{cell::Cell, fmt, rc::Rc, sync::Arc}; +use self::sqlite3_ondisk::{checksum_wal, PageContent, WAL_MAGIC_BE, WAL_MAGIC_LE}; +use super::buffer_pool::BufferPool; +use super::pager::{PageRef, Pager}; +use super::sqlite3_ondisk::{self, WalHeader}; use crate::fast_lock::SpinLock; use crate::io::{File, IO}; use crate::result::LimboResult; +#[cfg(feature = "encryption")] +use crate::storage::encryption::EncryptionKey; +#[cfg(feature = "encryption")] +use crate::storage::encryption::{decrypt_page, encrypt_page}; use crate::storage::sqlite3_ondisk::{ begin_read_wal_frame, begin_read_wal_frame_raw, finish_read_page, prepare_wal_frame, write_pages_vectored, PageSize, WAL_FRAME_HEADER_SIZE, WAL_HEADER_SIZE, @@ -25,12 +33,6 @@ use crate::{ }; use crate::{Completion, Page}; -use self::sqlite3_ondisk::{checksum_wal, PageContent, WAL_MAGIC_BE, WAL_MAGIC_LE}; - -use super::buffer_pool::BufferPool; -use super::pager::{PageRef, Pager}; -use super::sqlite3_ondisk::{self, WalHeader}; - #[derive(Debug, Clone, Default)] pub struct CheckpointResult { /// number of frames in WAL that could have been backfilled @@ -287,6 +289,9 @@ pub trait Wal: Debug { /// Return unique set of pages changed **after** frame_watermark position and until current WAL session max_frame_no fn changed_pages_after(&self, frame_watermark: u64) -> Result>; + #[cfg(feature = "encryption")] + fn set_encryption_key(&mut self, key: Option); + #[cfg(debug_assertions)] fn as_any(&self) -> &dyn std::any::Any; } @@ -446,6 +451,9 @@ pub struct WalFile { /// Manages locks needed for checkpointing checkpoint_guard: Option, + + #[cfg(feature = "encryption")] + encryption_key: RefCell>, } impl fmt::Debug for WalFile { @@ -1226,6 +1234,11 @@ impl Wal for WalFile { fn as_any(&self) -> &dyn std::any::Any { self } + + #[cfg(feature = "encryption")] + fn set_encryption_key(&mut self, key: Option) { + self.encryption_key.replace(key); + } } impl WalFile { @@ -1265,6 +1278,8 @@ impl WalFile { prev_checkpoint: CheckpointResult::default(), checkpoint_guard: None, header: *header, + #[cfg(feature = "encryption")] + encryption_key: RefCell::new(None), } } From 94d38be1a2fe327a0271fc23e489bc23bd43fe07 Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Tue, 12 Aug 2025 21:54:03 +0530 Subject: [PATCH 04/10] Set reserved_space to 28 for encrypted databases We will use this space to store nonce and tag --- core/storage/sqlite3_ondisk.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/storage/sqlite3_ondisk.rs b/core/storage/sqlite3_ondisk.rs index 2bd0dbbf6..c6dce74a1 100644 --- a/core/storage/sqlite3_ondisk.rs +++ b/core/storage/sqlite3_ondisk.rs @@ -309,6 +309,9 @@ impl Default for DatabaseHeader { page_size: Default::default(), write_version: Version::Wal, read_version: Version::Wal, + #[cfg(feature = "encryption")] + reserved_space: 28, + #[cfg(not(feature = "encryption"))] reserved_space: 0, max_embed_frac: 64, min_embed_frac: 32, From 201262b3ddaa3357583b7fc8e48e3ad45e52393b Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Tue, 12 Aug 2025 21:55:22 +0530 Subject: [PATCH 05/10] Update `DatabaseStorage` to pass encryption context --- core/storage/database.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/core/storage/database.rs b/core/storage/database.rs index d7f4cf552..59a354b8c 100644 --- a/core/storage/database.rs +++ b/core/storage/database.rs @@ -1,4 +1,8 @@ use crate::error::LimboError; +#[cfg(feature = "encryption")] +use crate::storage::encryption::EncryptionKey; +#[cfg(feature = "encryption")] +use crate::storage::encryption::{decrypt_page, encrypt_page}; use crate::{io::Completion, Buffer, Result}; use std::sync::Arc; use tracing::{instrument, Level}; @@ -10,14 +14,26 @@ use tracing::{instrument, Level}; /// or something like a remote page server service. pub trait DatabaseStorage: Send + Sync { fn read_header(&self, c: Completion) -> Result; - fn read_page(&self, page_idx: usize, c: Completion) -> Result; - fn write_page(&self, page_idx: usize, buffer: Arc, c: Completion) - -> Result; + + fn read_page( + &self, + page_idx: usize, + #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + c: Completion, + ) -> Result; + fn write_page( + &self, + page_idx: usize, + buffer: Arc, + #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + c: Completion, + ) -> Result; fn write_pages( &self, first_page_idx: usize, page_size: usize, buffers: Vec>, + #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, c: Completion, ) -> Result; fn sync(&self, c: Completion) -> Result; From 657daeded356053423a1c27a96bf0b07e0e6140a Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Tue, 12 Aug 2025 21:56:07 +0530 Subject: [PATCH 06/10] encrypt/decrypt when writing/reading from WAL --- core/storage/wal.rs | 57 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/core/storage/wal.rs b/core/storage/wal.rs index 9db5bc267..8eb1a5199 100644 --- a/core/storage/wal.rs +++ b/core/storage/wal.rs @@ -921,18 +921,34 @@ impl Wal for WalFile { let offset = self.frame_offset(frame_id); page.set_locked(); let frame = page.clone(); - let complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { - let Ok((buf, bytes_read)) = res else { - return; - }; + #[cfg(feature = "encryption")] + let page_idx = page.get().id; + #[cfg(feature = "encryption")] + let key = self.encryption_key.borrow().clone(); + let complete = Box::new(move |buf: Arc, bytes_read: i32| { let buf_len = buf.len(); turso_assert!( bytes_read == buf_len as i32, "read({bytes_read}) less than expected({buf_len}): frame_id={frame_id}" ); let frame = frame.clone(); - finish_read_page(page.get().id, buf, frame); + #[cfg(feature = "encryption")] + { + if let Some(key) = key.clone() { + match decrypt_page(buf.as_slice(), page_idx, &key) { + Ok(decrypted_data) => { + buf.as_mut_slice().copy_from_slice(&decrypted_data); + } + Err(_) => { + tracing::error!("Failed to decrypt page data for frame_id={frame_id}"); + return; + } + } + } + } + finish_read_page(page.get().id, buf, frame).unwrap(); }); + begin_read_wal_frame( &self.get_shared().file, offset + WAL_FRAME_HEADER_SIZE, @@ -1083,6 +1099,31 @@ impl Wal for WalFile { let checksums = self.last_checksum; let page_content = page.get_contents(); let page_buf = page_content.as_ptr(); + + #[cfg(feature = "encryption")] + let encrypted_data = { + let key = self.encryption_key.borrow(); + if let Some(key) = key.as_ref() { + Some(encrypt_page(page_buf, page_id, key)?) + } else { + None + } + }; + let data_to_write = { + #[cfg(feature = "encryption")] + { + if let Some(ref data) = encrypted_data { + data.as_slice() + } else { + page_buf + } + } + #[cfg(not(feature = "encryption"))] + { + page_buf + } + }; + let (frame_checksums, frame_bytes) = prepare_wal_frame( &self.buffer_pool, &header, @@ -1090,7 +1131,7 @@ impl Wal for WalFile { header.page_size, page_id as u32, db_size, - page_buf, + data_to_write, ); let c = Completion::new_write({ @@ -1494,6 +1535,8 @@ impl WalFile { pager, std::mem::take(&mut self.ongoing_checkpoint.batch), &self.ongoing_checkpoint.pending_flush, + #[cfg(feature = "encryption")] + self.encryption_key.borrow().as_ref(), )?; // batch is queued self.ongoing_checkpoint.state = CheckpointState::AfterFlush; @@ -1907,7 +1950,7 @@ pub mod test { sync::{atomic::Ordering, Arc}, }; #[allow(clippy::arc_with_non_send_sync)] - fn get_database() -> (Arc, std::path::PathBuf) { + pub(crate) fn get_database() -> (Arc, std::path::PathBuf) { let mut path = tempfile::tempdir().unwrap().keep(); let dbpath = path.clone(); path.push("test.db"); From bd9b4bbfd25b39614272063b3f28ca9501bf2e0d Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Tue, 12 Aug 2025 21:57:35 +0530 Subject: [PATCH 07/10] encrypt/decrypt when writing/reading from DB --- core/io/mod.rs | 4 ++ core/storage/btree.rs | 5 ++ core/storage/database.rs | 93 ++++++++++++++++++++++++++++++++-- core/storage/pager.rs | 33 ++++++++---- core/storage/sqlite3_ondisk.rs | 7 ++- 5 files changed, 125 insertions(+), 17 deletions(-) diff --git a/core/io/mod.rs b/core/io/mod.rs index a54443ce5..beb674d67 100644 --- a/core/io/mod.rs +++ b/core/io/mod.rs @@ -280,6 +280,10 @@ impl ReadCompletion { pub fn callback(&self, bytes_read: Result) { (self.complete)(bytes_read.map(|b| (self.buf.clone(), b))); } + + pub fn buf_arc(&self) -> Arc { + self.buf.clone() + } } pub struct WriteCompletion { diff --git a/core/storage/btree.rs b/core/storage/btree.rs index de831757d..e72673778 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -8539,6 +8539,11 @@ mod tests { let c = Completion::new_write(move |_| { let _ = _buf.clone(); }); + #[cfg(feature = "encryption")] + let _c = pager + .db_file + .write_page(current_page as usize, buf.clone(), None, c)?; + #[cfg(not(feature = "encryption"))] let _c = pager .db_file .write_page(current_page as usize, buf.clone(), c)?; diff --git a/core/storage/database.rs b/core/storage/database.rs index 59a354b8c..df7dd577f 100644 --- a/core/storage/database.rs +++ b/core/storage/database.rs @@ -58,7 +58,12 @@ impl DatabaseStorage for DatabaseFile { self.file.pread(0, c) } #[instrument(skip_all, level = Level::DEBUG)] - fn read_page(&self, page_idx: usize, c: Completion) -> Result { + fn read_page( + &self, + page_idx: usize, + #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + c: Completion, + ) -> Result { let r = c.as_read(); let size = r.buf().len(); assert!(page_idx > 0); @@ -66,7 +71,42 @@ impl DatabaseStorage for DatabaseFile { return Err(LimboError::NotADB); } let pos = (page_idx - 1) * size; - self.file.pread(pos, c) + + #[cfg(feature = "encryption")] + { + if let Some(key) = encryption_key { + let key_clone = key.clone(); + let read_buffer = r.buf_arc(); + let original_c = c.clone(); + + let decrypt_complete = Box::new(move |buf: Arc, bytes_read: i32| { + if bytes_read > 0 { + match decrypt_page(buf.as_slice(), page_idx, &key_clone) { + Ok(decrypted_data) => { + let original_buf = original_c.as_read().buf(); + original_buf.as_mut_slice().copy_from_slice(&decrypted_data); + original_c.complete(bytes_read); + } + Err(_) => { + original_c.complete(-1); + } + } + } else { + original_c.complete(bytes_read); + } + }); + + let new_completion = Completion::new_read(read_buffer, decrypt_complete); + self.file.pread(pos, new_completion) + } else { + self.file.pread(pos, c) + } + } + + #[cfg(not(feature = "encryption"))] + { + self.file.pread(pos, c) + } } #[instrument(skip_all, level = Level::DEBUG)] @@ -74,6 +114,7 @@ impl DatabaseStorage for DatabaseFile { &self, page_idx: usize, buffer: Arc, + #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, c: Completion, ) -> Result { let buffer_size = buffer.len(); @@ -82,21 +123,57 @@ impl DatabaseStorage for DatabaseFile { assert!(buffer_size <= 65536); assert_eq!(buffer_size & (buffer_size - 1), 0); let pos = (page_idx - 1) * buffer_size; + let buffer = { + #[cfg(feature = "encryption")] + { + if let Some(key) = encryption_key { + encrypt_buffer(page_idx, buffer, key) + } else { + buffer + } + } + #[cfg(not(feature = "encryption"))] + { + buffer + } + }; self.file.pwrite(pos, buffer, c) } fn write_pages( &self, - page_idx: usize, + first_page_idx: usize, page_size: usize, buffers: Vec>, + #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, c: Completion, ) -> Result { - assert!(page_idx > 0); + assert!(first_page_idx > 0); assert!(page_size >= 512); assert!(page_size <= 65536); assert_eq!(page_size & (page_size - 1), 0); - let pos = (page_idx - 1) * page_size; + + let pos = (first_page_idx - 1) * page_size; + + let buffers = { + #[cfg(feature = "encryption")] + { + if let Some(key) = encryption_key { + buffers + .into_iter() + .enumerate() + .map(|(i, buffer)| encrypt_buffer(first_page_idx + i, buffer, key)) + .collect::>() + } else { + buffers + } + } + #[cfg(not(feature = "encryption"))] + { + buffers + } + }; + let c = self.file.pwritev(pos, buffers, c)?; Ok(c) } @@ -124,3 +201,9 @@ impl DatabaseFile { Self { file } } } + +#[cfg(feature = "encryption")] +fn encrypt_buffer(page_idx: usize, buffer: Arc, key: &EncryptionKey) -> Arc { + let encrypted_data = encrypt_page(buffer.as_slice(), page_idx, key).unwrap(); + Arc::new(Buffer::new(encrypted_data.to_vec())) +} diff --git a/core/storage/pager.rs b/core/storage/pager.rs index 7f000c80b..6ba49cb84 100644 --- a/core/storage/pager.rs +++ b/core/storage/pager.rs @@ -633,8 +633,8 @@ impl Pager { // Check if the calculated offset for the entry is within the bounds of the actual page data length. if offset_in_ptrmap_page + PTRMAP_ENTRY_SIZE > actual_data_length { return Err(LimboError::InternalError(format!( - "Ptrmap offset {offset_in_ptrmap_page} + entry size {PTRMAP_ENTRY_SIZE} out of bounds for page {ptrmap_pg_no} (actual data len {actual_data_length})" - ))); + "Ptrmap offset {offset_in_ptrmap_page} + entry size {PTRMAP_ENTRY_SIZE} out of bounds for page {ptrmap_pg_no} (actual data len {actual_data_length})" + ))); } let entry_slice = &ptrmap_page_data_slice @@ -676,8 +676,8 @@ impl Pager { || is_ptrmap_page(db_page_no_to_update, page_size) { return Err(LimboError::InternalError(format!( - "Cannot set ptrmap entry for page {db_page_no_to_update}: it's a header/ptrmap page or invalid." - ))); + "Cannot set ptrmap entry for page {db_page_no_to_update}: it's a header/ptrmap page or invalid." + ))); } let ptrmap_pg_no = get_ptrmap_page_no_for_db_page(db_page_no_to_update, page_size); @@ -1012,8 +1012,15 @@ impl Pager { matches!(frame_watermark, Some(0) | None), "frame_watermark must be either None or Some(0) because DB has no WAL and read with other watermark is invalid" ); + page.set_locked(); - let c = self.begin_read_disk_page(page_idx, page.clone(), allow_empty_read)?; + let c = self.begin_read_disk_page( + page_idx, + page.clone(), + allow_empty_read, + #[cfg(feature = "encryption")] + self.encryption_key.borrow().as_ref(), + )?; return Ok((page, c)); }; @@ -1026,7 +1033,13 @@ impl Pager { return Ok((page, c)); } - let c = self.begin_read_disk_page(page_idx, page.clone(), allow_empty_read)?; + let c = self.begin_read_disk_page( + page_idx, + page.clone(), + allow_empty_read, + #[cfg(feature = "encryption")] + self.encryption_key.borrow().as_ref(), + )?; Ok((page, c)) } @@ -2087,10 +2100,10 @@ mod ptrmap { pub fn serialize(&self, buffer: &mut [u8]) -> Result<()> { if buffer.len() < PTRMAP_ENTRY_SIZE { return Err(LimboError::InternalError(format!( - "Buffer too small to serialize ptrmap entry. Expected at least {} bytes, got {}", - PTRMAP_ENTRY_SIZE, - buffer.len() - ))); + "Buffer too small to serialize ptrmap entry. Expected at least {} bytes, got {}", + PTRMAP_ENTRY_SIZE, + buffer.len() + ))); } buffer[0] = self.entry_type as u8; buffer[1..5].copy_from_slice(&self.parent_page_no.to_be_bytes()); diff --git a/core/storage/sqlite3_ondisk.rs b/core/storage/sqlite3_ondisk.rs index c6dce74a1..5c5a1a419 100644 --- a/core/storage/sqlite3_ondisk.rs +++ b/core/storage/sqlite3_ondisk.rs @@ -874,6 +874,7 @@ pub fn begin_read_page( page: PageRef, page_idx: usize, allow_empty_read: bool, + encryption_key: Option<&EncryptionKey>, ) -> Result { tracing::trace!("begin_read_btree_page(page_idx = {})", page_idx); let buf = buffer_pool.get_page(); @@ -895,7 +896,7 @@ pub fn begin_read_page( finish_read_page(page_idx, buf, page.clone()); }); let c = Completion::new_read(buf, complete); - db_file.read_page(page_idx, c) + db_file.read_page(page_idx, encryption_key, c) } #[instrument(skip_all, level = Level::INFO)] @@ -946,7 +947,7 @@ pub fn begin_write_btree_page(pager: &Pager, page: &PageRef) -> Result>, flush: &PendingFlush, + encryption_key: Option<&EncryptionKey>, ) -> Result> { if batch.is_empty() { return Ok(Vec::new()); @@ -1043,6 +1045,7 @@ pub fn write_pages_vectored( start_id, page_sz, std::mem::replace(&mut run_bufs, Vec::with_capacity(EST_BUFF_CAPACITY)), + encryption_key, c, ) { Ok(c) => { From fc3b76de1bfa43c4b1d82f8c285e756035bf4ff3 Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Tue, 12 Aug 2025 21:57:54 +0530 Subject: [PATCH 08/10] fix Database storage for WASM bindings --- bindings/javascript/Cargo.toml | 3 +++ bindings/javascript/src/lib.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/bindings/javascript/Cargo.toml b/bindings/javascript/Cargo.toml index 18a9b319d..077f07fb3 100644 --- a/bindings/javascript/Cargo.toml +++ b/bindings/javascript/Cargo.toml @@ -16,5 +16,8 @@ napi = { version = "3.1.3", default-features = false, features = ["napi6"] } napi-derive = { version = "3.1.1", default-features = true } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +[features] +encryption = ["turso_core/encryption"] + [build-dependencies] napi-build = "2.2.3" diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index b02503a87..3ada9ff0e 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -561,6 +561,7 @@ impl turso_core::DatabaseStorage for DatabaseFile { fn read_page( &self, page_idx: usize, + #[cfg(feature = "encryption")] _key: Option<&turso_core::EncryptionKey>, c: turso_core::Completion, ) -> turso_core::Result { let r = c.as_read(); @@ -577,6 +578,7 @@ impl turso_core::DatabaseStorage for DatabaseFile { &self, page_idx: usize, buffer: Arc, + #[cfg(feature = "encryption")] _key: Option<&turso_core::EncryptionKey>, c: turso_core::Completion, ) -> turso_core::Result { let size = buffer.len(); @@ -589,6 +591,7 @@ impl turso_core::DatabaseStorage for DatabaseFile { page_idx: usize, page_size: usize, buffers: Vec>, + #[cfg(feature = "encryption")] _key: Option<&turso_core::EncryptionKey>, c: turso_core::Completion, ) -> turso_core::Result { let pos = page_idx.saturating_sub(1) * page_size; From eb45a156fca5fce791a78838f70134f22b4b339c Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Wed, 13 Aug 2025 11:42:06 +0530 Subject: [PATCH 09/10] Add a simple test for encryption --- .github/workflows/rust.yml | 2 + tests/Cargo.toml | 3 + .../query_processing/encryption.rs | 70 +++++++++++++++++++ tests/integration/query_processing/mod.rs | 3 + 4 files changed, 78 insertions(+) create mode 100644 tests/integration/query_processing/encryption.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c3becd317..12073def9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -38,6 +38,8 @@ jobs: python-version: "3.10" - name: Build run: cargo build --verbose + - name: Test Encryption + run: cargo test --features encryption --color=always --test integration_tests query_processing::encryption - name: Test env: RUST_LOG: ${{ runner.debug && 'turso_core::storage=trace' || '' }} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index f2936a014..b26ca9b5f 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -32,3 +32,6 @@ zerocopy = "0.8.26" test-log = { version = "0.2.17", features = ["trace"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing = "0.1.41" + +[features] +encryption = ["turso_core/encryption"] \ No newline at end of file diff --git a/tests/integration/query_processing/encryption.rs b/tests/integration/query_processing/encryption.rs new file mode 100644 index 000000000..c286bde7c --- /dev/null +++ b/tests/integration/query_processing/encryption.rs @@ -0,0 +1,70 @@ +use crate::common::{do_flush, TempDatabase}; +use crate::query_processing::test_write_path::{run_query, run_query_on_row}; +use rand::{rng, RngCore}; +use std::panic; +use turso_core::Row; + +#[test] +fn test_per_page_encryption() -> anyhow::Result<()> { + let _ = env_logger::try_init(); + let db_name = format!("test-{}.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 key = 'super secret key for encryption';", + )?; + run_query( + &tmp_db, + &conn, + "CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT);", + )?; + run_query( + &tmp_db, + &conn, + "INSERT INTO test (value) VALUES ('Hello, World!')", + )?; + let mut row_count = 0; + run_query_on_row(&tmp_db, &conn, "SELECT * FROM test", |row: &Row| { + assert_eq!(row.get::(0).unwrap(), 1); + assert_eq!(row.get::(1).unwrap(), "Hello, World!"); + row_count += 1; + })?; + assert_eq!(row_count, 1); + do_flush(&conn, &tmp_db)?; + } + + { + // this should panik because we should not be able to access the encrypted database + // without the key + let conn = tmp_db.connect_limbo(); + let should_panic = panic::catch_unwind(panic::AssertUnwindSafe(|| { + run_query_on_row(&tmp_db, &conn, "SELECT * FROM test", |_: &Row| {}).unwrap(); + })); + assert!( + should_panic.is_err(), + "should panic when accessing encrypted DB without key" + ); + } + + { + // let's test the existing db with the key + let existing_db = TempDatabase::new_with_existent(&db_path, false); + let conn = existing_db.connect_limbo(); + run_query( + &existing_db, + &conn, + "PRAGMA key = 'super secret key for encryption';", + )?; + run_query_on_row(&existing_db, &conn, "SELECT * FROM test", |row: &Row| { + assert_eq!(row.get::(0).unwrap(), 1); + assert_eq!(row.get::(1).unwrap(), "Hello, World!"); + })?; + } + + Ok(()) +} diff --git a/tests/integration/query_processing/mod.rs b/tests/integration/query_processing/mod.rs index c77608718..742cdf52c 100644 --- a/tests/integration/query_processing/mod.rs +++ b/tests/integration/query_processing/mod.rs @@ -4,3 +4,6 @@ mod test_write_path; mod test_multi_thread; mod test_transactions; + +#[cfg(feature = "encryption")] +mod encryption; From 40a209c000244383c46ccef12b6d2163c76f38a2 Mon Sep 17 00:00:00 2001 From: Avinash Sajjanshetty Date: Wed, 13 Aug 2025 14:53:49 +0530 Subject: [PATCH 10/10] simplify feature flag usage for encryption --- bindings/javascript/src/lib.rs | 10 ++-- core/Cargo.toml | 6 +-- core/lib.rs | 4 -- core/storage/btree.rs | 5 -- core/storage/database.rs | 87 +++++++++++++--------------------- core/storage/encryption.rs | 20 ++++++++ core/storage/mod.rs | 1 - core/storage/pager.rs | 11 +---- core/storage/sqlite3_ondisk.rs | 1 - core/storage/wal.rs | 62 ++++++++---------------- core/translate/pragma.rs | 35 ++++++-------- 11 files changed, 99 insertions(+), 143 deletions(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 3ada9ff0e..1c0218eeb 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -561,7 +561,7 @@ impl turso_core::DatabaseStorage for DatabaseFile { fn read_page( &self, page_idx: usize, - #[cfg(feature = "encryption")] _key: Option<&turso_core::EncryptionKey>, + _key: Option<&turso_core::EncryptionKey>, c: turso_core::Completion, ) -> turso_core::Result { let r = c.as_read(); @@ -578,7 +578,7 @@ impl turso_core::DatabaseStorage for DatabaseFile { &self, page_idx: usize, buffer: Arc, - #[cfg(feature = "encryption")] _key: Option<&turso_core::EncryptionKey>, + _key: Option<&turso_core::EncryptionKey>, c: turso_core::Completion, ) -> turso_core::Result { let size = buffer.len(); @@ -588,13 +588,13 @@ impl turso_core::DatabaseStorage for DatabaseFile { fn write_pages( &self, - page_idx: usize, + first_page_idx: usize, page_size: usize, buffers: Vec>, - #[cfg(feature = "encryption")] _key: Option<&turso_core::EncryptionKey>, + _key: Option<&turso_core::EncryptionKey>, c: turso_core::Completion, ) -> turso_core::Result { - let pos = page_idx.saturating_sub(1) * page_size; + let pos = first_page_idx.saturating_sub(1) * page_size; let c = self.file.pwritev(pos, buffers, c)?; Ok(c) } diff --git a/core/Cargo.toml b/core/Cargo.toml index cd9632931..ce88e7566 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -27,7 +27,7 @@ omit_autovacuum = [] simulator = ["fuzz", "serde"] serde = ["dep:serde"] series = [] -encryption = ["dep:aes-gcm", "dep:aes"] +encryption = [] [target.'cfg(target_os = "linux")'.dependencies] io-uring = { version = "0.7.5", optional = true } @@ -74,8 +74,8 @@ uuid = { version = "1.11.0", features = ["v4", "v7"], optional = true } tempfile = "3.8.0" pack1 = { version = "1.0.0", features = ["bytemuck"] } bytemuck = "1.23.1" -aes-gcm = { version = "0.10.3", optional = true } -aes = { version = "0.8.4", optional = true } +aes-gcm = { version = "0.10.3"} +aes = { version = "0.8.4"} [build-dependencies] chrono = { version = "0.4.38", default-features = false } diff --git a/core/lib.rs b/core/lib.rs index 0d0c737ab..44b0b376f 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -76,7 +76,6 @@ use std::{ }; #[cfg(feature = "fs")] use storage::database::DatabaseFile; -#[cfg(feature = "encryption")] pub use storage::encryption::EncryptionKey; use storage::page_cache::DumbLruPageCache; use storage::pager::{AtomicDbState, DbState}; @@ -427,7 +426,6 @@ impl Database { view_transaction_states: RefCell::new(HashMap::new()), metrics: RefCell::new(ConnectionMetrics::new()), is_nested_stmt: Cell::new(false), - #[cfg(feature = "encryption")] encryption_key: RefCell::new(None), }); let builtin_syms = self.builtin_syms.borrow(); @@ -856,7 +854,6 @@ pub struct Connection { /// Whether the connection is executing a statement initiated by another statement. /// Generally this is only true for ParseSchema. is_nested_stmt: Cell, - #[cfg(feature = "encryption")] encryption_key: RefCell>, } @@ -1932,7 +1929,6 @@ impl Connection { self.syms.borrow().vtab_modules.keys().cloned().collect() } - #[cfg(feature = "encryption")] pub fn set_encryption_key(&self, key: Option) { tracing::trace!("setting encryption key for connection"); *self.encryption_key.borrow_mut() = key.clone(); diff --git a/core/storage/btree.rs b/core/storage/btree.rs index e72673778..b0474e344 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -8539,14 +8539,9 @@ mod tests { let c = Completion::new_write(move |_| { let _ = _buf.clone(); }); - #[cfg(feature = "encryption")] let _c = pager .db_file .write_page(current_page as usize, buf.clone(), None, c)?; - #[cfg(not(feature = "encryption"))] - let _c = pager - .db_file - .write_page(current_page as usize, buf.clone(), c)?; pager.io.run_once()?; let (page, _c) = cursor.read_page(current_page as usize)?; diff --git a/core/storage/database.rs b/core/storage/database.rs index df7dd577f..980dce8e4 100644 --- a/core/storage/database.rs +++ b/core/storage/database.rs @@ -1,9 +1,6 @@ use crate::error::LimboError; -#[cfg(feature = "encryption")] -use crate::storage::encryption::EncryptionKey; -#[cfg(feature = "encryption")] -use crate::storage::encryption::{decrypt_page, encrypt_page}; -use crate::{io::Completion, Buffer, Result}; +use crate::storage::encryption::{decrypt_page, encrypt_page, EncryptionKey}; +use crate::{io::Completion, Buffer, CompletionError, Result}; use std::sync::Arc; use tracing::{instrument, Level}; @@ -18,14 +15,14 @@ pub trait DatabaseStorage: Send + Sync { fn read_page( &self, page_idx: usize, - #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + encryption_key: Option<&EncryptionKey>, c: Completion, ) -> Result; fn write_page( &self, page_idx: usize, buffer: Arc, - #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + encryption_key: Option<&EncryptionKey>, c: Completion, ) -> Result; fn write_pages( @@ -33,7 +30,7 @@ pub trait DatabaseStorage: Send + Sync { first_page_idx: usize, page_size: usize, buffers: Vec>, - #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + encryption_key: Option<&EncryptionKey>, c: Completion, ) -> Result; fn sync(&self, c: Completion) -> Result; @@ -57,11 +54,12 @@ impl DatabaseStorage for DatabaseFile { fn read_header(&self, c: Completion) -> Result { self.file.pread(0, c) } + #[instrument(skip_all, level = Level::DEBUG)] fn read_page( &self, page_idx: usize, - #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + encryption_key: Option<&EncryptionKey>, c: Completion, ) -> Result { let r = c.as_read(); @@ -72,14 +70,16 @@ impl DatabaseStorage for DatabaseFile { } let pos = (page_idx - 1) * size; - #[cfg(feature = "encryption")] - { - if let Some(key) = encryption_key { - let key_clone = key.clone(); - let read_buffer = r.buf_arc(); - let original_c = c.clone(); + if let Some(key) = encryption_key { + let key_clone = key.clone(); + let read_buffer = r.buf_arc(); + let original_c = c.clone(); - let decrypt_complete = Box::new(move |buf: Arc, bytes_read: i32| { + let decrypt_complete = + Box::new(move |res: Result<(Arc, i32), CompletionError>| { + let Ok((buf, bytes_read)) = res else { + return; + }; if bytes_read > 0 { match decrypt_page(buf.as_slice(), page_idx, &key_clone) { Ok(decrypted_data) => { @@ -88,6 +88,9 @@ impl DatabaseStorage for DatabaseFile { original_c.complete(bytes_read); } Err(_) => { + tracing::error!( + "Failed to decrypt page data for page_id={page_idx}" + ); original_c.complete(-1); } } @@ -96,15 +99,9 @@ impl DatabaseStorage for DatabaseFile { } }); - let new_completion = Completion::new_read(read_buffer, decrypt_complete); - self.file.pread(pos, new_completion) - } else { - self.file.pread(pos, c) - } - } - - #[cfg(not(feature = "encryption"))] - { + let new_completion = Completion::new_read(read_buffer, decrypt_complete); + self.file.pread(pos, new_completion) + } else { self.file.pread(pos, c) } } @@ -114,7 +111,7 @@ impl DatabaseStorage for DatabaseFile { &self, page_idx: usize, buffer: Arc, - #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + encryption_key: Option<&EncryptionKey>, c: Completion, ) -> Result { let buffer_size = buffer.len(); @@ -124,16 +121,9 @@ impl DatabaseStorage for DatabaseFile { assert_eq!(buffer_size & (buffer_size - 1), 0); let pos = (page_idx - 1) * buffer_size; let buffer = { - #[cfg(feature = "encryption")] - { - if let Some(key) = encryption_key { - encrypt_buffer(page_idx, buffer, key) - } else { - buffer - } - } - #[cfg(not(feature = "encryption"))] - { + if let Some(key) = encryption_key { + encrypt_buffer(page_idx, buffer, key) + } else { buffer } }; @@ -145,7 +135,7 @@ impl DatabaseStorage for DatabaseFile { first_page_idx: usize, page_size: usize, buffers: Vec>, - #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + encryption_key: Option<&EncryptionKey>, c: Completion, ) -> Result { assert!(first_page_idx > 0); @@ -154,22 +144,14 @@ impl DatabaseStorage for DatabaseFile { assert_eq!(page_size & (page_size - 1), 0); let pos = (first_page_idx - 1) * page_size; - let buffers = { - #[cfg(feature = "encryption")] - { - if let Some(key) = encryption_key { - buffers - .into_iter() - .enumerate() - .map(|(i, buffer)| encrypt_buffer(first_page_idx + i, buffer, key)) - .collect::>() - } else { - buffers - } - } - #[cfg(not(feature = "encryption"))] - { + if let Some(key) = encryption_key { + buffers + .into_iter() + .enumerate() + .map(|(i, buffer)| encrypt_buffer(first_page_idx + i, buffer, key)) + .collect::>() + } else { buffers } }; @@ -202,7 +184,6 @@ impl DatabaseFile { } } -#[cfg(feature = "encryption")] fn encrypt_buffer(page_idx: usize, buffer: Arc, key: &EncryptionKey) -> Arc { let encrypted_data = encrypt_page(buffer.as_slice(), page_idx, key).unwrap(); Arc::new(Buffer::new(encrypted_data.to_vec())) diff --git a/core/storage/encryption.rs b/core/storage/encryption.rs index b308096fa..97c2b3574 100644 --- a/core/storage/encryption.rs +++ b/core/storage/encryption.rs @@ -1,3 +1,6 @@ +#![allow(unused_variables, dead_code)] +#[cfg(not(feature = "encryption"))] +use crate::LimboError; use crate::Result; use aes_gcm::{ aead::{Aead, AeadCore, KeyInit, OsRng}, @@ -68,6 +71,14 @@ impl Drop for EncryptionKey { } } +#[cfg(not(feature = "encryption"))] +pub fn encrypt_page(page: &[u8], page_id: usize, key: &EncryptionKey) -> Result> { + Err(LimboError::InvalidArgument( + "encryption is not enabled, cannot encrypt page. enable via passing `--features encryption`".into(), + )) +} + +#[cfg(feature = "encryption")] pub fn encrypt_page(page: &[u8], page_id: usize, key: &EncryptionKey) -> Result> { if page_id == 1 { tracing::debug!("skipping encryption for page 1 (database header)"); @@ -104,6 +115,14 @@ pub fn encrypt_page(page: &[u8], page_id: usize, key: &EncryptionKey) -> Result< Ok(result) } +#[cfg(not(feature = "encryption"))] +pub fn decrypt_page(encrypted_page: &[u8], page_id: usize, key: &EncryptionKey) -> Result> { + Err(LimboError::InvalidArgument( + "encryption is not enabled, cannot decrypt page. enable via passing `--features encryption`".into(), + )) +} + +#[cfg(feature = "encryption")] pub fn decrypt_page(encrypted_page: &[u8], page_id: usize, key: &EncryptionKey) -> Result> { if page_id == 1 { tracing::debug!("skipping decryption for page 1 (database header)"); @@ -160,6 +179,7 @@ mod tests { use rand::Rng; #[test] + #[cfg(feature = "encryption")] fn test_encrypt_decrypt_round_trip() { let mut rng = rand::thread_rng(); let data_size = ENCRYPTED_PAGE_SIZE - ENCRYPTION_METADATA_SIZE; diff --git a/core/storage/mod.rs b/core/storage/mod.rs index bcba83d5a..52c2e3f47 100644 --- a/core/storage/mod.rs +++ b/core/storage/mod.rs @@ -13,7 +13,6 @@ pub(crate) mod btree; pub(crate) mod buffer_pool; pub(crate) mod database; -#[cfg(feature = "encryption")] pub(crate) mod encryption; pub(crate) mod page_cache; #[allow(clippy::arc_with_non_send_sync)] diff --git a/core/storage/pager.rs b/core/storage/pager.rs index 6ba49cb84..7ea80cde4 100644 --- a/core/storage/pager.rs +++ b/core/storage/pager.rs @@ -28,12 +28,11 @@ use super::btree::{btree_init_page, BTreePage}; use super::page_cache::{CacheError, CacheResizeResult, DumbLruPageCache, PageCacheKey}; use super::sqlite3_ondisk::begin_write_btree_page; use super::wal::CheckpointMode; +use crate::storage::encryption::EncryptionKey; /// SQLite's default maximum page count const DEFAULT_MAX_PAGE_COUNT: u32 = 0xfffffffe; -#[cfg(feature = "encryption")] -use crate::storage::encryption::EncryptionKey; #[cfg(not(feature = "omit_autovacuum"))] use ptrmap::*; @@ -427,7 +426,6 @@ pub struct Pager { header_ref_state: RefCell, #[cfg(not(feature = "omit_autovacuum"))] btree_create_vacuum_full_state: Cell, - #[cfg(feature = "encryption")] pub(crate) encryption_key: RefCell>, } @@ -530,7 +528,6 @@ impl Pager { header_ref_state: RefCell::new(HeaderRefState::Start), #[cfg(not(feature = "omit_autovacuum"))] btree_create_vacuum_full_state: Cell::new(BtreeCreateVacuumFullState::Start), - #[cfg(feature = "encryption")] encryption_key: RefCell::new(None), }) } @@ -1018,7 +1015,6 @@ impl Pager { page_idx, page.clone(), allow_empty_read, - #[cfg(feature = "encryption")] self.encryption_key.borrow().as_ref(), )?; return Ok((page, c)); @@ -1037,7 +1033,6 @@ impl Pager { page_idx, page.clone(), allow_empty_read, - #[cfg(feature = "encryption")] self.encryption_key.borrow().as_ref(), )?; Ok((page, c)) @@ -1064,7 +1059,7 @@ impl Pager { page_idx: usize, page: PageRef, allow_empty_read: bool, - #[cfg(feature = "encryption")] encryption_key: Option<&EncryptionKey>, + encryption_key: Option<&EncryptionKey>, ) -> Result { sqlite3_ondisk::begin_read_page( self.db_file.clone(), @@ -1072,7 +1067,6 @@ impl Pager { page, page_idx, allow_empty_read, - #[cfg(feature = "encryption")] encryption_key, ) } @@ -1980,7 +1974,6 @@ impl Pager { Ok(IOResult::Done(f(header))) } - #[cfg(feature = "encryption")] pub fn set_encryption_key(&self, key: Option) { self.encryption_key.replace(key.clone()); let Some(wal) = self.wal.as_ref() else { return }; diff --git a/core/storage/sqlite3_ondisk.rs b/core/storage/sqlite3_ondisk.rs index 5c5a1a419..c3e472c88 100644 --- a/core/storage/sqlite3_ondisk.rs +++ b/core/storage/sqlite3_ondisk.rs @@ -59,7 +59,6 @@ use crate::storage::btree::offset::{ use crate::storage::btree::{payload_overflow_threshold_max, payload_overflow_threshold_min}; use crate::storage::buffer_pool::BufferPool; use crate::storage::database::DatabaseStorage; -#[cfg(feature = "encryption")] use crate::storage::encryption::EncryptionKey; use crate::storage::pager::Pager; use crate::storage::wal::{PendingFlush, READMARK_NOT_USED}; diff --git a/core/storage/wal.rs b/core/storage/wal.rs index 8eb1a5199..e7a4bd936 100644 --- a/core/storage/wal.rs +++ b/core/storage/wal.rs @@ -2,7 +2,7 @@ #![allow(clippy::not_unsafe_ptr_arg_deref)] use std::array; -use std::cell::UnsafeCell; +use std::cell::{RefCell, UnsafeCell}; use std::collections::{BTreeMap, HashMap, HashSet}; use strum::EnumString; use tracing::{instrument, Level}; @@ -18,10 +18,7 @@ use super::sqlite3_ondisk::{self, WalHeader}; use crate::fast_lock::SpinLock; use crate::io::{File, IO}; use crate::result::LimboResult; -#[cfg(feature = "encryption")] -use crate::storage::encryption::EncryptionKey; -#[cfg(feature = "encryption")] -use crate::storage::encryption::{decrypt_page, encrypt_page}; +use crate::storage::encryption::{decrypt_page, encrypt_page, EncryptionKey}; use crate::storage::sqlite3_ondisk::{ begin_read_wal_frame, begin_read_wal_frame_raw, finish_read_page, prepare_wal_frame, write_pages_vectored, PageSize, WAL_FRAME_HEADER_SIZE, WAL_HEADER_SIZE, @@ -289,7 +286,6 @@ pub trait Wal: Debug { /// Return unique set of pages changed **after** frame_watermark position and until current WAL session max_frame_no fn changed_pages_after(&self, frame_watermark: u64) -> Result>; - #[cfg(feature = "encryption")] fn set_encryption_key(&mut self, key: Option); #[cfg(debug_assertions)] @@ -452,7 +448,6 @@ pub struct WalFile { /// Manages locks needed for checkpointing checkpoint_guard: Option, - #[cfg(feature = "encryption")] encryption_key: RefCell>, } @@ -921,34 +916,32 @@ impl Wal for WalFile { let offset = self.frame_offset(frame_id); page.set_locked(); let frame = page.clone(); - #[cfg(feature = "encryption")] let page_idx = page.get().id; - #[cfg(feature = "encryption")] let key = self.encryption_key.borrow().clone(); - let complete = Box::new(move |buf: Arc, bytes_read: i32| { + let complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { + let Ok((buf, bytes_read)) = res else { + page.clear_locked(); + return; + }; let buf_len = buf.len(); turso_assert!( bytes_read == buf_len as i32, "read({bytes_read}) less than expected({buf_len}): frame_id={frame_id}" ); let frame = frame.clone(); - #[cfg(feature = "encryption")] - { - if let Some(key) = key.clone() { - match decrypt_page(buf.as_slice(), page_idx, &key) { - Ok(decrypted_data) => { - buf.as_mut_slice().copy_from_slice(&decrypted_data); - } - Err(_) => { - tracing::error!("Failed to decrypt page data for frame_id={frame_id}"); - return; - } + if let Some(key) = key.clone() { + match decrypt_page(buf.as_slice(), page_idx, &key) { + Ok(decrypted_data) => { + buf.as_mut_slice().copy_from_slice(&decrypted_data); + } + Err(_) => { + tracing::error!("Failed to decrypt page data for frame_id={frame_id}"); + return; } } } - finish_read_page(page.get().id, buf, frame).unwrap(); + finish_read_page(page.get().id, buf, frame); }); - begin_read_wal_frame( &self.get_shared().file, offset + WAL_FRAME_HEADER_SIZE, @@ -1100,28 +1093,18 @@ impl Wal for WalFile { let page_content = page.get_contents(); let page_buf = page_content.as_ptr(); - #[cfg(feature = "encryption")] + let key = self.encryption_key.borrow(); let encrypted_data = { - let key = self.encryption_key.borrow(); if let Some(key) = key.as_ref() { Some(encrypt_page(page_buf, page_id, key)?) } else { None } }; - let data_to_write = { - #[cfg(feature = "encryption")] - { - if let Some(ref data) = encrypted_data { - data.as_slice() - } else { - page_buf - } - } - #[cfg(not(feature = "encryption"))] - { - page_buf - } + let data_to_write = if key.as_ref().is_some() { + encrypted_data.as_ref().unwrap().as_slice() + } else { + page_buf }; let (frame_checksums, frame_bytes) = prepare_wal_frame( @@ -1276,7 +1259,6 @@ impl Wal for WalFile { self } - #[cfg(feature = "encryption")] fn set_encryption_key(&mut self, key: Option) { self.encryption_key.replace(key); } @@ -1319,7 +1301,6 @@ impl WalFile { prev_checkpoint: CheckpointResult::default(), checkpoint_guard: None, header: *header, - #[cfg(feature = "encryption")] encryption_key: RefCell::new(None), } } @@ -1535,7 +1516,6 @@ impl WalFile { pager, std::mem::take(&mut self.ongoing_checkpoint.batch), &self.ongoing_checkpoint.pending_flush, - #[cfg(feature = "encryption")] self.encryption_key.borrow().as_ref(), )?; // batch is queued diff --git a/core/translate/pragma.rs b/core/translate/pragma.rs index 2af63a977..f8afb272e 100644 --- a/core/translate/pragma.rs +++ b/core/translate/pragma.rs @@ -10,7 +10,6 @@ use turso_sqlite3_parser::ast::{PragmaName, QualifiedName}; use super::integrity_check::translate_integrity_check; use crate::pragma::pragma_for; use crate::schema::Schema; -#[cfg(feature = "encryption")] use crate::storage::encryption::EncryptionKey; use crate::storage::pager::AutoVacuumMode; use crate::storage::pager::Pager; @@ -311,12 +310,9 @@ fn update_pragma( program, ), PragmaName::EncryptionKey => { - #[cfg(feature = "encryption")] - { - let value = parse_string(&value)?; - let key = EncryptionKey::from_string(&value); - connection.set_encryption_key(Some(key)); - } + let value = parse_string(&value)?; + let key = EncryptionKey::from_string(&value); + connection.set_encryption_key(Some(key)); Ok((program, TransactionMode::None)) } } @@ -577,20 +573,17 @@ fn query_pragma( Ok((program, TransactionMode::None)) } PragmaName::EncryptionKey => { - #[cfg(feature = "encryption")] - { - let msg = { - if connection.encryption_key.borrow().is_some() { - "encryption key is set for this session" - } else { - "encryption key is not set for this session" - } - }; - let register = program.alloc_register(); - program.emit_string8(msg.to_string(), register); - program.emit_result_row(register, 1); - program.add_pragma_result_column(pragma.to_string()); - } + let msg = { + if connection.encryption_key.borrow().is_some() { + "encryption key is set for this session" + } else { + "encryption key is not set for this session" + } + }; + let register = program.alloc_register(); + program.emit_string8(msg.to_string(), register); + program.emit_result_row(register, 1); + program.add_pragma_result_column(pragma.to_string()); Ok((program, TransactionMode::None)) } }