1use sea_query::Value as SeaValue;
10use serde_json::json;
11
12use cratefield_core::{Database, DbError, Row, Statement};
13use cratefield_kms::{Dek, Kms};
14
15use crate::SealedError;
16use crate::outer;
17use crate::record::{CreateBlob, Wrap, b64url_decode, b64url_encode};
18
19pub const INLINE_BODY_MAX_BYTES: usize = 32 * 1024;
23
24#[derive(Debug)]
26#[non_exhaustive]
27pub enum StoreError {
28 Taken,
30 NotFound,
32 Stale,
35 Db(DbError),
37 Kms(cratefield_kms::KmsError),
39 Tampered(String),
41 Internal(String),
44}
45
46impl std::fmt::Display for StoreError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Taken => f.write_str("that blob_id is already stored"),
50 Self::NotFound => f.write_str("no such blob"),
51 Self::Stale => f.write_str("the record changed since the caller last read it"),
52 Self::Db(err) => write!(f, "{err}"),
53 Self::Kms(err) => write!(f, "{err}"),
54 Self::Tampered(what) => write!(f, "the stored body failed authentication: {what}"),
55 Self::Internal(what) => write!(f, "{what}"),
56 }
57 }
58}
59
60impl std::error::Error for StoreError {}
61
62impl From<DbError> for StoreError {
63 fn from(err: DbError) -> Self {
64 Self::Db(err)
65 }
66}
67
68impl From<cratefield_kms::KmsError> for StoreError {
69 fn from(err: cratefield_kms::KmsError) -> Self {
70 Self::Kms(err)
71 }
72}
73
74impl From<SealedError> for StoreError {
75 fn from(err: SealedError) -> Self {
76 match err {
77 SealedError::Tampered(what) => Self::Tampered(what),
78 other => Self::Internal(other.to_string()),
79 }
80 }
81}
82
83impl From<cratefield_core::BlobError> for StoreError {
84 fn from(err: cratefield_core::BlobError) -> Self {
85 Self::Internal(format!("the blob store refused: {err}"))
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct BlobRow {
93 pub blob_id: String,
94 pub version: u32,
95 pub purpose: String,
96 pub alg: String,
97 pub wraps: String,
99 pub created_by_credential: String,
100 pub ciphertext_len: i64,
102 pub outer_ct: Option<Vec<u8>>,
105 pub object_key: Option<String>,
106 pub revision: i64,
112 pub created_at: String,
113 pub updated_at: String,
114}
115
116fn text(value: &str) -> SeaValue {
117 SeaValue::String(Some(Box::new(value.to_owned())))
118}
119
120fn bytes(value: &[u8]) -> SeaValue {
121 SeaValue::Bytes(Some(Box::new(value.to_vec())))
122}
123
124fn row_text(row: &Row, column: &str) -> Option<String> {
125 row.get::<Option<String>>(column).flatten()
126}
127
128const ROW_COLUMNS: &str = "blob_id, version, purpose, alg, wraps, created_by_credential, \
129 ciphertext_len, outer_ct, object_key, revision, created_at, updated_at";
130
131fn from_row(row: &Row) -> BlobRow {
132 BlobRow {
133 blob_id: row.get::<String>("blob_id").unwrap_or_default(),
134 version: u32::try_from(row.get::<i64>("version").unwrap_or_default()).unwrap_or_default(),
135 purpose: row_text(row, "purpose").unwrap_or_default(),
136 alg: row_text(row, "alg").unwrap_or_default(),
137 wraps: row_text(row, "wraps").unwrap_or_default(),
138 created_by_credential: row_text(row, "created_by_credential").unwrap_or_default(),
139 ciphertext_len: row.get::<i64>("ciphertext_len").unwrap_or_default(),
140 outer_ct: match row.get::<SeaValue>("outer_ct") {
141 Some(SeaValue::Bytes(Some(bytes))) => Some(bytes.as_ref().clone()),
142 _ => None,
143 },
144 object_key: row_text(row, "object_key"),
145 revision: row.get::<i64>("revision").unwrap_or_default(),
146 created_at: row_text(row, "created_at").unwrap_or_default(),
147 updated_at: row_text(row, "updated_at").unwrap_or_default(),
148 }
149}
150
151fn object_key(subject: &str, blob_id: &str, version: u32) -> Result<String, StoreError> {
158 use sha2::{Digest, Sha256};
159 use std::fmt::Write as _;
160 let digest = Sha256::digest(format!("{subject}/{blob_id}").as_bytes());
161 let mut slug = String::with_capacity(48);
162 for byte in digest.iter().take(16) {
163 let _ = write!(slug, "{byte:02x}");
164 }
165 let mut suffix = [0u8; 8];
166 getrandom::fill(&mut suffix).map_err(|err| StoreError::Internal(err.to_string()))?;
167 let _ = write!(slug, "-v{version}-");
168 for byte in suffix {
169 let _ = write!(slug, "{byte:02x}");
170 }
171 Ok(format!("blobs/{slug}.outer"))
175}
176
177pub(crate) fn is_inline(outer_ct_len: usize) -> bool {
179 outer_ct_len <= INLINE_BODY_MAX_BYTES
180}
181
182fn fresh_revision() -> Result<i64, StoreError> {
188 let mut bytes = [0_u8; 8];
189 getrandom::fill(&mut bytes).map_err(|err| StoreError::Internal(err.to_string()))?;
190 Ok(i64::from_be_bytes(bytes) & i64::MAX)
191}
192
193pub struct SealedBody {
197 pub wrapped_dek: Vec<u8>,
198 pub provider: &'static str,
199 pub key_ref: String,
200 pub outer_ct: Vec<u8>,
201 pub ciphertext_len: usize,
203 pub object_key: Option<String>,
205}
206
207pub async fn seal_body(
214 kms: &dyn Kms,
215 body: &[u8],
216 subject: &str,
217 blob_id: &str,
218 version: u32,
219) -> Result<SealedBody, StoreError> {
220 let dek = Dek::generate().map_err(|err| StoreError::Internal(err.to_string()))?;
221 let wrapped_dek = kms.wrap(&dek).await?;
222 let outer_ct = outer::seal(&dek, body, subject, blob_id, version)?;
223 let inline = is_inline(outer_ct.len());
224 let object_key = match (!inline).then(|| object_key(subject, blob_id, version)) {
225 Some(key) => Some(key?),
226 None => None,
227 };
228 Ok(SealedBody {
229 wrapped_dek,
230 provider: kms.provider(),
231 key_ref: kms.key_ref().to_owned(),
232 ciphertext_len: body.len(),
233 object_key,
234 outer_ct,
235 })
236}
237
238pub async fn open_body(
248 kms: &dyn Kms,
249 blob: Option<&dyn cratefield_core::Blob>,
250 db: &dyn Database,
251 subject: &str,
252 row: &BlobRow,
253) -> Result<Vec<u8>, StoreError> {
254 let rows = db
255 .query(&Statement::with_values(
256 "SELECT wrapped_dek FROM sealed_deks WHERE subject = ? AND blob_id = ?",
257 vec![text(subject), text(&row.blob_id)],
258 ))
259 .await?;
260 let wrapped_dek = rows
261 .first()
262 .and_then(|r| r.get::<Vec<u8>>("wrapped_dek"))
263 .ok_or(StoreError::NotFound)?;
264 let dek = kms.unwrap(&wrapped_dek).await?;
265 let outer_ct = match (&row.outer_ct, &row.object_key) {
266 (Some(inline), _) => inline.clone(),
267 (None, Some(key)) => {
268 let store = blob.ok_or_else(|| {
269 StoreError::Internal("no Blob port is mounted, but the body lives in one".into())
270 })?;
271 store
272 .get(key)
273 .await?
274 .map(|object| object.bytes)
275 .ok_or(StoreError::NotFound)?
276 }
277 (None, None) => {
278 return Err(StoreError::Internal(
279 "a blob row names neither an inline body nor an object key".into(),
280 ));
281 }
282 };
283 Ok(outer::open(
284 &dek,
285 &outer_ct,
286 subject,
287 &row.blob_id,
288 row.version,
289 )?)
290}
291
292pub fn wire_record(
303 row: &BlobRow,
304 subject: &str,
305 ciphertext: &[u8],
306) -> Result<serde_json::Value, StoreError> {
307 let wraps: Vec<Wrap> = serde_json::from_str(&row.wraps).map_err(|err| {
308 StoreError::Internal(format!("a stored wraps column does not parse: {err}"))
309 })?;
310 Ok(json!({
311 "blob_id": row.blob_id,
312 "version": row.version,
313 "purpose": row.purpose,
314 "alg": row.alg,
315 "ciphertext": b64url_encode(ciphertext),
316 "wraps": wraps,
317 "created_by_credential": row.created_by_credential,
318 "subject": subject,
319 "created_at": row.created_at,
320 "updated_at": row.updated_at,
321 }))
322}
323
324pub fn decode_body(blob: &CreateBlob) -> Result<Vec<u8>, StoreError> {
332 b64url_decode(&blob.ciphertext)
333 .ok_or_else(|| StoreError::Internal("a validated ciphertext failed to decode".into()))
334}
335
336pub async fn insert(
346 db: &dyn Database,
347 subject: &str,
348 blob: &CreateBlob,
349 sealed: &SealedBody,
350 ts: &str,
351) -> Result<(), StoreError> {
352 let existing = db
353 .query(&Statement::with_values(
354 "SELECT blob_id FROM sealed_blobs WHERE subject = ? AND blob_id = ?",
355 vec![text(subject), text(&blob.blob_id)],
356 ))
357 .await?;
358 if !existing.is_empty() {
359 return Err(StoreError::Taken);
360 }
361 let (outer_ct, object_key): (SeaValue, SeaValue) = match &sealed.object_key {
362 None => (bytes(&sealed.outer_ct), SeaValue::String(None)),
363 Some(key) => (SeaValue::Bytes(None), text(key)),
364 };
365 db.batch_atomic(&[
366 Statement::with_values(
367 "INSERT INTO sealed_blobs (subject, blob_id, version, purpose, alg, wraps, \
368 created_by_credential, ciphertext_len, outer_ct, object_key, revision, \
369 created_at, updated_at) \
370 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)",
371 vec![
372 text(subject),
373 text(&blob.blob_id),
374 SeaValue::BigInt(Some(i64::from(blob.version))),
375 text(&blob.purpose),
376 text(&blob.alg),
377 text(&wraps_json(&blob.wraps)?),
378 text(&blob.created_by_credential),
379 big(sealed.ciphertext_len),
380 outer_ct,
381 object_key,
382 text(ts),
383 text(ts),
384 ],
385 ),
386 Statement::with_values(
387 "INSERT INTO sealed_deks (subject, blob_id, kms_provider, kms_key_ref, wrapped_dek, \
388 revision) VALUES (?, ?, ?, ?, ?, 1)",
389 vec![
390 text(subject),
391 text(&blob.blob_id),
392 text(sealed.provider),
393 text(&sealed.key_ref),
394 bytes(&sealed.wrapped_dek),
395 ],
396 ),
397 ])
398 .await?;
399 Ok(())
400}
401
402fn wraps_json(wraps: &[Wrap]) -> Result<String, StoreError> {
405 serde_json::to_string(wraps).map_err(|err| {
406 StoreError::Internal(format!("a validated wrap set does not serialise: {err}"))
407 })
408}
409
410fn big(len: usize) -> SeaValue {
412 SeaValue::BigInt(Some(i64::try_from(len).unwrap_or(i64::MAX)))
413}
414
415pub async fn load(
421 db: &dyn Database,
422 subject: &str,
423 blob_id: &str,
424) -> Result<Option<BlobRow>, StoreError> {
425 let rows = db
426 .query(&Statement::with_values(
427 format!("SELECT {ROW_COLUMNS} FROM sealed_blobs WHERE subject = ? AND blob_id = ?"),
428 vec![text(subject), text(blob_id)],
429 ))
430 .await?;
431 Ok(rows.first().map(from_row))
432}
433
434pub async fn list(db: &dyn Database, subject: &str) -> Result<Vec<BlobRow>, StoreError> {
440 let rows = db
441 .query(&Statement::with_values(
442 format!(
443 "SELECT {ROW_COLUMNS} FROM sealed_blobs WHERE subject = ? ORDER BY created_at DESC"
444 ),
445 vec![text(subject)],
446 ))
447 .await?;
448 Ok(rows.rows.iter().map(from_row).collect())
449}
450
451pub async fn replace_wraps(
468 db: &dyn Database,
469 subject: &str,
470 blob_id: &str,
471 expected_revision: i64,
472 wraps: &str,
473 ts: &str,
474) -> Result<(), StoreError> {
475 let mut fresh = fresh_revision()?;
476 if fresh == expected_revision {
477 fresh ^= 1;
478 }
479 let expected = SeaValue::BigInt(Some(expected_revision));
480 let written = SeaValue::BigInt(Some(fresh));
481 db.batch_atomic(&[
482 Statement::with_values(
483 "UPDATE sealed_blobs SET wraps = ?, updated_at = ?, revision = ? \
484 WHERE subject = ? AND blob_id = ? AND revision = ?",
485 vec![
486 text(wraps),
487 text(ts),
488 written.clone(),
489 text(subject),
490 text(blob_id),
491 expected,
492 ],
493 ),
494 Statement::with_values(
495 "UPDATE sealed_deks SET revision = ? \
496 WHERE subject = ? AND blob_id = ? AND revision = ?",
497 vec![
498 written,
499 text(subject),
500 text(blob_id),
501 SeaValue::BigInt(Some(expected_revision)),
502 ],
503 ),
504 ])
505 .await?;
506 match load(db, subject, blob_id).await? {
509 Some(row) if row.revision == fresh => Ok(()),
510 Some(_) => Err(StoreError::Stale),
511 None => Err(StoreError::NotFound),
512 }
513}
514
515pub async fn rotate(
530 db: &dyn Database,
531 subject: &str,
532 blob: &CreateBlob,
533 sealed: &SealedBody,
534 ts: &str,
535 expected_revision: i64,
536 store: Option<&dyn cratefield_core::Blob>,
537) -> Result<(), StoreError> {
538 let old_key = load(db, subject, &blob.blob_id)
539 .await?
540 .ok_or(StoreError::NotFound)?
541 .object_key;
542 let previous_version = blob.version.saturating_sub(1);
543 let mut fresh = fresh_revision()?;
544 if fresh == expected_revision {
545 fresh ^= 1;
546 }
547 let expected = SeaValue::BigInt(Some(expected_revision));
548 let written = SeaValue::BigInt(Some(fresh));
549 let (outer_ct, new_key): (SeaValue, SeaValue) = match &sealed.object_key {
550 None => (bytes(&sealed.outer_ct), SeaValue::String(None)),
551 Some(key) => (SeaValue::Bytes(None), text(key)),
552 };
553 db.batch_atomic(&[
554 Statement::with_values(
555 "UPDATE sealed_blobs SET version = ?, wraps = ?, ciphertext_len = ?, outer_ct = ?, \
556 object_key = ?, updated_at = ?, revision = ? \
557 WHERE subject = ? AND blob_id = ? AND version = ? AND revision = ?",
558 vec![
559 SeaValue::BigInt(Some(i64::from(blob.version))),
560 text(&wraps_json(&blob.wraps)?),
561 big(sealed.ciphertext_len),
562 outer_ct,
563 new_key,
564 text(ts),
565 written.clone(),
566 text(subject),
567 text(&blob.blob_id),
568 SeaValue::BigInt(Some(i64::from(previous_version))),
569 expected.clone(),
570 ],
571 ),
572 Statement::with_values(
573 "UPDATE sealed_deks SET kms_provider = ?, kms_key_ref = ?, wrapped_dek = ?, \
574 revision = ? WHERE subject = ? AND blob_id = ? AND revision = ?",
575 vec![
576 text(sealed.provider),
577 text(&sealed.key_ref),
578 bytes(&sealed.wrapped_dek),
579 written,
580 text(subject),
581 text(&blob.blob_id),
582 expected,
583 ],
584 ),
585 ])
586 .await?;
587 match load(db, subject, &blob.blob_id).await? {
590 Some(row) if row.revision == fresh => {}
591 Some(_) => return Err(StoreError::Stale),
592 None => return Err(StoreError::NotFound),
593 }
594 if let (Some(old_key), Some(store)) = (old_key, store)
599 && let Err(err) = store.delete(&old_key).await
600 {
601 tracing::warn!(error = %err, "sealed: the previous body object could not be removed");
602 }
603 Ok(())
604}
605
606pub async fn erase(
615 db: &dyn Database,
616 subject: &str,
617 blob_id: &str,
618) -> Result<Option<String>, StoreError> {
619 let row = load(db, subject, blob_id)
620 .await?
621 .ok_or(StoreError::NotFound)?;
622 db.batch_atomic(&[
623 Statement::with_values(
624 "DELETE FROM sealed_deks WHERE subject = ? AND blob_id = ?",
625 vec![text(subject), text(blob_id)],
626 ),
627 Statement::with_values(
628 "DELETE FROM sealed_blobs WHERE subject = ? AND blob_id = ?",
629 vec![text(subject), text(blob_id)],
630 ),
631 ])
632 .await?;
633 Ok(row.object_key)
634}