aven_core/sync/seed_claim/
persistence.rs1use anyhow::{Result, ensure};
2use subtle::ConstantTimeEq;
3
4use super::{
5 ClaimAuthentication, ClaimRefusal, ClaimResult, Genesis, SetupAuthority, codec,
6 credential_verifier,
7};
8use crate::db::{self, Database, begin_immediate};
9
10const SERVER_SETUP_KEY: &str = "e2ee_server_setup";
11const SEED_CLAIM_REFUSED: &str = "e2ee_seed_claim_refused";
12
13async fn ensure_empty_server_domain(conn: &mut sqlx::SqliteConnection) -> Result<()> {
14 crate::sync::shared_state::ensure_empty_domain(conn)
15 .await
16 .map_err(|error| error.context(super::StorageNotEmpty))?;
17 let customized_workspace: bool = sqlx::query_scalar(
18 "SELECT EXISTS(SELECT 1 FROM workspaces
19 WHERE name != 'default' OR key != 'default' OR archived != 0)",
20 )
21 .fetch_one(conn)
22 .await?;
23 ensure!(!customized_workspace, super::StorageNotEmpty);
24 Ok(())
25}
26
27impl Database {
28 pub async fn admit_seed_claim(
35 &self,
36 request: &[u8],
37 authentication: ClaimAuthentication<'_>,
38 ) -> Result<ClaimResult> {
39 let now = crate::sync::client::engine::unix_now()?;
40 self.admit_seed_claim_at(request, authentication, now).await
41 }
42
43 pub(in crate::sync::seed_claim) async fn admit_seed_claim_at(
44 &self,
45 request: &[u8],
46 authentication: ClaimAuthentication<'_>,
47 now: u64,
48 ) -> Result<ClaimResult> {
49 let incoming = codec::claim_record(request)?;
50 let mut conn = self.acquire_writer().await?;
51 let mut tx = begin_immediate(&mut conn).await?;
52 let (issued, expired) = match db::get_meta(&mut tx, SERVER_SETUP_KEY).await? {
53 Some(value) => {
54 let (setup, expires_at) = parse_server_setup(&value)?;
55 if now < expires_at {
56 (Some(setup), None)
57 } else {
58 (None, Some(setup))
59 }
60 }
61 None => (None, None),
62 };
63 let genesis_only: Option<bool> =
64 sqlx::query_scalar("SELECT genesis_only FROM server_seed_claim WHERE singleton = 1")
65 .fetch_optional(&mut *tx)
66 .await?;
67 ensure!(genesis_only != Some(false), ClaimRefusal::Retired);
68 let stored: Option<Vec<u8>> =
69 sqlx::query_scalar("SELECT genesis FROM server_seed_claim WHERE singleton = 1")
70 .fetch_optional(&mut *tx)
71 .await?;
72 let genesis = Genesis::from_record(stored.as_deref().unwrap_or(incoming))?;
73 let mut expired_secret = false;
76 let authorized = match authentication {
77 ClaimAuthentication::SetupSecret(secret) => {
78 expired_secret = expired
79 .as_ref()
80 .is_some_and(|setup| setup.authorizes(genesis.setup, secret));
81 issued
82 .as_ref()
83 .is_some_and(|setup| setup.authorizes(genesis.setup, secret))
84 }
85 ClaimAuthentication::SeedBearer(token) => {
86 stored.is_some()
87 && bool::from(
88 credential_verifier(genesis.context.vault_id, genesis.device, token)
89 .ct_eq(&genesis.verifier),
90 )
91 }
92 };
93 ensure!(
94 authorized || !expired_secret || stored.is_some(),
95 ClaimRefusal::Expired
96 );
97 ensure!(
98 authorized,
99 ClaimRefusal::Unauthorized {
100 claimed: stored.is_some()
101 }
102 );
103 if let Some(stored) = stored {
104 ensure!(stored == incoming, ClaimRefusal::Conflict);
105 } else {
106 sqlx::query("INSERT INTO server_seed_claim(singleton, genesis) VALUES (1, ?)")
107 .bind(genesis.record().as_slice())
108 .execute(&mut *tx)
109 .await?;
110 }
111 tx.commit().await?;
112 Ok(ClaimResult::from_genesis(&genesis))
113 }
114
115 pub async fn e2ee_server_is_claimed(&self) -> Result<bool> {
117 let mut conn = self.acquire_reader().await?;
118 Ok(
119 sqlx::query_scalar(
120 "SELECT EXISTS(SELECT 1 FROM server_seed_claim WHERE singleton = 1)",
121 )
122 .fetch_one(&mut *conn)
123 .await?,
124 )
125 }
126
127 pub async fn local_seed_genesis_commitment(&self) -> Result<Option<[u8; 32]>> {
129 let mut conn = self.acquire_reader().await?;
130 let value: Option<Vec<u8>> =
131 sqlx::query_scalar("SELECT commitment FROM local_seed_genesis_pin WHERE singleton = 1")
132 .fetch_optional(&mut *conn)
133 .await?;
134 value
135 .map(|bytes| {
136 bytes
137 .try_into()
138 .map_err(|_| anyhow::anyhow!("error seed-claim-local-pin-corrupt"))
139 })
140 .transpose()
141 }
142
143 pub async fn pin_local_seed_genesis(&self, genesis: &Genesis) -> Result<()> {
146 let mut conn = self.acquire_writer().await?;
147 let mut tx = begin_immediate(&mut conn).await?;
148 let commitment = genesis.commitment();
149 sqlx::query(
150 "INSERT OR IGNORE INTO local_seed_genesis_pin(singleton, commitment) VALUES (1, ?)",
151 )
152 .bind(commitment.as_slice())
153 .execute(&mut *tx)
154 .await?;
155 let stored: Vec<u8> =
156 sqlx::query_scalar("SELECT commitment FROM local_seed_genesis_pin WHERE singleton = 1")
157 .fetch_one(&mut *tx)
158 .await?;
159 ensure!(stored == commitment, "error seed-claim-local-pin-conflict");
160 tx.commit().await?;
161 Ok(())
162 }
163
164 pub async fn mark_local_seed_claim_refused(&self, reason: &str) -> Result<()> {
168 let mut conn = self.acquire_writer().await?;
169 crate::db::set_meta(&mut conn, SEED_CLAIM_REFUSED, reason).await
170 }
171
172 pub async fn clear_local_seed_claim_refused(&self) -> Result<()> {
173 let mut conn = self.acquire_writer().await?;
174 sqlx::query("DELETE FROM meta WHERE key = ?")
175 .bind(SEED_CLAIM_REFUSED)
176 .execute(&mut *conn)
177 .await?;
178 Ok(())
179 }
180
181 pub async fn local_seed_claim_refused(&self) -> Result<bool> {
182 Ok(self.meta(SEED_CLAIM_REFUSED).await?.is_some())
183 }
184
185 pub async fn rollback_local_seed_genesis(&self, expected: [u8; 32]) -> Result<()> {
189 let mut conn = self.acquire_writer().await?;
190 let mut tx = begin_immediate(&mut conn).await?;
191 let fenced: bool = sqlx::query_scalar(
192 "SELECT EXISTS(SELECT 1 FROM local_seed_source)
193 OR EXISTS(SELECT 1 FROM local_seed_publication_intent)
194 OR EXISTS(SELECT 1 FROM local_shared_capture_journal)",
195 )
196 .fetch_one(&mut *tx)
197 .await?;
198 ensure!(!fenced, "error seed-claim-rollback-fenced");
199 let stored: Option<Vec<u8>> =
200 sqlx::query_scalar("SELECT commitment FROM local_seed_genesis_pin WHERE singleton = 1")
201 .fetch_optional(&mut *tx)
202 .await?;
203 ensure!(
204 stored
205 .as_deref()
206 .is_none_or(|value| value == expected.as_slice()),
207 "error seed-claim-rollback-conflict"
208 );
209 sqlx::query("DELETE FROM local_seed_genesis_pin WHERE singleton = 1")
210 .execute(&mut *tx)
211 .await?;
212 sqlx::query("DELETE FROM meta WHERE key = ?")
213 .bind(SEED_CLAIM_REFUSED)
214 .execute(&mut *tx)
215 .await?;
216 tx.commit().await?;
217 Ok(())
218 }
219
220 pub async fn issue_e2ee_server_setup(
226 &self,
227 secret: &super::Secret,
228 fresh_id: [u8; 32],
229 expires_at: u64,
230 ) -> Result<[u8; 32]> {
231 let mut conn = self.acquire_writer().await?;
232 let mut tx = begin_immediate(&mut conn).await?;
233 let claimed: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM server_seed_claim)")
234 .fetch_one(&mut *tx)
235 .await?;
236 ensure!(!claimed, "error e2ee-server-already-claimed");
237 ensure_empty_server_domain(&mut tx).await?;
238 let id = match db::get_meta(&mut tx, SERVER_SETUP_KEY).await? {
239 Some(value) => parse_server_setup(&value)?.0.id,
240 None => fresh_id,
241 };
242 let value = format!(
243 "{}:{}:{expires_at}",
244 hex::encode(id),
245 hex::encode(SetupAuthority::verifier(id, secret))
246 );
247 db::set_meta(&mut tx, SERVER_SETUP_KEY, &value).await?;
248 tx.commit().await?;
249 Ok(id)
250 }
251
252 pub async fn e2ee_server_setup(&self, now: u64) -> Result<Option<SetupAuthority>> {
254 let Some(value) = self.meta(SERVER_SETUP_KEY).await? else {
255 return Ok(None);
256 };
257 let (setup, expires_at) = parse_server_setup(&value)?;
258 Ok((now < expires_at).then_some(setup))
259 }
260
261 pub async fn is_e2ee_server_storage(&self) -> Result<bool> {
264 let mut conn = self.acquire_reader().await?;
265 let prepared = db::get_meta(&mut conn, SERVER_SETUP_KEY).await?.is_some();
266 let claimed: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM server_seed_claim)")
267 .fetch_one(&mut *conn)
268 .await?;
269 if prepared || claimed {
270 ensure_empty_server_domain(&mut conn).await?;
271 }
272 Ok(prepared || claimed)
273 }
274
275 pub async fn has_change_history(&self) -> Result<bool> {
278 let mut conn = self.acquire_reader().await?;
279 Ok(sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM changes)")
280 .fetch_one(&mut *conn)
281 .await?)
282 }
283}
284
285fn parse_server_setup(value: &str) -> Result<(SetupAuthority, u64)> {
286 let corrupt = || anyhow::anyhow!("error e2ee-server-setup-corrupt");
287 let mut parts = value.split(':');
288 let (Some(id), Some(verifier), Some(expires_at), None) =
289 (parts.next(), parts.next(), parts.next(), parts.next())
290 else {
291 return Err(corrupt());
292 };
293 let decode = |text: &str| -> Result<[u8; 32]> {
294 hex::decode(text)
295 .ok()
296 .and_then(|bytes| bytes.try_into().ok())
297 .ok_or_else(corrupt)
298 };
299 let expires_at = expires_at.parse().map_err(|_| corrupt())?;
300 Ok((
301 SetupAuthority::from_verifier(decode(id)?, decode(verifier)?),
302 expires_at,
303 ))
304}