1use super::super::{Context, hash, valid};
2use super::{codec::Descriptor, *};
3#[cfg(test)]
4use crate::sync::encrypted_tail::attachments::codec::DescriptorAuthority;
5use crate::sync::seed_claim::membership::Membership;
6use crate::{
7 attachments::lifecycle::LifecyclePolicy,
8 db::{Database, begin_immediate},
9 sync::seed_claim::Secret,
10};
11use anyhow::{Context as _, Result, ensure};
12use sqlx::SqliteConnection;
13
14async fn load(
15 conn: &mut SqliteConnection,
16 object: &[u8; 32],
17 commitment: &[u8; 32],
18) -> Result<(Descriptor, bool)> {
19 let (bytes, complete): (Vec<u8>, bool) =
20 sqlx::query_as("SELECT descriptor,complete FROM server_e2ee_images WHERE object=?")
21 .bind(object.as_slice())
22 .fetch_optional(conn)
23 .await?
24 .context("error encrypted-image-unknown")?;
25 valid(hash(&bytes) == *commitment)?;
26 let d = Descriptor::decode(&bytes)?;
27 valid(d.object == *object)?;
28 Ok((d, complete))
29}
30async fn refresh(conn: &mut SqliteConnection, now: i64) -> Result<()> {
35 sqlx::query("UPDATE server_e2ee_images SET unreferenced_at=CASE WHEN EXISTS(SELECT 1 FROM server_e2ee_image_references r JOIN server_e2ee_image_parents p ON p.workspace=r.workspace AND p.parent=r.parent WHERE r.object=server_e2ee_images.object AND r.deleted=0 AND (p.deleted=0 OR p.protected=1 OR p.version IS NULL)) OR EXISTS(SELECT 1 FROM server_e2ee_image_tickets t WHERE t.object=server_e2ee_images.object AND t.expires_at>?) THEN NULL ELSE COALESCE(unreferenced_at,?) END").bind(now).bind(now).execute(conn).await?;
36 Ok(())
37}
38pub(in crate::sync::encrypted_tail) async fn retain_parent(
41 conn: &mut SqliteConnection,
42 workspace: &str,
43 parent: &str,
44) -> Result<()> {
45 sqlx::query("UPDATE server_e2ee_images SET unreferenced_at=NULL WHERE unreferenced_at IS NOT NULL AND object IN (SELECT r.object FROM server_e2ee_image_references r JOIN server_e2ee_image_parents p ON p.workspace=r.workspace AND p.parent=r.parent WHERE r.workspace=? AND r.parent=? AND r.deleted=0 AND (p.deleted=0 OR p.protected=1 OR p.version IS NULL))").bind(workspace).bind(parent).execute(conn).await?;
46 Ok(())
47}
48async fn protected(
49 conn: &mut SqliteConnection,
50 object: &[u8; 32],
51 workspace: &str,
52) -> Result<bool> {
53 Ok(sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM server_e2ee_image_references r JOIN server_e2ee_image_parents p ON p.workspace=r.workspace AND p.parent=r.parent WHERE r.object=? AND r.workspace=? AND r.deleted=0 AND (p.deleted=0 OR p.protected=1 OR p.version IS NULL))").bind(object.as_slice()).bind(workspace).fetch_one(conn).await?)
54}
55async fn scope(conn: &mut SqliteConnection, object: &[u8; 32], workspace: &str) -> Result<()> {
56 valid(
57 sqlx::query_scalar(
58 "SELECT EXISTS(SELECT 1 FROM server_e2ee_image_scopes WHERE object=? AND workspace=?)",
59 )
60 .bind(object.as_slice())
61 .bind(workspace)
62 .fetch_one(conn)
63 .await?,
64 )
65}
66async fn reserve(
67 conn: &mut SqliteConnection,
68 d: &Descriptor,
69 workspace: &str,
70 device: &[u8; 32],
71 quota: i64,
72 now: i64,
73) -> Result<()> {
74 sqlx::query("DELETE FROM server_e2ee_image_tickets WHERE expires_at<=?")
75 .bind(now)
76 .execute(&mut *conn)
77 .await?;
78 let existing: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM server_e2ee_image_tickets WHERE object=? AND workspace=? AND device=? AND expires_at>?)").bind(d.object.as_slice()).bind(workspace).bind(device.as_slice()).bind(now).fetch_one(&mut *conn).await?;
79 if existing {
80 return Ok(());
81 }
82 let reactivating: bool = sqlx::query_scalar("SELECT origin IS NULL AND NOT EXISTS(SELECT 1 FROM server_e2ee_image_chunks c WHERE c.object=i.object) AND NOT EXISTS(SELECT 1 FROM server_e2ee_image_tickets t WHERE t.object=i.object AND t.expires_at>?) FROM server_e2ee_images i WHERE object=?")
83 .bind(now).bind(d.object.as_slice()).fetch_one(&mut *conn).await?;
84 if reactivating {
85 let active: i64 = sqlx::query_scalar("SELECT count(*) FROM server_e2ee_images i WHERE origin IS NULL AND (EXISTS(SELECT 1 FROM server_e2ee_image_chunks c WHERE c.object=i.object) OR EXISTS(SELECT 1 FROM server_e2ee_image_tickets t WHERE t.object=i.object AND t.expires_at>?))")
86 .bind(now).fetch_one(&mut *conn).await?;
87 valid(active < 128)?;
88 }
89 let counted = protected(conn, &d.object, workspace).await? || sqlx::query_scalar::<_,bool>("SELECT EXISTS(SELECT 1 FROM server_e2ee_image_tickets WHERE object=? AND workspace=? AND expires_at>?)").bind(d.object.as_slice()).bind(workspace).bind(now).fetch_one(&mut *conn).await?;
90 if !counted {
91 let used: i64 = sqlx::query_scalar("SELECT COALESCE(SUM(i.byte_size),0) FROM server_e2ee_images i WHERE EXISTS(SELECT 1 FROM server_e2ee_image_references r JOIN server_e2ee_image_parents p ON p.workspace=r.workspace AND p.parent=r.parent WHERE r.object=i.object AND r.workspace=? AND r.deleted=0 AND (p.deleted=0 OR p.protected=1 OR p.version IS NULL)) OR EXISTS(SELECT 1 FROM server_e2ee_image_tickets t WHERE t.object=i.object AND t.workspace=? AND t.expires_at>?)").bind(workspace).bind(workspace).bind(now).fetch_one(&mut *conn).await?;
92 ensure!(
93 used.checked_add(d.byte_size()).is_some_and(|n| n <= quota),
94 "error attachment-quota-exceeded"
95 );
96 }
97 let count: i64 =
98 sqlx::query_scalar("SELECT count(*) FROM server_e2ee_image_tickets WHERE expires_at>?")
99 .bind(now)
100 .fetch_one(&mut *conn)
101 .await?;
102 valid(count < 128)?;
103 let mut id = [0; 32];
104 getrandom::fill(&mut id).map_err(|_| anyhow::anyhow!("error encrypted-image-entropy"))?;
105 let expires = now
106 .checked_add(crate::attachments::lifecycle::LEASE_TTL.as_secs() as i64)
107 .context("error encrypted-image-clock")?;
108 sqlx::query("INSERT INTO server_e2ee_image_tickets(reservation,object,workspace,device,expires_at) VALUES(?,?,?,?,?) ON CONFLICT(object,workspace,device) DO UPDATE SET reservation=excluded.reservation,expires_at=excluded.expires_at")
109 .bind(id.as_slice()).bind(d.object.as_slice()).bind(workspace).bind(device.as_slice()).bind(expires).execute(&mut *conn).await?;
110 sqlx::query("UPDATE server_e2ee_images SET unreferenced_at=NULL WHERE object=?")
111 .bind(d.object.as_slice())
112 .execute(conn)
113 .await?;
114 Ok(())
115}
116async fn ticket(
117 conn: &mut SqliteConnection,
118 object: &[u8; 32],
119 workspace: &str,
120 device: &[u8; 32],
121 ticket: &Ticket,
122 now: i64,
123) -> Result<()> {
124 valid(sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM server_e2ee_image_tickets WHERE object=? AND workspace=? AND device=? AND reservation=? AND expires_at>?)").bind(object.as_slice()).bind(workspace).bind(device.as_slice()).bind(ticket.reservation.as_slice()).bind(now).fetch_one(conn).await?)
125}
126async fn status(
127 conn: &mut SqliteConnection,
128 d: &Descriptor,
129 workspace: &str,
130 device: &[u8; 32],
131 now: i64,
132) -> Result<Reply> {
133 let complete: bool =
134 sqlx::query_scalar("SELECT complete FROM server_e2ee_images WHERE object=?")
135 .bind(d.object.as_slice())
136 .fetch_one(&mut *conn)
137 .await?;
138 let indices: Vec<i64> = sqlx::query_scalar(
139 "SELECT chunk_index FROM server_e2ee_image_chunks WHERE object=? ORDER BY chunk_index",
140 )
141 .bind(d.object.as_slice())
142 .fetch_all(&mut *conn)
143 .await?;
144 let t:Option<(Vec<u8>,i64)>=sqlx::query_as("SELECT reservation,expires_at FROM server_e2ee_image_tickets WHERE object=? AND workspace=? AND device=? AND expires_at>?").bind(d.object.as_slice()).bind(workspace).bind(device.as_slice()).bind(now).fetch_optional(conn).await?;
145 let (reservation, expires_at) = match t {
146 Some((id, e)) => (
147 Some(
148 id.try_into()
149 .map_err(|_| anyhow::anyhow!("error encrypted-image-storage"))?,
150 ),
151 Some(e),
152 ),
153 None => (None, None),
154 };
155 Ok(Reply::Status(Status {
156 complete,
157 missing: (0..d.artifact.chunks.len())
158 .filter(|i| !indices.contains(&(*i as i64)))
159 .collect(),
160 reservation,
161 expires_at,
162 }))
163}
164async fn eligible_object(
165 conn: &mut SqliteConnection,
166 m: &Membership,
167 d: &Descriptor,
168) -> Result<()> {
169 valid(m.generations().iter().any(|g| g.id == d.generation))?;
171 let admitted = if d.generation != m.current_generation().id {
172 sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM server_e2ee_images WHERE object=? AND origin IS NOT NULL AND descriptor=?)")
173 .bind(d.object.as_slice()).bind(d.encode()?).fetch_one(conn).await?
174 } else {
175 false
176 };
177 aven_protocol::tail::eligible_image(m, d.generation, admitted)?;
178 Ok(())
179}
180impl Database {
181 pub async fn prune_encrypted_images(
185 &self,
186 grace: std::time::Duration,
187 limit: usize,
188 ) -> Result<usize> {
189 let mut conn = self.acquire_writer().await?;
190 let mut tx = begin_immediate(&mut conn).await?;
191 let now = chrono::Utc::now().timestamp();
192 refresh(&mut tx, now).await?;
193 let cutoff = now
194 .checked_sub(i64::try_from(grace.as_secs())?)
195 .context("error encrypted-image-clock")?;
196 let objects:Vec<Vec<u8>>=sqlx::query_scalar("SELECT object FROM server_e2ee_images WHERE unreferenced_at<=? AND (complete=1 OR EXISTS(SELECT 1 FROM server_e2ee_image_chunks c WHERE c.object=server_e2ee_images.object)) ORDER BY object LIMIT ?").bind(cutoff).bind(i64::try_from(limit)?).fetch_all(&mut *tx).await?;
197 for object in &objects {
200 sqlx::query("UPDATE server_e2ee_images SET complete=0 WHERE object=?")
201 .bind(object)
202 .execute(&mut *tx)
203 .await?;
204 sqlx::query("DELETE FROM server_e2ee_image_chunks WHERE object=?")
205 .bind(object)
206 .execute(&mut *tx)
207 .await?;
208 }
209 tx.commit().await?;
210 Ok(objects.len())
211 }
212 pub async fn encrypted_image_exchange(
213 &self,
214 context: &Context,
215 bearer: &Secret,
216 op: Operation,
217 policy: LifecyclePolicy,
218 ) -> Result<Reply> {
219 let mut conn = self.acquire_writer().await?;
220 let mut tx = begin_immediate(&mut conn).await?;
221 let current =
222 crate::sync::seed_claim::membership::persistence::current(self, &mut tx).await?;
223 current
224 .membership
225 .authenticate(&context.authentication(bearer), false)?;
226 ensure!(
227 !current.membership.rotation_pending()
228 || matches!(&op, Operation::Status { .. } | Operation::Read { .. }),
229 "error membership-rotation-pending"
230 );
231 let binding = current.membership.publication().binding();
232 valid(
233 context.stream == binding.stream_id
234 && context.descriptor == binding.descriptor_commitment,
235 )?;
236 let now = chrono::Utc::now().timestamp();
237 let reply = match op {
238 Operation::Declare {
239 workspace,
240 descriptor,
241 } => {
242 workspace.parse::<crate::ids::WorkspaceId>()?;
243 let d = Descriptor::decode(&descriptor)?;
244 valid(d.vault == context.vault && d.stream == context.stream)?;
245 eligible_object(&mut tx, ¤t.membership, &d).await?;
246 let old: Option<Vec<u8>> =
247 sqlx::query_scalar("SELECT descriptor FROM server_e2ee_images WHERE object=?")
248 .bind(d.object.as_slice())
249 .fetch_optional(&mut *tx)
250 .await?;
251 if let Some(old) = old {
252 valid(old == descriptor)?;
253 } else {
254 let total: i64 = sqlx::query_scalar("SELECT count(*) FROM server_e2ee_images")
255 .fetch_one(&mut *tx)
256 .await?;
257 valid(total < 65536)?;
258 sqlx::query("INSERT INTO server_e2ee_images(object,byte_size,descriptor,complete,unreferenced_at) VALUES(?,?,?,0,?)").bind(d.object.as_slice()).bind(d.byte_size()).bind(&descriptor).bind(now).execute(&mut *tx).await?;
259 }
260 let known:bool=sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM server_e2ee_image_scopes WHERE object=? AND workspace=?)").bind(d.object.as_slice()).bind(&workspace).fetch_one(&mut *tx).await?;
261 if !known {
262 let count: i64 =
263 sqlx::query_scalar("SELECT count(*) FROM server_e2ee_image_scopes")
264 .fetch_one(&mut *tx)
265 .await?;
266 valid(count < 262144)?;
267 }
268 sqlx::query(
269 "INSERT OR IGNORE INTO server_e2ee_image_scopes(object,workspace) VALUES(?,?)",
270 )
271 .bind(d.object.as_slice())
272 .bind(&workspace)
273 .execute(&mut *tx)
274 .await?;
275 load(&mut tx, &d.object, &hash(&descriptor)).await?;
276 reserve(
277 &mut tx,
278 &d,
279 &workspace,
280 &context.device,
281 policy.quota_bytes,
282 now,
283 )
284 .await?;
285 status(&mut tx, &d, &workspace, &context.device, now).await?
286 }
287 other => dispatch_existing(&mut tx, context, ¤t.membership, other, now).await?,
288 };
289 tx.commit().await?;
290 Ok(reply)
291 }
292}
293async fn dispatch_existing(
294 conn: &mut SqliteConnection,
295 context: &Context,
296 membership: &Membership,
297 op: Operation,
298 now: i64,
299) -> Result<Reply> {
300 let (workspace, object, commitment) = match &op {
301 Operation::Status {
302 workspace,
303 object,
304 descriptor_commitment,
305 }
306 | Operation::Put {
307 workspace,
308 object,
309 descriptor_commitment,
310 ..
311 }
312 | Operation::Complete {
313 workspace,
314 object,
315 descriptor_commitment,
316 ..
317 }
318 | Operation::Read {
319 workspace,
320 object,
321 descriptor_commitment,
322 ..
323 }
324 | Operation::Release {
325 workspace,
326 object,
327 descriptor_commitment,
328 ..
329 } => (workspace, *object, *descriptor_commitment),
330 _ => unreachable!(),
331 };
332 workspace.parse::<crate::ids::WorkspaceId>()?;
333 scope(conn, &object, workspace).await?;
334 let (d, _) = load(conn, &object, &commitment).await?;
335 valid(d.vault == context.vault && d.stream == context.stream)?;
336 eligible_object(conn, membership, &d).await?;
337 match &op {
338 Operation::Status { .. } => status(conn, &d, workspace, &context.device, now).await,
339 Operation::Read { index, .. } => {
340 valid(*index < d.artifact.chunks.len())?;
341 let admitted: bool = sqlx::query_scalar(
342 "SELECT origin IS NOT NULL FROM server_e2ee_images WHERE object=?",
343 )
344 .bind(object.as_slice())
345 .fetch_one(&mut *conn)
346 .await?;
347 valid(admitted)?;
348 let bytes: Option<Vec<u8>> = sqlx::query_scalar(
349 "SELECT bytes FROM server_e2ee_image_chunks WHERE object=? AND chunk_index=?",
350 )
351 .bind(object.as_slice())
352 .bind(*index as i64)
353 .fetch_optional(conn)
354 .await?;
355 Ok(match bytes {
356 Some(b) => {
357 d.verify_chunk(*index, &b)?;
358 Reply::Chunk(b)
359 }
360 None => Reply::Unavailable,
361 })
362 }
363 Operation::Put { reservation, .. }
364 | Operation::Complete { reservation, .. }
365 | Operation::Release { reservation, .. } => {
366 ticket(
367 conn,
368 &object,
369 workspace,
370 &context.device,
371 &Ticket {
372 reservation: *reservation,
373 },
374 now,
375 )
376 .await?;
377 match &op {
378 Operation::Put { index, record, .. } => {
379 d.verify_chunk(*index, record)?;
380 let old:Option<Vec<u8>>=sqlx::query_scalar("SELECT bytes FROM server_e2ee_image_chunks WHERE object=? AND chunk_index=?").bind(object.as_slice()).bind(*index as i64).fetch_optional(&mut *conn).await?;
381 valid(old.as_ref().is_none_or(|b| b == record))?;
382 sqlx::query("INSERT OR IGNORE INTO server_e2ee_image_chunks(object,chunk_index,bytes) VALUES(?,?,?)").bind(object.as_slice()).bind(*index as i64).bind(record).execute(&mut *conn).await?;
383 }
384 Operation::Complete { .. } => {
385 let records:Vec<Vec<u8>>=sqlx::query_scalar("SELECT bytes FROM server_e2ee_image_chunks WHERE object=? ORDER BY chunk_index").bind(object.as_slice()).fetch_all(&mut *conn).await?;
386 d.verify(&records)?;
387 sqlx::query("UPDATE server_e2ee_images SET complete=1 WHERE object=?")
388 .bind(object.as_slice())
389 .execute(&mut *conn)
390 .await?;
391 }
392 Operation::Release { .. } => {
393 sqlx::query("DELETE FROM server_e2ee_image_tickets WHERE reservation=?")
394 .bind(reservation.as_slice())
395 .execute(&mut *conn)
396 .await?;
397 }
398 _ => unreachable!(),
399 }
400 Ok(Reply::Done)
401 }
402 _ => unreachable!(),
403 }
404}
405
406pub(in crate::sync::encrypted_tail) async fn admit(
407 conn: &mut SqliteConnection,
408 context: &Context,
409 membership: &Membership,
410 id: &str,
411 p: &super::super::domain::Projection,
412 t: Option<&Ticket>,
413) -> Result<()> {
414 use super::super::domain::Projection;
415 let now = chrono::Utc::now().timestamp();
416 match p {
417 Projection::Move {
418 source,
419 target,
420 tasks,
421 } => {
422 valid(t.is_none())?;
423 super::super::moves::admit(conn, source, target, tasks).await?;
424 }
425 Projection::Ref {
426 workspace,
427 task,
428 reference,
429 descriptor,
430 deleted,
431 version,
432 } => {
433 let authored_workspace = workspace;
434 let routed = super::super::moves::server_workspace(conn, task, workspace).await?;
435 let workspace = &routed;
436 let d = Descriptor::decode(descriptor)?;
437 eligible_object(conn, membership, &d).await?;
438 let (_, complete) = load(conn, &d.object, &hash(descriptor)).await?;
439 valid(complete && d.vault == context.vault && d.stream == context.stream)?;
440 let records: Vec<Vec<u8>> = sqlx::query_scalar(
441 "SELECT bytes FROM server_e2ee_image_chunks WHERE object=? ORDER BY chunk_index",
442 )
443 .bind(d.object.as_slice())
444 .fetch_all(&mut *conn)
445 .await?;
446 d.verify(&records)?;
447 let exists:bool=sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM server_e2ee_image_references WHERE workspace=? AND reference=?)").bind(workspace).bind(reference).fetch_one(&mut *conn).await?;
448 valid(!exists)?;
449 if !protected(conn, &d.object, workspace).await? {
450 ticket(
451 conn,
452 &d.object,
453 authored_workspace,
454 &context.device,
455 t.context("error encrypted-image-reservation-required")?,
456 now,
457 )
458 .await?;
459 }
460 let count: i64 =
461 sqlx::query_scalar("SELECT count(*) FROM server_e2ee_image_references")
462 .fetch_one(&mut *conn)
463 .await?;
464 valid(count < 262144)?;
465 let state:Option<(Option<String>,bool,bool)>=sqlx::query_as("SELECT version,deleted,protected FROM server_e2ee_image_parents WHERE workspace=? AND parent=?").bind(workspace).bind(task).fetch_optional(&mut *conn).await?;
466 if state.is_none() {
467 let count: i64 =
468 sqlx::query_scalar("SELECT count(*) FROM server_e2ee_image_parents")
469 .fetch_one(&mut *conn)
470 .await?;
471 valid(count < 262144)?;
472 }
473 let (known, state_deleted, sticky) = state.unwrap_or((None, false, false));
474 let mut state = crate::sync::persistence::parent_liveness::ParentState {
475 version: known.clone(),
476 deleted: state_deleted,
477 protected: sticky,
478 };
479 state.protect_hint(*deleted, version.as_deref());
480 sqlx::query("INSERT INTO server_e2ee_image_parents(workspace,parent,version,deleted,protected) VALUES(?,?,?,?,?) ON CONFLICT(workspace,parent) DO UPDATE SET protected=excluded.protected").bind(workspace).bind(task).bind(known).bind(state_deleted).bind(state.protected).execute(&mut *conn).await?;
481 sqlx::query("INSERT INTO server_e2ee_image_references(workspace,reference,parent,deleted,object) VALUES(?,?,?,0,?)").bind(workspace).bind(reference).bind(task).bind(d.object.as_slice()).execute(&mut *conn).await?;
482 sqlx::query("UPDATE server_e2ee_images SET origin=COALESCE(origin,?) WHERE object=?")
483 .bind(id)
484 .bind(d.object.as_slice())
485 .execute(&mut *conn)
486 .await?;
487 if let Some(t) = t {
488 sqlx::query("DELETE FROM server_e2ee_image_tickets WHERE reservation=? AND device=? AND workspace=? AND object=?").bind(t.reservation.as_slice()).bind(context.device.as_slice()).bind(authored_workspace).bind(d.object.as_slice()).execute(&mut *conn).await?;
489 }
490 sqlx::query(
491 "INSERT OR IGNORE INTO server_e2ee_image_scopes(object, workspace) VALUES (?, ?)",
492 )
493 .bind(d.object.as_slice())
494 .bind(workspace)
495 .execute(&mut *conn)
496 .await?;
497 let scopes: i64 = sqlx::query_scalar("SELECT count(*) FROM server_e2ee_image_scopes")
498 .fetch_one(&mut *conn)
499 .await?;
500 valid(scopes <= 262144)?;
501 retain_parent(conn, workspace, task).await?;
502 }
503 Projection::Unref {
504 workspace,
505 task,
506 reference,
507 } => {
508 let workspace = super::super::moves::server_workspace(conn, task, workspace).await?;
509 let changed=sqlx::query("UPDATE server_e2ee_image_references SET deleted=1 WHERE workspace=? AND reference=? AND parent=?").bind(&workspace).bind(reference).bind(task).execute(&mut *conn).await?.rows_affected();
510 valid(changed == 1)?;
511 }
512 _ => valid(t.is_none())?,
513 }
514 Ok(())
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 #[tokio::test]
522 async fn invalid_workspaces_refuse_authenticated_declare_and_existing_dispatch() {
523 use crate::ids::InvalidWorkspaceId;
524 use crate::sync::seed_claim::membership::test_support::Fixture;
525 use aven_protocol::artifact::catalog::{Artifact, Chunk};
526
527 let f = Fixture::new().await;
528 let membership = Membership::from_publication(
529 f.seed.genesis(),
530 &f.package.descriptor,
531 f.publication.record(),
532 )
533 .unwrap();
534 let binding = membership.publication().binding();
535 let context = Context {
536 vault: binding.vault_id,
537 genesis: binding.genesis_commitment,
538 device: f.seed.genesis().device_id(),
539 head: membership.head(),
540 stream: binding.stream_id,
541 descriptor: binding.descriptor_commitment,
542 };
543 let descriptor = Descriptor {
545 vault: context.vault,
546 stream: context.stream,
547 generation: membership.current_generation().id,
548 object: [44; 32],
549 artifact: Artifact {
550 total: 1,
551 aggregate: [45; 32],
552 chunks: vec![Chunk {
553 length: 223,
554 hash: [46; 32],
555 nonce: [47; 24],
556 }],
557 },
558 }
559 .encode()
560 .unwrap();
561 for op in [
562 Operation::Declare {
563 workspace: "invalid".into(),
564 descriptor: descriptor.clone(),
565 },
566 Operation::Status {
567 workspace: "0123456789ABCDEI".into(),
568 object: [44; 32],
569 descriptor_commitment: hash(&descriptor),
570 },
571 ] {
572 let error = f
573 .db
574 .encrypted_image_exchange(&context, f.seed.bearer(), op, LifecyclePolicy::default())
575 .await
576 .err()
577 .unwrap();
578 assert!(error.is::<InvalidWorkspaceId>());
579 assert_eq!(
580 error.to_string(),
581 "workspace ID must be 16 Crockford Base32 characters"
582 );
583 }
584 let mut conn = f.db.acquire_reader().await.unwrap();
585 let counts: (i64, i64, i64) = sqlx::query_as(
586 "SELECT (SELECT count(*) FROM server_e2ee_images), (SELECT count(*) FROM server_e2ee_image_scopes), (SELECT count(*) FROM server_e2ee_image_tickets)",
587 ).fetch_one(&mut *conn).await.unwrap();
588 assert_eq!(counts, (0, 0, 0));
589 }
590
591 #[tokio::test]
592 async fn retired_declaration_cannot_bypass_active_object_limit() {
593 let root = tempfile::tempdir().unwrap();
594 let db = Database::open(&root.path().join("test.sqlite"))
595 .await
596 .unwrap();
597 let a = crate::sync::encrypted_tail::tests::authority();
598 let mut conn = db.acquire_writer().await.unwrap();
599 let mut tx = begin_immediate(&mut conn).await.unwrap();
600 let (retired, _) = Descriptor::seal(&a, b"x").unwrap();
601 sqlx::query("INSERT INTO server_e2ee_images(object,byte_size,descriptor) VALUES(?,?,?)")
602 .bind(retired.object.as_slice())
603 .bind(retired.byte_size())
604 .bind(retired.encode().unwrap())
605 .execute(&mut *tx)
606 .await
607 .unwrap();
608 for _ in 0..128 {
610 let (d, records) = Descriptor::seal(&a, b"x").unwrap();
611 sqlx::query(
612 "INSERT INTO server_e2ee_images(object,byte_size,descriptor) VALUES(?,?,?)",
613 )
614 .bind(d.object.as_slice())
615 .bind(d.byte_size())
616 .bind(d.encode().unwrap())
617 .execute(&mut *tx)
618 .await
619 .unwrap();
620 sqlx::query(
621 "INSERT INTO server_e2ee_image_chunks(object,chunk_index,bytes) VALUES(?,0,?)",
622 )
623 .bind(d.object.as_slice())
624 .bind(&records[0])
625 .execute(&mut *tx)
626 .await
627 .unwrap();
628 }
629 assert!(
630 reserve(
631 &mut tx,
632 &retired,
633 "0000000000000000",
634 &a.context.device,
635 i64::MAX,
636 123
637 )
638 .await
639 .is_err()
640 );
641 let tickets: i64 = sqlx::query_scalar("SELECT count(*) FROM server_e2ee_image_tickets")
642 .fetch_one(&mut *tx)
643 .await
644 .unwrap();
645 assert_eq!(tickets, 0);
646 }
647}