use std::sync::Arc;
use crate::Hlc;
use crate::codec::keys::{KeyKind, LogicalKey};
use crate::codec::manifest::{EncryptionInfo, KeyFile};
use crate::crypto::{self, DataKey, KdfParams};
use crate::database::{Inner, Secret};
use crate::error::{Error, ErrorKind, Result};
use crate::model::{DbId, DeviceId};
use crate::remote::RemoteEntry;
fn key_required() -> Error {
Error::new(
ErrorKind::KeyRequired,
"the database is encrypted; unlock it with its password or key",
)
}
async fn blocking<T: Send + 'static>(f: impl FnOnce() -> Result<T> + Send + 'static) -> Result<T> {
tokio::task::spawn_blocking(f)
.await
.map_err(|e| Error::new(ErrorKind::Internal, "key derivation task failed").with_source(e))?
}
async fn newest_remote(inner: &Inner, db_id: DbId) -> Result<Option<(Hlc, DeviceId, String)>> {
let mut best: Option<(Hlc, DeviceId, String)> = None;
for e in inner.remote.list(&format!("{db_id}/keys/")).await? {
if let Some(LogicalKey {
kind: KeyKind::KeyFile(dev, hlc),
..
}) = LogicalKey::parse(&e.key)
&& best.as_ref().is_none_or(|(h, _, _)| hlc > *h)
{
best = Some((hlc, dev, e.key));
}
}
Ok(best)
}
async fn refresh(inner: &Arc<Inner>, db_id: DbId) -> Result<Option<DeviceId>> {
let Some((hlc, owner, key)) = newest_remote(inner, db_id).await? else {
return Ok(None);
};
if let Some(bytes) = inner.remote.get(&key).await? {
KeyFile::from_json(&bytes)?;
inner
.store(move |s| s.cache_key_file(hlc, &key, &bytes))
.await?;
}
Ok(Some(owner))
}
pub(crate) async fn unlock_password(inner: &Arc<Inner>, db_id: DbId, password: &str) -> Result<()> {
let (check, cached) = inner
.store(|s| Ok((s.key_check()?, s.cached_key_file()?)))
.await?;
let check = check.ok_or_else(|| Error::invalid_input("the database is not encrypted"))?;
let cached = match cached {
Some(c) => c,
None => {
refresh(inner, db_id).await?;
inner.store(|s| s.cached_key_file()).await?.ok_or_else(|| {
Error::new(
ErrorKind::RemoteInconsistent,
"encrypted database has no key file",
)
})?
}
};
let (_, name, json) = cached;
let file = KeyFile::from_json(&json)?;
let password = zeroize::Zeroizing::new(password.to_owned());
let dek = blocking(move || {
let kdf = KdfParams {
m_kib: file.m_kib,
t: file.t,
p: file.p,
salt: file.salt,
};
crypto::unwrap_dek(&password, &kdf, &file.wrapped_dek, &db_id, &name)
})
.await?;
if !crypto::verify_key_check(&dek, &db_id, &check) {
return Err(Error::new(
ErrorKind::WrongPassword,
"the password does not unlock this database",
));
}
inner.set_key(Some(dek));
Ok(())
}
pub(crate) async fn unlock_key(inner: &Arc<Inner>, db_id: DbId, dek: DataKey) -> Result<()> {
let check = inner
.store(|s| s.key_check())
.await?
.ok_or_else(|| Error::invalid_input("the database is not encrypted"))?;
if !crypto::verify_key_check(&dek, &db_id, &check) {
return Err(Error::new(
ErrorKind::WrongPassword,
"this key does not belong to the database",
));
}
inner.set_key(Some(dek));
Ok(())
}
pub(crate) async fn unlock_with_secret(inner: &Arc<Inner>, db_id: DbId) -> Result<bool> {
let secret = inner
.secret
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
match secret {
Some(Secret::Password(p)) => unlock_password(inner, db_id, &p).await.map(|_| true),
Some(Secret::Key(k)) => unlock_key(inner, db_id, k).await.map(|_| true),
None => Ok(false),
}
}
async fn write_key_file(
inner: &Arc<Inner>,
db_id: DbId,
dek: &DataKey,
password: &str,
) -> Result<String> {
let hlc = inner.store(|s| s.tick()).await?;
let name = LogicalKey::key_file(db_id, inner.device(), hlc).to_string();
let (m, t, p) = inner.kdf_cost;
let (dek2, password, name2) = (
dek.clone(),
zeroize::Zeroizing::new(password.to_owned()),
name.clone(),
);
let file = blocking(move || {
let kdf = KdfParams::with_cost(m, t, p)?;
let wrapped = crypto::wrap_dek(&dek2, &password, &kdf, &db_id, &name2, None)?;
Ok(KeyFile {
m_kib: kdf.m_kib,
t: kdf.t,
p: kdf.p,
salt: kdf.salt,
wrapped_dek: wrapped,
})
})
.await?;
let json = file.to_json();
super::put_once(inner, &LogicalKey::parse(&name).expect("valid key"), &json).await?;
let n = name.clone();
inner
.store(move |s| s.cache_key_file(hlc, &n, &json))
.await?;
Ok(name)
}
pub(crate) async fn create(inner: &Arc<Inner>, db_id: DbId) -> Result<Option<EncryptionInfo>> {
if !inner.encrypt {
return Ok(None);
}
let password = match inner
.secret
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
{
Some(Secret::Password(p)) => p,
_ => {
return Err(Error::invalid_input(
"an encrypted database needs a password",
));
}
};
let dek = DataKey::random()?;
let check = crypto::key_check(&dek, &db_id, None)?;
write_key_file(inner, db_id, &dek, &password).await?;
let c = check.clone();
inner.store(move |s| s.set_key_check(&c)).await?;
inner.set_key(Some(dek));
Ok(Some(EncryptionInfo { key_check: check }))
}
pub(crate) async fn join(inner: &Arc<Inner>, db_id: DbId, info: &EncryptionInfo) -> Result<()> {
let check = info.key_check.clone();
inner.store(move |s| s.set_key_check(&check)).await?;
refresh(inner, db_id).await?;
if !unlock_with_secret(inner, db_id).await? {
return Err(key_required());
}
Ok(())
}
pub(crate) async fn required_key(inner: &Arc<Inner>) -> Result<Option<DataKey>> {
if inner.store(|s| s.key_check()).await?.is_none() {
return Ok(None);
}
inner.key().map(Some).ok_or_else(key_required)
}
pub(crate) async fn on_entries(
inner: &Arc<Inner>,
db_id: DbId,
entries: &[RemoteEntry],
) -> Result<bool> {
let seen = entries.iter().any(|e| {
LogicalKey::parse(&e.key)
.is_some_and(|k| k.db_id == db_id && matches!(k.kind, KeyKind::KeyFile(..)))
});
if !seen || inner.store(|s| s.key_check()).await?.is_none() {
return Ok(false);
}
let before = inner
.store(|s| s.cached_key_file())
.await?
.map(|(h, _, _)| h);
let owner = refresh(inner, db_id).await?;
let after = inner
.store(|s| s.cached_key_file())
.await?
.map(|(h, _, _)| h);
let me = inner.device();
if let (Some(owner), Some(newest)) = (owner, after)
&& owner != me
{
delete_own_older(inner, db_id, newest).await?;
}
Ok(before.is_some() && after != before)
}
async fn delete_own_older(inner: &Arc<Inner>, db_id: DbId, newest: Hlc) -> Result<()> {
let me = inner.device();
for e in inner.remote.list(&format!("{db_id}/keys/{me}/")).await? {
if let Some(LogicalKey {
kind: KeyKind::KeyFile(_, h),
..
}) = LogicalKey::parse(&e.key)
&& h < newest
{
inner.remote.delete(&e.key).await?;
}
}
Ok(())
}
pub(crate) async fn change_password(
inner: &Arc<Inner>,
db_id: DbId,
old: &str,
new: &str,
) -> Result<()> {
refresh(inner, db_id).await?;
unlock_password(inner, db_id, old).await?;
let dek = inner.key().ok_or_else(key_required)?;
let name = write_key_file(inner, db_id, &dek, new).await?;
let newest = match LogicalKey::parse(&name).map(|k| k.kind) {
Some(KeyKind::KeyFile(_, h)) => h,
_ => unreachable!("write_key_file returns a key file name"),
};
*inner.secret.lock().unwrap_or_else(|e| e.into_inner()) =
Some(Secret::Password(zeroize::Zeroizing::new(new.to_owned())));
delete_own_older(inner, db_id, newest).await
}
pub(crate) fn seal_json(
key: Option<&DataKey>,
logical_key: &str,
json: Vec<u8>,
) -> Result<Vec<u8>> {
match key {
Some(k) => crypto::seal_json(k, logical_key, &json),
None => Ok(json),
}
}