use crate::hardened_bootstrap::derive_storage_key;
use crate::store::KeyspaceHandle;
pub async fn load_storage_key_for_cli(
config: &crate::config::AppConfig,
) -> Result<Option<[u8; 32]>, Box<dyn std::error::Error>> {
if !config.hardened.enabled {
return Ok(None);
}
let seed_store = crate::keys::seed_store::create_seed_store(config)
.map_err(|e| format!("hardened: failed to create seed store: {e}"))?;
let seed = zeroize::Zeroizing::new(
seed_store
.get()
.await
.map_err(|e| format!("hardened: seed load failed: {e}"))?
.ok_or("hardened: no seed in secret store — run `vta setup` first")?,
);
let key = *derive_storage_key(&seed, &config.hardened.storage_key_salt);
Ok(Some(key))
}
pub struct CliStore {
pub store: crate::store::Store,
enc_key: Option<[u8; 32]>,
}
impl std::ops::Deref for CliStore {
type Target = crate::store::Store;
fn deref(&self) -> &Self::Target {
&self.store
}
}
impl Drop for CliStore {
fn drop(&mut self) {
if let Some(ref mut key) = self.enc_key {
zeroize::Zeroize::zeroize(key);
}
}
}
impl CliStore {
pub async fn open(
config: &crate::config::AppConfig,
) -> Result<Self, Box<dyn std::error::Error>> {
let enc_key = load_storage_key_for_cli(config).await?;
let store = crate::store::Store::open(&config.store)
.map_err(|e| format!("failed to open store: {e}"))?;
Ok(Self { store, enc_key })
}
pub fn from_store(store: crate::store::Store, enc_key: Option<[u8; 32]>) -> Self {
Self { store, enc_key }
}
pub fn enc_key(&self) -> Option<[u8; 32]> {
self.enc_key
}
pub fn keyspace(&self, name: &str) -> Result<KeyspaceHandle, vti_common::error::AppError> {
let ks = self.store.keyspace(name)?;
Ok(match self.enc_key {
Some(key) => ks.with_encryption(key),
None => ks,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const KS_TEST: &str = "test";
const KS_SECRETS: &str = "secrets";
const KS_DATA: &str = "data";
fn temp_cli_store(enc_key: Option<[u8; 32]>) -> (CliStore, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("tempdir");
let config = vti_common::config::StoreConfig {
data_dir: dir.path().to_path_buf(),
};
let store = crate::store::Store::open(&config).expect("open store");
(CliStore::from_store(store, enc_key), dir)
}
#[tokio::test]
async fn cli_store_no_key_returns_bare_handle() {
let (cs, _dir) = temp_cli_store(None);
let ks = cs.keyspace(KS_TEST).expect("keyspace");
assert!(
!ks.is_encrypted(),
"handle must be bare when enc_key is None"
);
ks.insert_raw("row", b"value".to_vec()).await.unwrap();
let raw = cs
.store
.keyspace(KS_TEST)
.unwrap()
.get_raw("row")
.await
.unwrap()
.unwrap();
assert_eq!(raw, b"value", "bare write must be plaintext on disk");
}
#[tokio::test]
async fn cli_store_with_key_returns_encrypted_handle() {
let enc_key = [0xABu8; 32];
let (cs, _dir) = temp_cli_store(Some(enc_key));
let ks = cs.keyspace(KS_SECRETS).expect("keyspace");
assert!(
ks.is_encrypted(),
"handle must be encrypted when enc_key is Some"
);
ks.insert_raw("row", b"sensitive".to_vec()).await.unwrap();
let on_disk = cs
.store
.keyspace(KS_SECRETS)
.unwrap()
.get_raw("row")
.await
.unwrap()
.unwrap();
assert_ne!(on_disk, b"sensitive", "value must be encrypted on disk");
assert!(on_disk.starts_with(b"VAE1"), "must carry VAE1 magic");
}
#[tokio::test]
async fn cli_store_round_trips_with_same_key() {
let enc_key = [0x77u8; 32];
let dir = tempfile::tempdir().expect("tempdir");
let config = vti_common::config::StoreConfig {
data_dir: dir.path().to_path_buf(),
};
{
let store = crate::store::Store::open(&config).expect("open");
let cs = CliStore::from_store(store, Some(enc_key));
cs.keyspace(KS_DATA)
.unwrap()
.insert_raw("key", b"secret".to_vec())
.await
.unwrap();
cs.persist().await.unwrap();
}
{
let store = crate::store::Store::open(&config).expect("reopen");
let cs = CliStore::from_store(store, Some(enc_key));
let val = cs
.keyspace(KS_DATA)
.unwrap()
.get_raw("key")
.await
.unwrap()
.unwrap();
assert_eq!(val, b"secret");
}
}
#[tokio::test]
async fn cli_store_wrong_key_fails_to_decrypt() {
let dir = tempfile::tempdir().expect("tempdir");
let config = vti_common::config::StoreConfig {
data_dir: dir.path().to_path_buf(),
};
{
let store = crate::store::Store::open(&config).expect("open");
let cs = CliStore::from_store(store, Some([0x11u8; 32]));
cs.keyspace(KS_DATA)
.unwrap()
.insert_raw("row", b"secret".to_vec())
.await
.unwrap();
cs.persist().await.unwrap();
}
{
let store = crate::store::Store::open(&config).expect("reopen");
let cs = CliStore::from_store(store, Some([0x22u8; 32]));
let result = cs.keyspace(KS_DATA).unwrap().get_raw("row").await;
assert!(result.is_err(), "wrong key must fail AAD authentication");
}
}
#[test]
fn cli_store_enc_key_accessor() {
let dir = tempfile::tempdir().expect("tempdir");
let config = vti_common::config::StoreConfig {
data_dir: dir.path().to_path_buf(),
};
let store = crate::store::Store::open(&config).expect("open");
let cs_none = CliStore::from_store(store.clone(), None);
assert!(cs_none.enc_key().is_none());
let key = [0x55u8; 32];
let cs_some = CliStore::from_store(store, Some(key));
assert_eq!(cs_some.enc_key(), Some(key));
}
#[tokio::test]
async fn cli_store_deref_persist() {
let (cs, _dir) = temp_cli_store(None);
cs.persist().await.expect("persist via Deref must work");
}
}