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