use std::path::Path;
use std::sync::Arc;
use super::Db;
use super::flush_tests::PATH;
use crate::log::fault::FaultStorage;
use crate::storage::Storage as _;
use crate::{
Cipher, DbOptions, Error, KeyProvider, KeyspaceId, ReadOptions, RootKey, WriteBatch,
WriteOptions,
};
#[derive(Debug)]
pub(super) struct Keys(Vec<(u32, u8)>);
impl KeyProvider for Keys {
fn root_key(&self, key_id: u32) -> Option<RootKey> {
self.0
.iter()
.find(|(id, _)| *id == key_id)
.map(|(_, byte)| RootKey::new([*byte; 32]))
}
}
pub(super) fn encrypted(keys: Vec<(u32, u8)>, key_id: u32) -> DbOptions {
DbOptions::builder()
.encryption(Arc::new(Keys(keys)), key_id)
.build()
.expect("valid")
}
fn key(index: u32) -> Vec<u8> {
format!("secret-key-{index:04}").into_bytes()
}
fn value(index: u32) -> Vec<u8> {
format!("secret-value-{index:04}").into_bytes()
}
async fn fill(db: &Db, keys: u32) {
for half in [0..keys / 2, keys / 2..keys] {
let mut batch = WriteBatch::new();
for index in half {
batch
.put(KeyspaceId::DEFAULT, &key(index), &value(index))
.expect("entry");
}
db.write(&batch, &WriteOptions::default())
.await
.expect("written");
db.flush().await.expect("flushed");
}
db.compact_range(KeyspaceId::DEFAULT, b"", None)
.await
.expect("compacted");
}
async fn read_all(db: &Db, keys: u32) -> crate::Result<()> {
for index in 0..keys {
let found = db
.get(KeyspaceId::DEFAULT, &key(index), &ReadOptions::default())
.await?;
assert_eq!(found, Some(value(index)), "key {index}");
}
Ok(())
}
fn table_files(storage: &FaultStorage) -> Vec<String> {
storage
.list(Path::new(PATH))
.expect("listable")
.into_iter()
.filter(|name| name.ends_with(".sst"))
.collect()
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack
.windows(needle.len())
.any(|window| window == needle)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_encrypted_database_seals_its_tables_and_reads_them_back() {
let storage = FaultStorage::default();
let options = encrypted(vec![(1, 7)], 1);
let db = Db::open_with_storage(PATH, options.clone(), Arc::new(storage.clone()))
.await
.expect("opened");
fill(&db, 200).await;
read_all(&db, 200).await.expect("read");
db.close().await.expect("closed");
let tables = table_files(&storage);
assert!(!tables.is_empty());
for name in &tables {
let bytes = storage.bytes(&Path::new(PATH).join(name));
assert!(!contains(&bytes, b"secret-"), "{name} shows plaintext");
}
let db = Db::open_with_storage(PATH, options, Arc::new(storage.clone()))
.await
.expect("reopened");
read_all(&db, 200).await.expect("read after reopen");
db.close().await.expect("closed");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn control_a_plain_database_shows_its_keys() {
let storage = FaultStorage::default();
let db = Db::open_with_storage(PATH, DbOptions::default(), Arc::new(storage.clone()))
.await
.expect("opened");
fill(&db, 200).await;
db.close().await.expect("closed");
let shown = table_files(&storage)
.iter()
.any(|name| contains(&storage.bytes(&Path::new(PATH).join(name)), b"secret-"));
assert!(shown);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_encrypted_database_opens_only_with_its_key() {
let storage = FaultStorage::default();
let db = Db::open_with_storage(PATH, encrypted(vec![(1, 7)], 1), Arc::new(storage.clone()))
.await
.expect("opened");
fill(&db, 20).await;
db.close().await.expect("closed");
let open = |options| Db::open_with_storage(PATH, options, Arc::new(storage.clone()));
let wrong = open(encrypted(vec![(1, 8)], 1)).await;
assert!(
matches!(wrong, Err(Error::WrongKey { key_id: 1 })),
"{:?}",
wrong.as_ref().err()
);
let missing = open(encrypted(vec![(2, 7)], 2)).await;
assert!(
matches!(missing, Err(Error::KeyNotFound { key_id: 1 })),
"{:?}",
missing.as_ref().err()
);
let none = open(DbOptions::default()).await;
assert!(
matches!(none, Err(Error::KeyNotFound { key_id: 1 })),
"{:?}",
none.as_ref().err()
);
let plain = FaultStorage::default();
let db = Db::open_with_storage(PATH, DbOptions::default(), Arc::new(plain.clone()))
.await
.expect("opened");
db.close().await.expect("closed");
let later = Db::open_with_storage(PATH, encrypted(vec![(1, 7)], 1), Arc::new(plain)).await;
assert!(
matches!(later, Err(Error::Unsupported { .. })),
"{:?}",
later.as_ref().err()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_changed_table_byte_is_corruption() {
let storage = FaultStorage::default();
let options = encrypted(vec![(1, 7)], 1);
let db = Db::open_with_storage(PATH, options.clone(), Arc::new(storage.clone()))
.await
.expect("opened");
fill(&db, 20).await;
db.close().await.expect("closed");
for name in table_files(&storage) {
let path = Path::new(PATH).join(name);
let mut bytes = storage.bytes(&path);
bytes[42] ^= 1;
let mut file = storage.create(&path).expect("created");
file.append(&bytes).expect("written");
file.sync().expect("synced");
}
let db = Db::open_with_storage(PATH, options, Arc::new(storage.clone()))
.await
.expect("reopened: the index and filter are intact");
let read = read_all(&db, 20).await;
assert!(matches!(read, Err(Error::Corruption { .. })), "{read:?}");
db.close().await.expect("closed");
}
fn ciphers(storage: &FaultStorage) -> Vec<(String, u8)> {
storage
.list(Path::new(PATH))
.expect("listable")
.into_iter()
.filter(|name| {
name.ends_with(".sst") || name.starts_with("MANIFEST-") || name.ends_with(".log")
})
.map(|name| {
let bytes = storage.bytes(&Path::new(PATH).join(&name));
(name, bytes[5])
})
.collect()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_pinned_cipher_is_used_for_every_new_file() {
for (pinned, code) in [
(Some(Cipher::ChaCha20Poly1305), 2),
(Some(Cipher::Aes256Gcm), 1),
(
None,
if Cipher::preferred() == Cipher::Aes256Gcm {
1
} else {
2
},
),
] {
let storage = FaultStorage::default();
let mut builder = DbOptions::builder().encryption(Arc::new(Keys(vec![(1, 7)])), 1);
if let Some(cipher) = pinned {
builder = builder.encryption_cipher(cipher);
}
let options = builder.build().expect("valid");
assert_eq!(options.encryption_cipher(), pinned);
let db = Db::open_with_storage(PATH, options.clone(), Arc::new(storage.clone()))
.await
.expect("opened");
fill(&db, 20).await;
db.close().await.expect("closed");
let files = ciphers(&storage);
assert!(
files.iter().any(|(name, _)| name.ends_with(".sst")),
"{files:?}"
);
assert!(
files.iter().all(|(_, byte)| *byte == code),
"{pinned:?}: {files:?}"
);
let db = Db::open_with_storage(PATH, options, Arc::new(storage))
.await
.expect("reopened");
let read = db
.get(KeyspaceId::DEFAULT, &key(3), &ReadOptions::default())
.await
.expect("read");
assert_eq!(read, Some(value(3)));
db.close().await.expect("closed");
}
}