Skip to main content

aven_core/sync/seed_claim/
persistence.rs

1use 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    /// Admits one immutable sequence-zero claim under operator setup authority.
29    /// The storage's unexpired issued verifier is read in this claim
30    /// transaction, so a concurrent reissue either precedes the claim or finds
31    /// the storage claimed.
32    /// Exact authenticated retries return the original result without reissuance.
33    /// This does not authorize uploads, READY, or ordinary plaintext sync.
34    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        // Only the exact secret of the expired verifier learns that it
74        // expired, which tells a guesser nothing a live verifier wouldn't.
75        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    /// Whether encrypted server storage already has its immutable seed claim.
116    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    /// Nonsecret loss-detection pin. This is never a substitute for host authority.
128    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    /// Hosts call this only after protected storage has committed exact authority.
144    /// An old or replaced database may acquire a pin but cannot change one.
145    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    /// Records that a server refused this unfenced provisional claim. Refusals
165    /// are unauthenticated, so the seed authority stays for an exact retry;
166    /// hosts present the database as not set up until the next claim attempt.
167    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    /// Removes an abandoned provisional seed pin and its recorded refusal.
186    /// Once source authority or publication state exists, the installation is
187    /// fenced and this operation refuses to undo it.
188    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    /// Issues operator setup authority for `secret` on encrypted server
221    /// storage and returns its setup ID. An existing ID is kept, even after
222    /// expiry, so a device whose genesis already binds it can claim with the
223    /// reissued secret; the replaced verifier refuses earlier secrets. Claimed
224    /// storage and storage holding local domain data cannot take a new verifier.
225    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    /// The configured setup verifier while it is unexpired at `now` (Unix seconds).
253    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    /// True once storage was prepared for, or claimed by, an encrypted vault.
262    /// Refuses marked server storage containing local domain data.
263    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    /// True when storage holds change rows. Encrypted server storage never
276    /// does, so server launch refuses such storage as unsupported.
277    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}