use std::ops::Bound;
use std::sync::Arc;
use crate::engine::{KvPairs, ReadTxn, StorageBackend, WriteTxn};
use crate::error::TalaDbError;
#[cfg(feature = "encryption")]
const CRYPTO_FORMAT_V1: u8 = 0x01;
#[cfg(feature = "encryption")]
const NONCE_LEN_V1: usize = 12;
pub const MIN_PBKDF2_ITERATIONS: u32 = 600_000;
#[cfg(feature = "encryption")]
pub type EncryptionKey = zeroize::Zeroizing<[u8; 32]>;
#[cfg(not(feature = "encryption"))]
pub type EncryptionKey = [u8; 32];
#[cfg(feature = "encryption")]
fn make_aad(table: &str, key: &[u8]) -> Vec<u8> {
let mut aad = table.as_bytes().to_vec();
aad.push(0x00);
for b in key {
aad.push(b"0123456789abcdef"[(b >> 4) as usize]);
aad.push(b"0123456789abcdef"[(b & 0xf) as usize]);
}
aad
}
pub fn encrypt(
enc_key: &EncryptionKey,
table: &str,
key: &[u8],
plaintext: &[u8],
) -> Result<Vec<u8>, TalaDbError> {
#[cfg(feature = "encryption")]
{
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Nonce};
let cipher = Aes256Gcm::new_from_slice(enc_key.as_ref())
.map_err(|e| TalaDbError::Encryption(e.to_string()))?;
let mut nonce_bytes = [0u8; NONCE_LEN_V1];
getrandom::fill(&mut nonce_bytes).map_err(|e| {
TalaDbError::Encryption(format!("system entropy source failed for a GCM nonce: {e}"))
})?;
let nonce = Nonce::try_from(&nonce_bytes[..])
.map_err(|_| TalaDbError::Encryption("internal: bad nonce length".into()))?;
let aad = make_aad(table, key);
let payload = aes_gcm::aead::Payload {
msg: plaintext,
aad: &aad,
};
let ciphertext = cipher
.encrypt(&nonce, payload)
.map_err(|e| TalaDbError::Encryption(e.to_string()))?;
let mut out = Vec::with_capacity(1 + NONCE_LEN_V1 + ciphertext.len());
out.push(CRYPTO_FORMAT_V1);
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ciphertext);
Ok(out)
}
#[cfg(not(feature = "encryption"))]
{
let _ = (enc_key, table, key, plaintext);
Err(TalaDbError::Encryption(
"encrypt() called but the `encryption` feature is not enabled; \
enable it in Cargo.toml: taladb-core = { features = [\"encryption\"] }"
.into(),
))
}
}
pub fn decrypt(
enc_key: &EncryptionKey,
table: &str,
key: &[u8],
data: &[u8],
) -> Result<Vec<u8>, TalaDbError> {
#[cfg(feature = "encryption")]
{
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Nonce};
if data.len() < 1 + NONCE_LEN_V1 + 16 {
return Err(TalaDbError::Encryption("ciphertext too short".into()));
}
let version = data[0];
if version != CRYPTO_FORMAT_V1 {
return Err(TalaDbError::Encryption(format!(
"unsupported encrypted-value format version {version:#04x}; \
expected {CRYPTO_FORMAT_V1:#04x}"
)));
}
let nonce_bytes = &data[1..1 + NONCE_LEN_V1];
let ciphertext = &data[1 + NONCE_LEN_V1..];
let cipher = Aes256Gcm::new_from_slice(enc_key.as_ref())
.map_err(|e| TalaDbError::Encryption(e.to_string()))?;
let nonce = Nonce::try_from(nonce_bytes)
.map_err(|_| TalaDbError::Encryption("ciphertext nonce is malformed".into()))?;
let aad = make_aad(table, key);
let payload = aes_gcm::aead::Payload {
msg: ciphertext,
aad: &aad,
};
cipher
.decrypt(&nonce, payload)
.map_err(|e| TalaDbError::Encryption(e.to_string()))
}
#[cfg(not(feature = "encryption"))]
{
let _ = (enc_key, table, key, data);
Err(TalaDbError::Encryption(
"decrypt() called but the `encryption` feature is not enabled; \
enable it in Cargo.toml: taladb-core = { features = [\"encryption\"] }"
.into(),
))
}
}
pub struct EncryptedBackend {
inner: Arc<dyn StorageBackend>,
key: EncryptionKey,
}
impl EncryptedBackend {
#[cfg(feature = "encryption")]
pub fn new(inner: Arc<dyn StorageBackend>, key: EncryptionKey) -> Self {
Self { inner, key }
}
}
impl StorageBackend for EncryptedBackend {
fn begin_write(&self) -> Result<Box<dyn WriteTxn + '_>, TalaDbError> {
let inner_txn = self.inner.begin_write()?;
Ok(Box::new(EncryptedWriteTxn {
inner: inner_txn,
key: &self.key,
}))
}
fn begin_read(&self) -> Result<Box<dyn ReadTxn + '_>, TalaDbError> {
let inner_txn = self.inner.begin_read()?;
Ok(Box::new(EncryptedReadTxn {
inner: inner_txn,
key: &self.key,
}))
}
fn set_durability(&self, eventual: bool) {
self.inner.set_durability(eventual);
}
fn flush(&self) -> Result<(), TalaDbError> {
self.inner.flush()
}
}
struct EncryptedWriteTxn<'a> {
inner: Box<dyn WriteTxn + 'a>,
key: &'a EncryptionKey,
}
impl<'a> WriteTxn for EncryptedWriteTxn<'a> {
fn count_entries(&self, table: &str) -> Result<u64, TalaDbError> {
self.inner.count_entries(table)
}
fn put(&mut self, table: &str, key: &[u8], value: &[u8]) -> Result<(), TalaDbError> {
let encrypted = encrypt(self.key, table, key, value)?;
self.inner.put(table, key, &encrypted)
}
fn delete(&mut self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError> {
self.inner.delete(table, key)
}
fn get(&self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError> {
match self.inner.get(table, key)? {
Some(data) => Ok(Some(decrypt(self.key, table, key, &data)?)),
None => Ok(None),
}
}
fn range(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
) -> Result<KvPairs, TalaDbError> {
let raw = self.inner.range(table, start, end)?;
raw.into_iter()
.map(|(k, v)| {
let plain = decrypt(self.key, table, &k, &v)?;
Ok((k, plain))
})
.collect()
}
fn scan(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
f: crate::engine::ScanFn<'_>,
) -> Result<(), TalaDbError> {
self.inner.scan(table, start, end, &mut |k, v| {
let plain = decrypt(self.key, table, k, v)?;
f(k, &plain)
})
}
fn commit(self: Box<Self>) -> Result<(), TalaDbError> {
self.inner.commit()
}
fn list_tables(&self) -> Result<Vec<String>, TalaDbError> {
self.inner.list_tables()
}
fn delete_table(&mut self, table: &str) -> Result<bool, TalaDbError> {
self.inner.delete_table(table)
}
fn get_many(&self, table: &str, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, TalaDbError> {
self.inner
.get_many(table, keys)?
.into_iter()
.zip(keys)
.map(|(v, k)| match v {
Some(data) => Ok(Some(decrypt(self.key, table, k, &data)?)),
None => Ok(None),
})
.collect()
}
fn apply_batch(
&mut self,
table: &str,
ops: &[crate::engine::KvOp<'_>],
) -> Result<(), TalaDbError> {
use crate::engine::KvOp;
let mut ciphertexts: Vec<Option<Vec<u8>>> = Vec::with_capacity(ops.len());
for op in ops {
match op {
KvOp::Put(k, v) => ciphertexts.push(Some(encrypt(self.key, table, k, v)?)),
KvOp::Delete(_) => ciphertexts.push(None),
}
}
let inner_ops: Vec<KvOp<'_>> = ops
.iter()
.zip(&ciphertexts)
.map(|(op, ct)| match (op, ct) {
(KvOp::Put(k, _), Some(ct)) => KvOp::Put(k, ct.as_slice()),
(KvOp::Delete(k), _) => KvOp::Delete(k),
(KvOp::Put(k, v), None) => KvOp::Put(k, v),
})
.collect();
self.inner.apply_batch(table, &inner_ops)
}
}
struct EncryptedReadTxn<'a> {
inner: Box<dyn ReadTxn + 'a>,
key: &'a EncryptionKey,
}
impl<'a> ReadTxn for EncryptedReadTxn<'a> {
fn get(&self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError> {
match self.inner.get(table, key)? {
Some(data) => Ok(Some(decrypt(self.key, table, key, &data)?)),
None => Ok(None),
}
}
fn range(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
) -> Result<KvPairs, TalaDbError> {
let raw = self.inner.range(table, start, end)?;
raw.into_iter()
.map(|(k, v)| {
let plain = decrypt(self.key, table, &k, &v)?;
Ok((k, plain))
})
.collect()
}
fn scan_all(&self, table: &str) -> Result<KvPairs, TalaDbError> {
let raw = self.inner.scan_all(table)?;
raw.into_iter()
.map(|(k, v)| {
let plain = decrypt(self.key, table, &k, &v)?;
Ok((k, plain))
})
.collect()
}
fn list_tables(&self) -> Result<Vec<String>, TalaDbError> {
self.inner.list_tables()
}
fn count_entries(&self, table: &str) -> Result<u64, TalaDbError> {
self.inner.count_entries(table)
}
fn get_many(&self, table: &str, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, TalaDbError> {
self.inner
.get_many(table, keys)?
.into_iter()
.zip(keys)
.map(|(v, k)| match v {
Some(data) => Ok(Some(decrypt(self.key, table, k, &data)?)),
None => Ok(None),
})
.collect()
}
fn scan(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
f: crate::engine::ScanFn<'_>,
) -> Result<(), TalaDbError> {
let mut err = None;
self.inner.scan(
table,
start,
end,
&mut |k, v| match decrypt(self.key, table, k, v) {
Ok(plain) => f(k, &plain),
Err(e) => {
err = Some(e);
Ok(crate::engine::ScanFlow::Stop)
}
},
)?;
match err {
Some(e) => Err(e),
None => Ok(()),
}
}
}
pub fn derive_key(
passphrase: &str,
salt: &[u8],
iterations: u32,
) -> Result<EncryptionKey, TalaDbError> {
#[cfg(feature = "encryption")]
{
use pbkdf2::pbkdf2_hmac;
use sha2::Sha256;
if iterations < MIN_PBKDF2_ITERATIONS {
return Err(TalaDbError::Config(format!(
"PBKDF2 iterations too low ({iterations}); \
minimum is {MIN_PBKDF2_ITERATIONS}"
)));
}
let mut key = zeroize::Zeroizing::new([0u8; 32]);
pbkdf2_hmac::<Sha256>(passphrase.as_bytes(), salt, iterations, key.as_mut());
Ok(key)
}
#[cfg(not(feature = "encryption"))]
{
let _ = (passphrase, salt, iterations);
Err(TalaDbError::Encryption(
"derive_key() called but the `encryption` feature is not enabled; \
enable it in Cargo.toml: taladb-core = { features = [\"encryption\"] }"
.into(),
))
}
}
#[cfg(feature = "encryption")]
pub fn migrate_encrypted_v0_to_v1(
backend: &dyn crate::engine::StorageBackend,
key: &EncryptionKey,
) -> Result<usize, TalaDbError> {
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Nonce};
let rtxn = backend.begin_read()?;
let tables = rtxn.list_tables()?;
drop(rtxn);
let mut total = 0usize;
for table in &tables {
let rtxn = backend.begin_read()?;
let pairs = rtxn.scan_all(table)?;
drop(rtxn);
if pairs.is_empty() {
continue;
}
let cipher = Aes256Gcm::new_from_slice(key.as_ref())
.map_err(|e| TalaDbError::Encryption(e.to_string()))?;
let mut updates: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
for (raw_key, raw_val) in &pairs {
if raw_val.first() == Some(&CRYPTO_FORMAT_V1)
&& raw_val.len() >= 1 + NONCE_LEN_V1 + 16
&& decrypt(key, table, raw_key, raw_val).is_ok()
{
continue; }
if raw_val.len() < NONCE_LEN_V1 + 16 {
return Err(TalaDbError::Encryption(format!(
"migrate v0→v1: value in table \"{table}\" is too short \
to be a valid v0 ciphertext ({} bytes)",
raw_val.len()
)));
}
let nonce_bytes = &raw_val[..NONCE_LEN_V1];
let ciphertext = &raw_val[NONCE_LEN_V1..];
let nonce = Nonce::try_from(nonce_bytes).map_err(|_| {
TalaDbError::Encryption("legacy value has a malformed nonce".into())
})?;
let plaintext = cipher.decrypt(&nonce, ciphertext).map_err(|e| {
TalaDbError::Encryption(format!(
"migrate v0→v1: failed to decrypt value in table \"{table}\": {e}"
))
})?;
let new_val = encrypt(key, table, raw_key, &plaintext)?;
updates.push((raw_key.clone(), new_val));
}
if updates.is_empty() {
continue;
}
let mut wtxn = backend.begin_write()?;
for (k, v) in &updates {
wtxn.put(table, k, v)?;
}
wtxn.commit()?;
total += updates.len();
}
Ok(total)
}
#[cfg(feature = "encryption")]
#[derive(Clone)]
pub struct FieldEncryptionConfig {
pub fields: Vec<String>,
pub key: EncryptionKey,
}
#[cfg(feature = "encryption")]
pub fn encrypt_fields(
doc: &mut crate::document::Document,
config: &FieldEncryptionConfig,
) -> Result<(), crate::error::TalaDbError> {
let doc_id = doc.id;
for (name, val) in &mut doc.fields {
if config.fields.iter().any(|f| f == name) {
let aad_key = field_aad(doc_id, name);
let plain = postcard::to_allocvec(val)
.map_err(|e| crate::error::TalaDbError::Serialization(e.to_string()))?;
let ciphertext = encrypt(&config.key, "field", &aad_key, &plain)?;
*val = crate::document::Value::Bytes(ciphertext);
}
}
Ok(())
}
#[cfg(feature = "encryption")]
fn field_aad(doc_id: ulid::Ulid, field: &str) -> Vec<u8> {
format!("field:{doc_id}:{field}").into_bytes()
}
#[cfg(feature = "encryption")]
fn legacy_field_aad(field: &str) -> Vec<u8> {
format!("field:{field}").into_bytes()
}
#[cfg(feature = "encryption")]
pub fn decrypt_fields(
doc: &mut crate::document::Document,
config: &FieldEncryptionConfig,
) -> Result<(), crate::error::TalaDbError> {
let doc_id = doc.id;
for (name, val) in &mut doc.fields {
if config.fields.iter().any(|f| f == name)
&& let crate::document::Value::Bytes(ciphertext) = val
{
let plain = match decrypt(&config.key, "field", &field_aad(doc_id, name), ciphertext) {
Ok(p) => p,
Err(_) => decrypt(&config.key, "field", &legacy_field_aad(name), ciphertext)?,
};
let original: crate::document::Value = postcard::from_bytes(&plain)
.map_err(|e| crate::error::TalaDbError::Serialization(e.to_string()))?;
*val = original;
}
}
Ok(())
}
#[cfg(feature = "encryption")]
pub fn rekey(
backend: &dyn crate::engine::StorageBackend,
old_key: &EncryptionKey,
new_key: &EncryptionKey,
) -> Result<usize, TalaDbError> {
let rtxn = backend.begin_read()?;
let tables = rtxn.list_tables()?;
drop(rtxn);
let mut total = 0usize;
for table in &tables {
let rtxn = backend.begin_read()?;
let pairs = rtxn.scan_all(table)?;
drop(rtxn);
if pairs.is_empty() {
continue;
}
let mut updates: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
for (raw_key, raw_val) in &pairs {
let plaintext = match decrypt(old_key, table, raw_key, raw_val) {
Ok(p) => p,
Err(e) => {
if decrypt(new_key, table, raw_key, raw_val).is_ok() {
continue; }
return Err(e);
}
};
let new_val = encrypt(new_key, table, raw_key, &plaintext)?;
updates.push((raw_key.clone(), new_val));
}
let mut wtxn = backend.begin_write()?;
for (k, v) in &updates {
wtxn.put(table, k, v)?;
}
wtxn.commit()?;
total += updates.len();
}
Ok(total)
}
#[cfg(all(test, feature = "encryption"))]
mod tests {
use super::*;
fn test_key() -> EncryptionKey {
zeroize::Zeroizing::new([0x42u8; 32])
}
#[test]
fn writer_scan_stops_before_decrypting_an_untouched_corrupt_record() {
use crate::engine::{RedbBackend, ScanFlow};
let raw = Arc::new(RedbBackend::open_in_memory().unwrap());
let encrypted = EncryptedBackend::new(raw.clone(), test_key());
let mut txn = encrypted.begin_write().unwrap();
txn.put("items", b"a", b"valid").unwrap();
txn.commit().unwrap();
let mut txn = raw.begin_write().unwrap();
txn.put("items", b"b", b"corrupt ciphertext").unwrap();
txn.commit().unwrap();
let txn = encrypted.begin_write().unwrap();
let mut seen = 0;
txn.scan("items", Bound::Unbounded, Bound::Unbounded, &mut |k, v| {
assert_eq!(k, b"a");
assert_eq!(v, b"valid");
seen += 1;
Ok(ScanFlow::Stop)
})
.unwrap();
assert_eq!(seen, 1);
assert!(
txn.scan("items", Bound::Unbounded, Bound::Unbounded, &mut |_, _| Ok(
ScanFlow::Continue
))
.is_err()
);
}
#[test]
fn round_trip() {
let key = test_key();
let plaintext = b"hello world";
let ciphertext = encrypt(&key, "my_table", b"doc_id_1", plaintext).unwrap();
let decrypted = decrypt(&key, "my_table", b"doc_id_1", &ciphertext).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn wrong_key_fails() {
let key1 = test_key();
let key2 = zeroize::Zeroizing::new([0x99u8; 32]);
let ct = encrypt(&key1, "t", b"k", b"secret").unwrap();
assert!(decrypt(&key2, "t", b"k", &ct).is_err());
}
#[test]
fn aad_table_mismatch_fails() {
let key = test_key();
let ct = encrypt(&key, "table_a", b"key1", b"data").unwrap();
assert!(decrypt(&key, "table_b", b"key1", &ct).is_err());
}
#[test]
fn aad_key_mismatch_fails() {
let key = test_key();
let ct = encrypt(&key, "table_a", b"key1", b"data").unwrap();
assert!(decrypt(&key, "table_a", b"key2", &ct).is_err());
}
#[test]
fn unknown_version_fails() {
let key = test_key();
let mut ct = encrypt(&key, "t", b"k", b"data").unwrap();
ct[0] = 0xFF; let err = decrypt(&key, "t", b"k", &ct).unwrap_err();
assert!(
err.to_string()
.contains("unsupported encrypted-value format version")
);
}
#[test]
fn truncated_ciphertext_fails() {
let key = test_key();
assert!(decrypt(&key, "t", b"k", &[]).is_err());
assert!(decrypt(&key, "t", b"k", &[CRYPTO_FORMAT_V1; 10]).is_err());
}
#[test]
fn derive_key_enforces_min_iterations() {
let err = derive_key("pass", b"salt1234567890ab", 999).unwrap_err();
assert!(err.to_string().contains("iterations too low"));
}
#[test]
fn derive_key_at_min_iterations_succeeds() {
let key = derive_key("pass", b"salt1234567890ab", MIN_PBKDF2_ITERATIONS).unwrap();
assert_eq!(key.len(), 32);
}
#[test]
fn field_encryption_round_trip_and_doc_binding() {
use crate::document::{Document, Value};
let config = FieldEncryptionConfig {
fields: vec!["ssn".into()],
key: test_key(),
};
let mut doc_a = Document::new(vec![("ssn".into(), Value::Str("111-22-3333".into()))]);
let mut doc_b = Document::new(vec![("ssn".into(), Value::Str("999-88-7777".into()))]);
encrypt_fields(&mut doc_a, &config).unwrap();
encrypt_fields(&mut doc_b, &config).unwrap();
let stolen = doc_a.get("ssn").unwrap().clone();
doc_b.set("ssn", stolen);
assert!(
decrypt_fields(&mut doc_b, &config).is_err(),
"ciphertext moved between documents must fail authentication"
);
decrypt_fields(&mut doc_a, &config).unwrap();
assert_eq!(doc_a.get("ssn"), Some(&Value::Str("111-22-3333".into())));
}
#[test]
fn field_encryption_decrypts_legacy_field_only_aad() {
use crate::document::{Document, Value};
let config = FieldEncryptionConfig {
fields: vec!["token".into()],
key: test_key(),
};
let mut doc = Document::new(vec![("token".into(), Value::Str("secret".into()))]);
let plain = postcard::to_allocvec(doc.get("token").unwrap()).unwrap();
let legacy_ct = encrypt(&config.key, "field", b"field:token", &plain).unwrap();
doc.set("token", Value::Bytes(legacy_ct));
decrypt_fields(&mut doc, &config).unwrap();
assert_eq!(doc.get("token"), Some(&Value::Str("secret".into())));
}
#[test]
fn rekey_round_trip() {
use crate::engine::{RedbBackend, StorageBackend};
let old_key: EncryptionKey = zeroize::Zeroizing::new([0x11u8; 32]);
let new_key: EncryptionKey = zeroize::Zeroizing::new([0x22u8; 32]);
let raw = std::sync::Arc::new(RedbBackend::open_in_memory().unwrap());
let enc = EncryptedBackend::new(raw.clone(), old_key.clone());
let mut wtxn = enc.begin_write().unwrap();
wtxn.put("docs::test", b"k1", b"hello").unwrap();
wtxn.put("docs::test", b"k2", b"world").unwrap();
wtxn.commit().unwrap();
let count = super::rekey(raw.as_ref(), &old_key, &new_key).unwrap();
assert_eq!(count, 2);
let enc_old = EncryptedBackend::new(raw.clone(), old_key);
let rtxn_old = enc_old.begin_read().unwrap();
assert!(rtxn_old.get("docs::test", b"k1").is_err());
let enc_new = EncryptedBackend::new(raw, new_key);
let rtxn_new = enc_new.begin_read().unwrap();
assert_eq!(
rtxn_new.get("docs::test", b"k1").unwrap().unwrap(),
b"hello"
);
assert_eq!(
rtxn_new.get("docs::test", b"k2").unwrap().unwrap(),
b"world"
);
}
#[test]
fn rekey_wrong_old_key_fails() {
use crate::engine::{RedbBackend, StorageBackend};
let old_key: EncryptionKey = zeroize::Zeroizing::new([0x11u8; 32]);
let wrong_key: EncryptionKey = zeroize::Zeroizing::new([0xFFu8; 32]);
let new_key: EncryptionKey = zeroize::Zeroizing::new([0x22u8; 32]);
let raw = std::sync::Arc::new(RedbBackend::open_in_memory().unwrap());
let enc = EncryptedBackend::new(raw.clone(), old_key);
let mut wtxn = enc.begin_write().unwrap();
wtxn.put("t", b"k", b"v").unwrap();
wtxn.commit().unwrap();
assert!(super::rekey(raw.as_ref(), &wrong_key, &new_key).is_err());
}
}