How Encryption at Rest Protects Database Files in Turso
Turso implements encryption at rest by encrypting every 4 KB database page before it is written to disk, embedding the nonce and authentication tag within the page itself to ensure the file remains unreadable without the correct key.
The tursodatabase/turso repository provides a SQLite-compatible database engine that secures data at the storage layer. Unlike full-disk encryption, Turso’s design encrypts individual pages using authenticated encryption (AEAD), allowing the engine to detect tampering while maintaining compatibility with SQLite’s file format.
Page-Level Encryption Architecture
Turso protects data using page-level encryption, a design choice that operates at the granularity of SQLite’s default 4 KB pages (or user-configured sizes). Each page is encrypted independently before reaching the file system.
The Encrypted Page Layout
According to the implementation in core/storage/encryption.rs (lines 21-28), every encrypted page stores its cryptographic metadata inline. The layout includes:
- Nonce – A unique value ensuring identical pages produce different ciphertexts
- Ciphertext – The encrypted page contents
- Authentication tag – A cryptographic checksum verifying integrity
Because the nonce and tag reside inside the same 4 KB boundary, the physical page size does not change. This allows Turso to read and write pages exactly as SQLite does, while guaranteeing confidentiality and integrity for the underlying data.
Preserving SQLite Compatibility
To maintain compatibility with SQLite’s file format, Turso preserves the first 100 bytes of the database file (the standard SQLite header). This ensures that external tools can still identify the file as a valid SQLite database, even when encrypted.
However, the engine overwrites specific bytes within this preserved region to store metadata required for decryption, creating a hybrid header that satisfies both SQLite’s magic byte requirements and Turso’s encryption needs.
The Turso Header Format
Page 1 (the first page of the database) contains a special Turso header that tells the engine which cipher and key size to use when opening the file. As defined in core/storage/encryption.rs (lines 50-66), the engine preserves the SQLite magic bytes but overwrites the first 16 bytes with:
- "Turso" identifier (5 bytes)
- Version (1 byte)
- Cipher identifier (1 byte)
- Reserved space (9 bytes)
This header is read before the encryption context is created, allowing the engine to initialize the correct decryption algorithm without accessing encrypted content.
Cipher Selection and Key Management
Turso supports a flexible cipher suite based on modern AEAD constructions, giving operators control over the security/performance trade-off.
Supported AEAD Ciphers
The CipherMode enum defined in core/storage/encryption.rs (lines 48-60) supports the following algorithms:
- AES-GCM-128 and AES-GCM-256
- AEGIS-256
- AEGIS-128L, AEGIS-X2, AEGIS-X4
- AEGIS-256X2 and AEGIS-256X4
The chosen cipher determines the required key size (128-bit or 256-bit). These algorithms provide both confidentiality and authenticity, ensuring that any modification to the ciphertext is detected during decryption.
Key Handling
Keys are supplied as hexadecimal strings and parsed into the EncryptionKey enum. The implementation in core/storage/encryption.rs (lines 76-104) provides the from_hex_string conversion, validating the key length against the selected cipher’s requirements.
Keys remain in memory only for the lifetime of the connection and are never written to disk in plaintext.
Encryption Context and Pager Integration
When a database is opened, Turso constructs an EncryptionContext (located in core/storage/pager.rs). This context stores the selected cipher instance and the encryption key, acting as the gateway for all disk I/O.
All read and write operations flow through the pager:
EncryptionContext::encrypt– Automatically generates nonces and appends authentication tags before writing pages to diskEncryptionContext::decrypt– Verifies tags and decrypts pages when reading from disk
The pager (Pager::new, Pager::read_page, and Pager::write_page) uses this context transparently, meaning the rest of the engine operates on plaintext pages while the storage layer handles cryptographic protection.
Integrity Guarantees Against Tampering
Both AES-GCM and the AEGIS family provide authenticated encryption. As noted in core/storage/encryption.rs (lines 15-19), the design includes "verification of the ciphertext."
If a page is corrupted, tampered with, or subjected to bit-rot, decryption fails with a clear error rather than yielding garbage data. This protects against silent data corruption and ensures that attackers cannot modify database files without detection.
Enabling Encryption at Rest in Your Application
Applications enable encryption by configuring DatabaseOpts with a CipherMode and hex-encoded key. The same options apply to attached databases, ensuring every file in a process uses consistent protection.
use turso_core::{
Database, DatabaseOpts, CipherMode, EncryptionKey, OpenFlags,
};
fn open_encrypted_db(path: &str, hex_key: &str) -> anyhow::Result<Database> {
// Build the options – enable encryption, pick a cipher, supply the key.
let enc_key = EncryptionKey::from_hex_string(hex_key)?;
let opts = DatabaseOpts::new()
.with_encryption(true) // turn on at-rest encryption
.with_cipher(CipherMode::Aes256Gcm) // choose the cipher
.with_key(enc_key); // attach the key
// Open the database.
let db = Database::open(path, opts, OpenFlags::READ_WRITE)?;
Ok(db)
}
// Attaching a second encrypted database with a different key
fn attach_another_encrypted(db: &Database, path: &str, hex_key: &str) -> anyhow::Result<()> {
let key = EncryptionKey::from_hex_string(hex_key)?;
db.attach(
path,
DatabaseOpts::new()
.with_encryption(true)
.with_cipher(CipherMode::Aegis256)
.with_key(key),
)?;
Ok(())
}
The integration tests in tests/integration/query_processing/encryption.rs demonstrate end-to-end usage, including creating, opening, and attaching encrypted databases with various cipher configurations.
Summary
- Page-level granularity – Each 4 KB page is encrypted independently with its nonce and authentication tag stored inline, maintaining standard page sizes.
- Header compatibility – Page 1 preserves SQLite’s magic bytes while embedding a Turso-specific header that specifies the cipher algorithm.
- AEAD protection – AES-GCM and AEGIS ciphers provide authenticated encryption, preventing both data leakage and undetected tampering.
- Runtime configuration – Encryption is enabled via
DatabaseOptswith hex-encoded keys, with full support for attached databases using different keys.
Frequently Asked Questions
What encryption algorithms does Turso support for data at rest?
Turso supports AES-GCM-128, AES-GCM-256, and the AEGIS family including AEGIS-256, AEGIS-128L, AEGIS-X2, AEGIS-X4, AEGIS-256X2, and AEGIS-256X4. These are configured via the CipherMode enum when opening a database.
How does Turso maintain SQLite compatibility while encrypting database files?
Turso preserves the first 100 bytes of the SQLite header (containing magic bytes) but overwrites the first 16 bytes with a Turso-specific header containing cipher metadata. This allows standard SQLite tools to recognize the file type while enabling Turso to decrypt the contents.
What happens if an encrypted database file is corrupted or tampered with?
Because Turso uses authenticated encryption (AEAD), any modification to the encrypted pages causes decryption to fail with an error. The engine verifies the authentication tag before returning plaintext, preventing silent data corruption and detecting malicious tampering.
Can different databases in the same connection use different encryption keys?
Yes. The DatabaseOpts configuration is per-database, allowing you to open a primary database with one key and attach additional databases using db.attach() with different CipherMode and EncryptionKey configurations. Each file maintains its own independent encryption context.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →