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;
14
15impl ProtectedLocalKeyStore {
16 pub(crate) async fn bind_seed_origin(
19 &self,
20 database: &Database,
21 server: &str,
22 ) -> anyhow::Result<()> {
23 self.validate_database(database).await?;
24 let origin = crate::sync::client::server_origin(server)?;
25 let established = database.local_seed_genesis_commitment().await?.is_some()
26 || database.seed_source_pin().await?.is_some()
27 || database.seed_publication_intent_bytes().await?.is_some();
28 self.prepare()?;
29 let _guard = self.lock()?;
30 if let Some(saved) = self.read_owned(SEED_ORIGIN_ITEM, SEED_ORIGIN_BYTES, false)? {
31 anyhow::ensure!(
32 saved.as_slice() == origin.as_bytes(),
33 "error sync-setup-server-mismatch hint=\"resume with an invitation from the original server; to abandon setup, run `aven sync reset --force`\""
34 );
35 return Ok(());
36 }
37 anyhow::ensure!(
38 !established && !self.seed_authority_exists()?,
39 "error sync-setup-server-missing hint=\"the saved setup server is missing; abandon setup with `aven sync reset --force` before starting again\""
40 );
41 self.write_owned(SEED_ORIGIN_ITEM, SEED_ORIGIN_BYTES, origin.as_bytes())
42 }
43
44 pub async fn prepare_seed_claim(
48 &self,
49 database: &Database,
50 setup_id: [u8; 32],
51 ) -> anyhow::Result<SeedAuthority> {
52 self.validate_database(database).await?;
53 anyhow::ensure!(
54 database
55 .enrollment_pin()
56 .await?
57 .is_none_or(|(_, _, role)| role == "inviter"),
58 "error enrollment-peer-cannot-become-seed"
59 );
60 let mut pin = database.local_seed_genesis_commitment().await?;
61 if let Some(commitment) = pin
62 && self.clear_unfenced_orphan_pin(database, commitment).await?
63 {
64 pin = None;
65 }
66 let package = if pin.is_some()
67 || database
68 .has_local_shared_state_package_never_dispatched()
69 .await?
70 {
71 self.load_required()?
72 } else {
73 self.load_or_create()?
74 };
75 let seed = match (self.load_or_create_seed(&package, setup_id, pin), pin) {
76 (Err(error), Some(commitment))
79 if error.kind() == ProtectedLocalKeyStoreErrorKind::SetupMismatch
80 && database.local_seed_claim_refused().await? =>
81 {
82 self.rollback_seed_commitment(database, commitment).await?;
83 self.load_or_create_seed(&package, setup_id, None)?
84 }
85 (result, _) => result?,
86 };
87 database.pin_local_seed_genesis(seed.genesis()).await?;
88 Ok(seed)
89 }
90
91 pub async fn package_seed_capture(
95 &self,
96 database: &Database,
97 blob_dir: &Path,
98 setup_id: [u8; 32],
99 ) -> anyhow::Result<EncryptedLocalSharedStatePackage> {
100 let seed = self.prepare_seed_claim(database, setup_id).await?;
101 self.package_local_capture(database, blob_dir, seed.genesis().commitment())
102 .await
103 }
104
105 pub(crate) async fn package_seed_capture_validated(
109 &self,
110 database: &Database,
111 blob_dir: &Path,
112 setup_id: [u8; 32],
113 capture: Option<NeverDispatchedLocalSharedCapture>,
114 ) -> anyhow::Result<crate::sync::shared_state::validated::ValidatedSeed> {
115 let seed = self.prepare_seed_claim(database, setup_id).await?;
116 self.package_local_capture_validated(
117 database,
118 blob_dir,
119 seed.genesis().commitment(),
120 capture,
121 )
122 .await
123 }
124
125 #[cfg(test)]
128 pub async fn rollback_seed_claim(
129 &self,
130 database: &Database,
131 seed: &SeedAuthority,
132 ) -> anyhow::Result<()> {
133 self.validate_database(database).await?;
134 self.rollback_seed_commitment(database, seed.genesis().commitment())
135 .await
136 }
137
138 async fn rollback_seed_commitment(
139 &self,
140 database: &Database,
141 commitment: [u8; 32],
142 ) -> anyhow::Result<()> {
143 anyhow::ensure!(
144 database.seed_source_pin().await?.is_none()
145 && database.seed_publication_intent_bytes().await?.is_none()
146 && !database
147 .has_local_shared_state_package_never_dispatched()
148 .await?,
149 "error seed-claim-rollback-fenced"
150 );
151 {
154 let _guard = self.lock()?;
155 let package = self.load_required_locked()?;
156 if let Some(bytes) = self.load_secret(SEED_ITEM, SEED_BYTES)? {
157 let stored = self.decode_seed(&bytes, &package)?;
158 if stored.genesis().commitment() != commitment {
159 anyhow::bail!("error seed-claim-rollback-conflict");
160 }
161 }
162 self.delete_secret(SEED_ITEM)?;
163 self.remove_record(SEED_MARKER_RECORD)?;
164 }
165 database.rollback_local_seed_genesis(commitment).await
166 }
167
168 async fn clear_unfenced_orphan_pin(
172 &self,
173 database: &Database,
174 commitment: [u8; 32],
175 ) -> anyhow::Result<bool> {
176 if database.seed_source_pin().await?.is_some()
177 || database.seed_publication_intent_bytes().await?.is_some()
178 || database
179 .has_local_shared_state_package_never_dispatched()
180 .await?
181 {
182 return Ok(false);
183 }
184 {
185 let _guard = self.lock()?;
186 if self.load_secret(SEED_ITEM, SEED_BYTES)?.is_some() {
187 return Ok(false);
188 }
189 self.remove_record(SEED_MARKER_RECORD)?;
190 }
191 database.rollback_local_seed_genesis(commitment).await?;
192 Ok(true)
193 }
194
195 pub(super) fn seed_authority_exists(&self) -> StoreResult<bool> {
196 Ok(self
197 .read_record(SEED_MARKER_RECORD, SEED_MARKER_BYTES)?
198 .is_some()
199 || self.load_secret(SEED_ITEM, SEED_BYTES)?.is_some())
200 }
201
202 fn load_or_create_seed(
203 &self,
204 package: &ProtectedLocalPackageKey,
205 setup: [u8; 32],
206 pin: Option<[u8; 32]>,
207 ) -> StoreResult<SeedAuthority> {
208 self.prepare()?;
209 let _guard = self.lock()?;
210 let marker = self.read_record(SEED_MARKER_RECORD, SEED_MARKER_BYTES)?;
211 let seed = match self.load_secret(SEED_ITEM, SEED_BYTES)? {
212 Some(bytes) => self.decode_seed(&bytes, package)?,
213 None if marker.is_some() || pin.is_some() => {
214 return Err(error(ProtectedLocalKeyStoreErrorKind::MissingAuthority));
215 }
216 None => {
217 let seed = SeedAuthority::generate(package.context(), package.package_key(), setup)
218 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
219 let encoded = self.encode_seed(&seed)?;
220 self.create_secret(SEED_ITEM, &encoded)?;
221 let saved = self
222 .load_secret(SEED_ITEM, SEED_BYTES)?
223 .ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
224 if saved.as_slice() != encoded.as_slice() {
225 return Err(error(ProtectedLocalKeyStoreErrorKind::WriteFailed));
226 }
227 self.decode_seed(&saved, package)?
228 }
229 };
230 let commitment = seed.genesis().commitment();
231 if seed.genesis().setup_id() != setup {
232 return Err(error(ProtectedLocalKeyStoreErrorKind::SetupMismatch));
233 }
234 if pin.is_some_and(|pin| pin != commitment) {
235 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
236 }
237 let mut expected = SEED_MARKER.to_vec();
238 expected.extend(commitment);
239 match marker {
240 Some(marker) if marker != expected => {
241 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
242 }
243 Some(_) => {}
244 None => self.write_record(SEED_MARKER_RECORD, &expected)?,
245 }
246 Ok(seed)
247 }
248
249 fn encode_seed(&self, seed: &SeedAuthority) -> StoreResult<Zeroizing<Vec<u8>>> {
250 let mut out = Zeroizing::new(Vec::with_capacity(SEED_BYTES));
251 out.extend_from_slice(SEED_MAGIC);
252 out.extend_from_slice(
253 &hex::decode(&self.account)
254 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WrongDatabase))?,
255 );
256 out.extend_from_slice(&seed.protected_storage_bytes());
257 let checksum = Sha256::digest(out.as_slice());
258 out.extend_from_slice(&checksum);
259 Ok(out)
260 }
261
262 pub(super) fn decode_seed(
263 &self,
264 bytes: &[u8],
265 package: &ProtectedLocalPackageKey,
266 ) -> StoreResult<SeedAuthority> {
267 let account = hex::decode(&self.account)
268 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WrongDatabase))?;
269 if bytes.len() != SEED_BYTES
270 || &bytes[..8] != SEED_MAGIC
271 || bytes[8..40] != account
272 || Sha256::digest(&bytes[..SEED_BYTES - 32]).as_slice() != &bytes[SEED_BYTES - 32..]
273 {
274 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
275 }
276 SeedAuthority::from_protected_storage(
277 &bytes[40..SEED_BYTES - 32],
278 package.context(),
279 package.package_key(),
280 )
281 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Corrupt))
282 }
283}
284
285#[cfg(test)]
286mod tests;