Skip to main content

aven_core/sync/seed_claim/
persistence.rs

1use 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    /// Admits one immutable sequence-zero claim under operator setup authority.
25    /// The storage's unexpired issued verifier is read in this claim
26    /// transaction, so a concurrent reissue either precedes the claim or finds
27    /// the storage claimed.
28    /// Exact authenticated retries return the original result without reissuance.
29    /// This does not authorize uploads, READY, or ordinary plaintext sync.
30    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        // Only the exact secret of the expired verifier learns that it
70        // expired, which tells a guesser nothing a live verifier wouldn't.
71        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    /// Whether encrypted server storage already has its immutable seed claim.
108    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    /// Nonsecret loss-detection pin. This is never a substitute for host authority.
120    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    /// Hosts call this only after protected storage has committed exact authority.
136    /// An old or replaced database may acquire a pin but cannot change one.
137    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    /// Records that a server refused this unfenced provisional claim. Refusals
157    /// are unauthenticated, so the seed authority stays for an exact retry;
158    /// hosts present the database as not set up until the next claim attempt.
159    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    /// Removes an abandoned provisional seed pin and its recorded refusal.
178    /// Once source authority or publication state exists, the installation is
179    /// fenced and this operation refuses to undo it.
180    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    /// Issues operator setup authority for `secret` on encrypted server
213    /// storage and returns its setup ID. An existing ID is kept, even after
214    /// expiry, so a device whose genesis already binds it can claim with the
215    /// reissued secret; the replaced verifier refuses earlier secrets. Claimed
216    /// storage and storage holding local domain data cannot take a new verifier.
217    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    /// The configured setup verifier while it is unexpired at `now` (Unix seconds).
245    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    /// True once storage was prepared for, or claimed by, an encrypted vault.
254    /// Refuses marked server storage containing local domain data.
255    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    /// True when storage holds change rows. Encrypted server storage never
268    /// does, so server launch refuses such storage as unsupported.
269    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}