aven_core/sync/client/keys/
seed.rs1use super::*;
2use crate::sync::seed_claim::{SEED_STORAGE_BYTES, SeedAuthority};
3use std::path::Path;
4
5const SEED_MAGIC: &[u8; 8] = b"AVENSED1";
6const SEED_MARKER: &[u8; 8] = b"AVENSPN1";
7pub(super) const SEED_BYTES: usize = 8 + 32 + SEED_STORAGE_BYTES + 32;
8const SEED_MARKER_BYTES: usize = 8 + 32;
9pub(super) const SEED_ITEM: &str = "seed";
10const SEED_MARKER_RECORD: &str = "seed-authority";
12const SEED_ORIGIN_ITEM: &str = "seed-origin";
13const SEED_ORIGIN_BYTES: usize = 44 + crate::sync::client::MAX_SERVER_BYTES;
14const 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\"";
15
16impl ProtectedLocalKeyStore {
17 pub(crate) async fn bind_seed_origin(
20 &self,
21 database: &Database,
22 server: &str,
23 ) -> anyhow::Result<()> {
24 self.validate_database(database).await?;
25 let origin = crate::sync::client::server_origin(server)?;
26 let established = database.local_seed_genesis_commitment().await?.is_some()
27 || database.seed_source_pin().await?.is_some()
28 || database.seed_publication_intent_bytes().await?.is_some();
29 self.prepare()?;
30 let _guard = self.lock()?;
31 if let Some(saved) = self.read_owned(SEED_ORIGIN_ITEM, SEED_ORIGIN_BYTES, false)? {
32 anyhow::ensure!(
33 saved.as_slice() == origin.as_bytes(),
34 "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\""
35 );
36 return Ok(());
37 }
38 anyhow::ensure!(
39 !established && !self.seed_authority_exists()?,
40 SEED_ORIGIN_MISSING
41 );
42 self.write_owned(SEED_ORIGIN_ITEM, SEED_ORIGIN_BYTES, origin.as_bytes())
43 }
44
45 pub(crate) async fn retained_seed_setup(
49 &self,
50 database: &Database,
51 ) -> anyhow::Result<(String, [u8; 32])> {
52 self.validate_database(database).await?;
53 self.prepare()?;
54 let _guard = self.lock()?;
55 let package = self.load_required_locked()?;
56 let origin = self
57 .read_owned(SEED_ORIGIN_ITEM, SEED_ORIGIN_BYTES, false)?
58 .ok_or_else(|| anyhow::anyhow!(SEED_ORIGIN_MISSING))?;
59 let seed = self
60 .load_secret(SEED_ITEM, SEED_BYTES)?
61 .ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::MissingAuthority))?;
62 let seed = self.decode_seed(&seed, &package)?;
63 Ok((
64 String::from_utf8(origin.to_vec())?,
65 seed.genesis().setup_id(),
66 ))
67 }
68
69 pub async fn prepare_seed_claim(
73 &self,
74 database: &Database,
75 setup_id: [u8; 32],
76 ) -> anyhow::Result<SeedAuthority> {
77 self.validate_database(database).await?;
78 anyhow::ensure!(
79 database
80 .enrollment_pin()
81 .await?
82 .is_none_or(|(_, _, role)| role == "inviter"),
83 "error enrollment-peer-cannot-become-seed"
84 );
85 let mut pin = database.local_seed_genesis_commitment().await?;
86 if let Some(commitment) = pin
87 && self.clear_unfenced_orphan_pin(database, commitment).await?
88 {
89 pin = None;
90 }
91 let package = if pin.is_some()
92 || database
93 .has_local_shared_state_package_never_dispatched()
94 .await?
95 {
96 self.load_required()?
97 } else {
98 self.load_or_create()?
99 };
100 let seed = match (self.load_or_create_seed(&package, setup_id, pin), pin) {
101 (Err(error), Some(commitment))
104 if error.kind() == ProtectedLocalKeyStoreErrorKind::SetupMismatch
105 && database.local_seed_claim_refused().await? =>
106 {
107 self.rollback_seed_commitment(database, commitment).await?;
108 self.load_or_create_seed(&package, setup_id, None)?
109 }
110 (result, _) => result?,
111 };
112 database.pin_local_seed_genesis(seed.genesis()).await?;
113 Ok(seed)
114 }
115
116 pub async fn package_seed_capture(
120 &self,
121 database: &Database,
122 blob_dir: &Path,
123 setup_id: [u8; 32],
124 ) -> anyhow::Result<EncryptedLocalSharedStatePackage> {
125 let seed = self.prepare_seed_claim(database, setup_id).await?;
126 self.package_local_capture(database, blob_dir, seed.genesis().commitment())
127 .await
128 }
129
130 pub(crate) async fn package_seed_capture_validated(
134 &self,
135 database: &Database,
136 blob_dir: &Path,
137 setup_id: [u8; 32],
138 capture: Option<NeverDispatchedLocalSharedCapture>,
139 ) -> anyhow::Result<crate::sync::shared_state::validated::ValidatedSeed> {
140 let seed = self.prepare_seed_claim(database, setup_id).await?;
141 self.package_local_capture_validated(
142 database,
143 blob_dir,
144 seed.genesis().commitment(),
145 capture,
146 )
147 .await
148 }
149
150 #[cfg(test)]
153 pub async fn rollback_seed_claim(
154 &self,
155 database: &Database,
156 seed: &SeedAuthority,
157 ) -> anyhow::Result<()> {
158 self.validate_database(database).await?;
159 self.rollback_seed_commitment(database, seed.genesis().commitment())
160 .await
161 }
162
163 async fn rollback_seed_commitment(
164 &self,
165 database: &Database,
166 commitment: [u8; 32],
167 ) -> anyhow::Result<()> {
168 anyhow::ensure!(
169 database.seed_source_pin().await?.is_none()
170 && database.seed_publication_intent_bytes().await?.is_none()
171 && !database
172 .has_local_shared_state_package_never_dispatched()
173 .await?,
174 "error seed-claim-rollback-fenced"
175 );
176 {
179 let _guard = self.lock()?;
180 let package = self.load_required_locked()?;
181 if let Some(bytes) = self.load_secret(SEED_ITEM, SEED_BYTES)? {
182 let stored = self.decode_seed(&bytes, &package)?;
183 if stored.genesis().commitment() != commitment {
184 anyhow::bail!("error seed-claim-rollback-conflict");
185 }
186 }
187 self.delete_secret(SEED_ITEM)?;
188 self.remove_record(SEED_MARKER_RECORD)?;
189 }
190 database.rollback_local_seed_genesis(commitment).await
191 }
192
193 async fn clear_unfenced_orphan_pin(
197 &self,
198 database: &Database,
199 commitment: [u8; 32],
200 ) -> anyhow::Result<bool> {
201 if database.seed_source_pin().await?.is_some()
202 || database.seed_publication_intent_bytes().await?.is_some()
203 || database
204 .has_local_shared_state_package_never_dispatched()
205 .await?
206 {
207 return Ok(false);
208 }
209 {
210 let _guard = self.lock()?;
211 if self.load_secret(SEED_ITEM, SEED_BYTES)?.is_some() {
212 return Ok(false);
213 }
214 self.remove_record(SEED_MARKER_RECORD)?;
215 }
216 database.rollback_local_seed_genesis(commitment).await?;
217 Ok(true)
218 }
219
220 pub(super) fn seed_authority_exists(&self) -> StoreResult<bool> {
221 Ok(self
222 .read_record(SEED_MARKER_RECORD, SEED_MARKER_BYTES)?
223 .is_some()
224 || self.load_secret(SEED_ITEM, SEED_BYTES)?.is_some())
225 }
226
227 fn load_or_create_seed(
228 &self,
229 package: &ProtectedLocalPackageKey,
230 setup: [u8; 32],
231 pin: Option<[u8; 32]>,
232 ) -> StoreResult<SeedAuthority> {
233 self.prepare()?;
234 let _guard = self.lock()?;
235 let marker = self.read_record(SEED_MARKER_RECORD, SEED_MARKER_BYTES)?;
236 let seed = match self.load_secret(SEED_ITEM, SEED_BYTES)? {
237 Some(bytes) => self.decode_seed(&bytes, package)?,
238 None if marker.is_some() || pin.is_some() => {
239 return Err(error(ProtectedLocalKeyStoreErrorKind::MissingAuthority));
240 }
241 None => {
242 let seed = SeedAuthority::generate(package.context(), package.package_key(), setup)
243 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
244 let encoded = self.encode_seed(&seed)?;
245 self.create_secret(SEED_ITEM, &encoded)?;
246 let saved = self
247 .load_secret(SEED_ITEM, SEED_BYTES)?
248 .ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
249 if saved.as_slice() != encoded.as_slice() {
250 return Err(error(ProtectedLocalKeyStoreErrorKind::WriteFailed));
251 }
252 self.decode_seed(&saved, package)?
253 }
254 };
255 let commitment = seed.genesis().commitment();
256 if seed.genesis().setup_id() != setup {
257 return Err(error(ProtectedLocalKeyStoreErrorKind::SetupMismatch));
258 }
259 if pin.is_some_and(|pin| pin != commitment) {
260 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
261 }
262 let mut expected = SEED_MARKER.to_vec();
263 expected.extend(commitment);
264 match marker {
265 Some(marker) if marker != expected => {
266 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
267 }
268 Some(_) => {}
269 None => self.write_record(SEED_MARKER_RECORD, &expected)?,
270 }
271 Ok(seed)
272 }
273
274 fn encode_seed(&self, seed: &SeedAuthority) -> StoreResult<Zeroizing<Vec<u8>>> {
275 let mut out = Zeroizing::new(Vec::with_capacity(SEED_BYTES));
276 out.extend_from_slice(SEED_MAGIC);
277 out.extend_from_slice(
278 &hex::decode(&self.account)
279 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WrongDatabase))?,
280 );
281 out.extend_from_slice(&seed.protected_storage_bytes());
282 let checksum = Sha256::digest(out.as_slice());
283 out.extend_from_slice(&checksum);
284 Ok(out)
285 }
286
287 pub(super) fn decode_seed(
288 &self,
289 bytes: &[u8],
290 package: &ProtectedLocalPackageKey,
291 ) -> StoreResult<SeedAuthority> {
292 let account = hex::decode(&self.account)
293 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WrongDatabase))?;
294 if bytes.len() != SEED_BYTES
295 || &bytes[..8] != SEED_MAGIC
296 || bytes[8..40] != account
297 || Sha256::digest(&bytes[..SEED_BYTES - 32]).as_slice() != &bytes[SEED_BYTES - 32..]
298 {
299 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
300 }
301 SeedAuthority::from_protected_storage(
302 &bytes[40..SEED_BYTES - 32],
303 package.context(),
304 package.package_key(),
305 )
306 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Corrupt))
307 }
308}
309
310#[cfg(test)]
311mod tests;