aven_core/sync/client/keys/
adoption.rs1use super::*;
3use crate::db::installation::InstallationGuard;
4use crate::sync::seed_claim::{PublicationOutcome, SeedAuthority};
5use crate::sync::shared_state::validated::ProofCache;
6use crate::sync::{SeedPublicationIntent, SeedSourceAuthority};
7use anyhow::Context;
8
9impl ProtectedLocalKeyStore {
10 pub async fn seed_http_inputs(
12 &self,
13 database: &Database,
14 ) -> anyhow::Result<(
15 SeedAuthority,
16 SeedPublicationIntent,
17 Option<crate::sync::bootstrap_format::Package>,
18 )> {
19 let mut proofs = ProofCache::default();
20 let (seed, intent) = self.seed_resume_intent(database, &mut proofs).await?;
21 let upload = match self.seed_upload(database, &intent, &mut proofs).await? {
22 Some(upload) => Some(upload.package(database).await?),
23 None => None,
24 };
25 Ok((seed, intent, upload))
26 }
27
28 pub(crate) async fn seed_resume_intent(
30 &self,
31 database: &Database,
32 proofs: &mut ProofCache,
33 ) -> anyhow::Result<(SeedAuthority, SeedPublicationIntent)> {
34 let intent = self
35 .prepare_seed_adoption_intent_with(database, proofs)
36 .await?;
37 let package = self.load_required()?;
38 let seed = {
39 let _guard = self.lock()?;
40 self.required_seed(&package)?
41 };
42 Ok((seed, intent))
43 }
44
45 pub(crate) async fn seed_upload(
47 &self,
48 database: &Database,
49 intent: &SeedPublicationIntent,
50 proofs: &mut ProofCache,
51 ) -> anyhow::Result<Option<crate::sync::shared_state::package::upload::FrozenUpload>> {
52 let _installation = InstallationGuard::acquire(database.path())?;
53 self.validate_database(database).await?;
54 let package = self.load_required()?;
55 let _guard = self.lock()?;
56 let seed = self.required_seed(&package)?;
57 let source = self.decode_source(
58 &self
59 .load_adoption_record("source", 104, true)?
60 .context("missing source")?,
61 &seed,
62 )?;
63 let protected = self
64 .load_adoption_record("intent", 65536, true)?
65 .context("missing intent")?;
66 anyhow::ensure!(
67 protected == intent.protected_storage_bytes(),
68 "error seed-protected-intent-mismatch"
69 );
70 proofs.trust_protected(intent);
71 let _timer = super::super::bootstrap::StageTimer::start("load_upload");
72 database
73 .seed_publication_upload(&source, intent, &seed, package.package_key(), proofs)
74 .await
75 }
76
77 fn adoption_marker(kind: &str) -> String {
79 format!("{kind}-authority")
80 }
81
82 pub(super) fn load_adoption_record(
83 &self,
84 kind: &str,
85 limit: usize,
86 required: bool,
87 ) -> anyhow::Result<Option<Vec<u8>>> {
88 let marker = self.read_record(&Self::adoption_marker(kind), 32)?;
89 let bytes = self.load_secret(kind, limit)?;
90 match bytes {
91 Some(bytes) => {
92 let digest = Sha256::digest(&bytes);
93 anyhow::ensure!(
94 marker.as_deref().is_none_or(|m| m == digest.as_slice()),
95 "error seed-protected-record-corrupt"
96 );
97 if marker.is_none() {
98 self.write_record(&Self::adoption_marker(kind), &digest)?;
99 }
100 if kind == "intent" {
101 let length = u32::from_be_bytes(bytes[..4].try_into()?) as usize;
102 anyhow::ensure!(
103 length <= limit - 4 && bytes[4 + length..].iter().all(|b| *b == 0),
104 "error seed-protected-intent-framing"
105 );
106 return Ok(Some(bytes[4..4 + length].to_vec()));
107 }
108 Ok(Some(bytes.to_vec()))
109 }
110 None => {
111 anyhow::ensure!(
112 !required && marker.is_none(),
113 "error seed-protected-authority-missing"
114 );
115 Ok(None)
116 }
117 }
118 }
119
120 fn create_adoption_record(&self, kind: &str, bytes: &[u8], limit: usize) -> anyhow::Result<()> {
121 let encoded;
122 let storage = if kind == "intent" {
123 anyhow::ensure!(bytes.len() <= limit - 4, "error seed-intent-too-large");
124 let mut frame = vec![0; limit];
125 frame[..4].copy_from_slice(&(bytes.len() as u32).to_be_bytes());
126 frame[4..4 + bytes.len()].copy_from_slice(bytes);
127 encoded = frame;
128 encoded.as_slice()
129 } else {
130 bytes
131 };
132 self.create_secret(kind, storage)?;
133 let stored = self
134 .load_adoption_record(kind, limit, true)?
135 .context("error seed-protected-write-missing")?;
136 anyhow::ensure!(stored == bytes, "error seed-protected-write-mismatch");
137 Ok(())
138 }
139
140 pub fn required_seed(
141 &self,
142 package: &ProtectedLocalPackageKey,
143 ) -> anyhow::Result<SeedAuthority> {
144 let bytes = self
145 .load_secret(seed::SEED_ITEM, seed::SEED_BYTES)?
146 .context("error seed-protected-authority-missing")?;
147 Ok(self.decode_seed(&bytes, package)?)
148 }
149
150 pub(super) fn decode_source(
151 &self,
152 bytes: &[u8],
153 seed: &SeedAuthority,
154 ) -> anyhow::Result<SeedSourceAuthority> {
155 SeedSourceAuthority::from_protected_storage(
156 bytes,
157 hex::decode(&self.account)?
158 .try_into()
159 .map_err(|_| anyhow::anyhow!("invalid source account"))?,
160 seed.genesis(),
161 )
162 }
163
164 pub async fn prepare_seed_source(
167 &self,
168 database: &Database,
169 ) -> anyhow::Result<SeedSourceAuthority> {
170 let installation = InstallationGuard::acquire(database.path())?;
171 self.validate_database(database).await?;
172 #[cfg(test)]
173 wait_source_boundary(database.path()).await;
174 let was_unbound = installation.ensure_unbound().is_ok();
175 let package = self.load_required()?;
176 self.prepare()?;
177 let _guard = self.lock()?;
178 let seed = self.required_seed(&package)?;
179 let pin = database.seed_source_pin().await?;
180 let existing = self.load_adoption_record("source", 104, pin.is_some())?;
181 if existing.is_none() {
182 anyhow::ensure!(
183 was_unbound,
184 "error seed-source-incomplete installation-remains-fenced"
185 );
186 anyhow::ensure!(
187 database
188 .resume_local_shared_state_never_dispatched()
189 .await?
190 .is_none(),
191 "error seed-source-requires-recapture-never-dispatched"
192 );
193 }
194 installation.fence()?;
195 let source = match existing {
196 Some(bytes) => self.decode_source(&bytes, &seed)?,
197 None => {
198 let source = SeedSourceAuthority::generate(
199 hex::decode(&self.account)?
200 .try_into()
201 .map_err(|_| anyhow::anyhow!("invalid source account"))?,
202 seed.genesis(),
203 )?;
204 self.create_adoption_record("source", source.protected_storage_bytes(), 104)?;
205 source
206 }
207 };
208 if pin.is_none() {
209 anyhow::ensure!(
210 self.load_adoption_record("intent", 65536, false)?.is_none(),
211 "error seed-source-database-lost"
212 );
213 }
214 database.bind_seed_source(&source, &installation).await?;
215 Ok(source)
216 }
217
218 pub async fn prepare_seed_adoption_intent(
221 &self,
222 database: &Database,
223 ) -> anyhow::Result<SeedPublicationIntent> {
224 self.prepare_seed_adoption_intent_with(database, &mut ProofCache::default())
225 .await
226 }
227
228 pub(crate) async fn prepare_seed_adoption_intent_with(
229 &self,
230 database: &Database,
231 proofs: &mut ProofCache,
232 ) -> anyhow::Result<SeedPublicationIntent> {
233 let _installation = InstallationGuard::acquire(database.path())?;
234 self.validate_database(database).await?;
235 let package = self.load_required()?;
236 let _guard = self.lock()?;
237 let seed = self.required_seed(&package)?;
238 let source = self.decode_source(
239 &self
240 .load_adoption_record("source", 104, true)?
241 .context("missing source")?,
242 &seed,
243 )?;
244 let existing = database.seed_publication_intent_bytes().await?;
245 let protected = self.load_adoption_record(
246 "intent",
247 65536,
248 existing
249 .as_ref()
250 .is_some_and(|(_, state)| state != "preparing"),
251 )?;
252 anyhow::ensure!(
253 protected.is_none() || existing.is_some(),
254 "error seed-intent-database-lost"
255 );
256 if let Some(bytes) = &protected {
259 proofs.trust_protected(&SeedPublicationIntent::from_protected_storage(
260 bytes,
261 &source,
262 seed.genesis(),
263 )?);
264 }
265 let timer = super::super::bootstrap::StageTimer::start("prepare_intent");
266 let intent = database
267 .prepare_seed_publication_intent_with(&source, &seed, package.package_key(), proofs)
268 .await?;
269 match protected {
270 Some(bytes) => anyhow::ensure!(
271 bytes == intent.protected_storage_bytes(),
272 "error seed-protected-intent-mismatch"
273 ),
274 None => {
275 self.create_adoption_record("intent", intent.protected_storage_bytes(), 65536)?
276 }
277 }
278 let reloaded = SeedPublicationIntent::from_protected_storage(
279 &self
280 .load_adoption_record("intent", 65536, true)?
281 .context("missing intent")?,
282 &source,
283 seed.genesis(),
284 )?;
285 drop(timer);
286 let _timer = super::super::bootstrap::StageTimer::start("seal");
287 if existing.is_none_or(|(_, state)| state != "adopted") {
288 database
289 .seal_seed_publication_intent(&source, &reloaded)
290 .await?;
291 }
292 Ok(reloaded)
293 }
294
295 pub async fn adopt_seed_publication(
298 &self,
299 database: &Database,
300 outcome: &PublicationOutcome,
301 ) -> anyhow::Result<bool> {
302 self.adopt_seed_publication_with(database, outcome, &mut ProofCache::default())
303 .await
304 }
305
306 pub(crate) async fn adopt_seed_publication_with(
307 &self,
308 database: &Database,
309 outcome: &PublicationOutcome,
310 proofs: &mut ProofCache,
311 ) -> anyhow::Result<bool> {
312 let _installation = InstallationGuard::acquire(database.path())?;
313 self.validate_database(database).await?;
314 let package = self.load_required()?;
315 let _guard = self.lock()?;
316 let seed = self.required_seed(&package)?;
317 let source = self.decode_source(
318 &self
319 .load_adoption_record("source", 104, true)?
320 .context("missing source")?,
321 &seed,
322 )?;
323 let intent = SeedPublicationIntent::from_protected_storage(
324 &self
325 .load_adoption_record("intent", 65536, true)?
326 .context("missing intent")?,
327 &source,
328 seed.genesis(),
329 )?;
330 proofs.trust_protected(&intent);
331 let adopted = database
332 .adopt_seed_publication(
333 &source,
334 &intent,
335 &seed,
336 package.package_key(),
337 outcome,
338 proofs,
339 )
340 .await?;
341 database
342 .cleanup_adopted_seed_capture(&source, &intent)
343 .await?;
344 Ok(adopted)
345 }
346}
347
348#[cfg(test)]
349mod tests;
350
351#[cfg(test)]
352type SourceBarrier = (
353 std::path::PathBuf,
354 tokio::sync::oneshot::Sender<()>,
355 tokio::sync::oneshot::Receiver<()>,
356);
357#[cfg(test)]
358static SOURCE_BARRIER: std::sync::Mutex<Option<SourceBarrier>> = std::sync::Mutex::new(None);
359#[cfg(test)]
360async fn wait_source_boundary(path: &Path) {
361 let barrier = {
362 let mut slot = SOURCE_BARRIER.lock().unwrap();
363 if slot.as_ref().is_some_and(|(p, _, _)| p == path) {
364 slot.take()
365 } else {
366 None
367 }
368 };
369 if let Some((_, entered, resume)) = barrier {
370 let _ = entered.send(());
371 let _ = resume.await;
372 }
373}