use super::*;
use crate::sync::seed_claim::{SEED_STORAGE_BYTES, SeedAuthority};
use std::path::Path;
const SEED_MAGIC: &[u8; 8] = b"AVENSED1";
const SEED_MARKER: &[u8; 8] = b"AVENSPN1";
pub(super) const SEED_BYTES: usize = 8 + 32 + SEED_STORAGE_BYTES + 32;
const SEED_MARKER_BYTES: usize = 8 + 32;
pub(super) const SEED_ITEM: &str = "seed";
const SEED_MARKER_RECORD: &str = "seed-authority";
const SEED_ORIGIN_ITEM: &str = "seed-origin";
const SEED_ORIGIN_BYTES: usize = 44 + crate::sync::client::MAX_SERVER_BYTES;
const SEED_ORIGIN_MISSING: &str = "error sync-setup-server-missing hint=\"the saved setup server is missing; abandon setup with `aven sync reset --force` before starting again\"";
impl ProtectedLocalKeyStore {
pub(crate) async fn bind_seed_origin(
&self,
database: &Database,
server: &str,
) -> anyhow::Result<()> {
self.validate_database(database).await?;
let origin = crate::sync::client::server_origin(server)?;
let established = database.local_seed_genesis_commitment().await?.is_some()
|| database.seed_source_pin().await?.is_some()
|| database.seed_publication_intent_bytes().await?.is_some();
self.prepare()?;
let _guard = self.lock()?;
if let Some(saved) = self.read_owned(SEED_ORIGIN_ITEM, SEED_ORIGIN_BYTES, false)? {
anyhow::ensure!(
saved.as_slice() == origin.as_bytes(),
"error sync-setup-server-mismatch hint=\"resume with an invitation from the original server; if none is available, back up this database and restore it to a new path for a local-only copy\""
);
return Ok(());
}
anyhow::ensure!(
!established && !self.seed_authority_exists()?,
SEED_ORIGIN_MISSING
);
self.write_owned(SEED_ORIGIN_ITEM, SEED_ORIGIN_BYTES, origin.as_bytes())
}
pub(crate) async fn retained_seed_setup(
&self,
database: &Database,
) -> anyhow::Result<(String, [u8; 32])> {
self.validate_database(database).await?;
self.prepare()?;
let _guard = self.lock()?;
let package = self.load_required_locked()?;
let origin = self
.read_owned(SEED_ORIGIN_ITEM, SEED_ORIGIN_BYTES, false)?
.ok_or_else(|| anyhow::anyhow!(SEED_ORIGIN_MISSING))?;
let seed = self
.load_secret(SEED_ITEM, SEED_BYTES)?
.ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::MissingAuthority))?;
let seed = self.decode_seed(&seed, &package)?;
Ok((
String::from_utf8(origin.to_vec())?,
seed.genesis().setup_id(),
))
}
pub async fn prepare_seed_claim(
&self,
database: &Database,
setup_id: [u8; 32],
) -> anyhow::Result<SeedAuthority> {
self.validate_database(database).await?;
anyhow::ensure!(
database
.enrollment_pin()
.await?
.is_none_or(|(_, _, role)| role == "inviter"),
"error enrollment-peer-cannot-become-seed"
);
let mut pin = database.local_seed_genesis_commitment().await?;
if let Some(commitment) = pin
&& self.clear_unfenced_orphan_pin(database, commitment).await?
{
pin = None;
}
let package = if pin.is_some()
|| database
.has_local_shared_state_package_never_dispatched()
.await?
{
self.load_required()?
} else {
self.load_or_create()?
};
let seed = match (self.load_or_create_seed(&package, setup_id, pin), pin) {
(Err(error), Some(commitment))
if error.kind() == ProtectedLocalKeyStoreErrorKind::SetupMismatch
&& database.local_seed_claim_refused().await? =>
{
self.rollback_seed_commitment(database, commitment).await?;
self.load_or_create_seed(&package, setup_id, None)?
}
(result, _) => result?,
};
database.pin_local_seed_genesis(seed.genesis()).await?;
Ok(seed)
}
pub async fn package_seed_capture(
&self,
database: &Database,
blob_dir: &Path,
setup_id: [u8; 32],
) -> anyhow::Result<EncryptedLocalSharedStatePackage> {
let seed = self.prepare_seed_claim(database, setup_id).await?;
self.package_local_capture(database, blob_dir, seed.genesis().commitment())
.await
}
pub(crate) async fn package_seed_capture_validated(
&self,
database: &Database,
blob_dir: &Path,
setup_id: [u8; 32],
capture: Option<NeverDispatchedLocalSharedCapture>,
) -> anyhow::Result<crate::sync::shared_state::validated::ValidatedSeed> {
let seed = self.prepare_seed_claim(database, setup_id).await?;
self.package_local_capture_validated(
database,
blob_dir,
seed.genesis().commitment(),
capture,
)
.await
}
#[cfg(test)]
pub async fn rollback_seed_claim(
&self,
database: &Database,
seed: &SeedAuthority,
) -> anyhow::Result<()> {
self.validate_database(database).await?;
self.rollback_seed_commitment(database, seed.genesis().commitment())
.await
}
async fn rollback_seed_commitment(
&self,
database: &Database,
commitment: [u8; 32],
) -> anyhow::Result<()> {
anyhow::ensure!(
database.seed_source_pin().await?.is_none()
&& database.seed_publication_intent_bytes().await?.is_none()
&& !database
.has_local_shared_state_package_never_dispatched()
.await?,
"error seed-claim-rollback-fenced"
);
{
let _guard = self.lock()?;
let package = self.load_required_locked()?;
if let Some(bytes) = self.load_secret(SEED_ITEM, SEED_BYTES)? {
let stored = self.decode_seed(&bytes, &package)?;
if stored.genesis().commitment() != commitment {
anyhow::bail!("error seed-claim-rollback-conflict");
}
}
self.delete_secret(SEED_ITEM)?;
self.remove_record(SEED_MARKER_RECORD)?;
}
database.rollback_local_seed_genesis(commitment).await
}
async fn clear_unfenced_orphan_pin(
&self,
database: &Database,
commitment: [u8; 32],
) -> anyhow::Result<bool> {
if database.seed_source_pin().await?.is_some()
|| database.seed_publication_intent_bytes().await?.is_some()
|| database
.has_local_shared_state_package_never_dispatched()
.await?
{
return Ok(false);
}
{
let _guard = self.lock()?;
if self.load_secret(SEED_ITEM, SEED_BYTES)?.is_some() {
return Ok(false);
}
self.remove_record(SEED_MARKER_RECORD)?;
}
database.rollback_local_seed_genesis(commitment).await?;
Ok(true)
}
pub(super) fn seed_authority_exists(&self) -> StoreResult<bool> {
Ok(self
.read_record(SEED_MARKER_RECORD, SEED_MARKER_BYTES)?
.is_some()
|| self.load_secret(SEED_ITEM, SEED_BYTES)?.is_some())
}
fn load_or_create_seed(
&self,
package: &ProtectedLocalPackageKey,
setup: [u8; 32],
pin: Option<[u8; 32]>,
) -> StoreResult<SeedAuthority> {
self.prepare()?;
let _guard = self.lock()?;
let marker = self.read_record(SEED_MARKER_RECORD, SEED_MARKER_BYTES)?;
let seed = match self.load_secret(SEED_ITEM, SEED_BYTES)? {
Some(bytes) => self.decode_seed(&bytes, package)?,
None if marker.is_some() || pin.is_some() => {
return Err(error(ProtectedLocalKeyStoreErrorKind::MissingAuthority));
}
None => {
let seed = SeedAuthority::generate(package.context(), package.package_key(), setup)
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
let encoded = self.encode_seed(&seed)?;
self.create_secret(SEED_ITEM, &encoded)?;
let saved = self
.load_secret(SEED_ITEM, SEED_BYTES)?
.ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
if saved.as_slice() != encoded.as_slice() {
return Err(error(ProtectedLocalKeyStoreErrorKind::WriteFailed));
}
self.decode_seed(&saved, package)?
}
};
let commitment = seed.genesis().commitment();
if seed.genesis().setup_id() != setup {
return Err(error(ProtectedLocalKeyStoreErrorKind::SetupMismatch));
}
if pin.is_some_and(|pin| pin != commitment) {
return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
}
let mut expected = SEED_MARKER.to_vec();
expected.extend(commitment);
match marker {
Some(marker) if marker != expected => {
return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
}
Some(_) => {}
None => self.write_record(SEED_MARKER_RECORD, &expected)?,
}
Ok(seed)
}
fn encode_seed(&self, seed: &SeedAuthority) -> StoreResult<Zeroizing<Vec<u8>>> {
let mut out = Zeroizing::new(Vec::with_capacity(SEED_BYTES));
out.extend_from_slice(SEED_MAGIC);
out.extend_from_slice(
&hex::decode(&self.account)
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WrongDatabase))?,
);
out.extend_from_slice(&seed.protected_storage_bytes());
let checksum = Sha256::digest(out.as_slice());
out.extend_from_slice(&checksum);
Ok(out)
}
pub(super) fn decode_seed(
&self,
bytes: &[u8],
package: &ProtectedLocalPackageKey,
) -> StoreResult<SeedAuthority> {
let account = hex::decode(&self.account)
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WrongDatabase))?;
if bytes.len() != SEED_BYTES
|| &bytes[..8] != SEED_MAGIC
|| bytes[8..40] != account
|| Sha256::digest(&bytes[..SEED_BYTES - 32]).as_slice() != &bytes[SEED_BYTES - 32..]
{
return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
}
SeedAuthority::from_protected_storage(
&bytes[40..SEED_BYTES - 32],
package.context(),
package.package_key(),
)
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Corrupt))
}
}
#[cfg(test)]
mod tests;