cratefield_module_sealed/
lib.rs1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4pub mod audit;
5pub mod notify;
6pub mod record;
7pub mod store;
8
9mod handlers;
10mod outer;
11
12use std::sync::Arc;
13
14use cratefield_core::{
15 Config, ConfigError, DataKind, Disposition, Migrations, Module, ModuleContext, PersonalDataSet,
16 Port, SqlMigration,
17};
18use cratefield_kms::Kms;
19
20pub use crate::notify::{
21 MailNotifier, NoopNotifier, Notice, NotifyError, UnlockNotifier,
22 conformance as notifier_conformance,
23};
24
25pub const MODULE_NAME: &str = "sealed";
27
28pub const MAX_RECORD_BYTES: usize = 1024 * 1024;
34
35#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
39pub enum SealedError {
40 #[error("{0}")]
42 BadInput(String),
43 #[error("{0}")]
45 BadWrap(String),
46 #[error("{0}")]
49 WeakParams(String),
50 #[error("{0}")]
52 ChainBroken(String),
53 #[error("{0}")]
55 Tampered(String),
56 #[error("{0}")]
59 Internal(String),
60}
61
62impl From<cratefield_core::DbError> for SealedError {
63 fn from(err: cratefield_core::DbError) -> Self {
64 Self::Internal(format!("a database statement failed: {err}"))
65 }
66}
67
68pub struct Sealed {
73 pub(crate) kms: Arc<dyn Kms>,
74 pub(crate) notifier: Arc<dyn UnlockNotifier>,
75}
76
77impl Sealed {
78 #[must_use]
80 pub fn new(kms: Arc<dyn Kms>, notifier: Arc<dyn UnlockNotifier>) -> Self {
81 Self { kms, notifier }
82 }
83}
84
85const MIGRATION_INIT: SqlMigration = SqlMigration::new(
89 "0001",
90 "init",
91 include_str!("../migrations/sqlite/0001_init.sql"),
92);
93
94const MIGRATION_INIT_PG: SqlMigration = SqlMigration::new(
95 "0001",
96 "init",
97 include_str!("../migrations/postgres/0001_init.sql"),
98);
99
100const MIGRATION_REVISION_BIGINT: SqlMigration = SqlMigration::new(
103 "0002",
104 "revision_bigint",
105 include_str!("../migrations/sqlite/0002_revision_bigint.sql"),
106);
107
108const MIGRATION_REVISION_BIGINT_PG: SqlMigration = SqlMigration::new(
109 "0002",
110 "revision_bigint",
111 include_str!("../migrations/postgres/0002_revision_bigint.sql"),
112);
113
114impl Module for Sealed {
115 fn name(&self) -> &'static str {
116 MODULE_NAME
117 }
118
119 fn version(&self) -> &'static str {
120 env!("CARGO_PKG_VERSION")
121 }
122
123 fn requires(&self) -> &'static [Port] {
124 &[Port::Db, Port::Auth, Port::Blob]
129 }
130
131 fn optional(&self) -> &'static [Port] {
132 &[Port::RateLimiter, Port::Clock]
136 }
137
138 fn tables(&self) -> &'static [&'static str] {
139 &["sealed_blobs", "sealed_deks", "sealed_audit"]
140 }
141
142 fn personal_data(&self) -> &'static [PersonalDataSet] {
143 const SETS: &[PersonalDataSet] = &[
144 PersonalDataSet {
145 table: "sealed_blobs",
146 subject: "subject",
147 kind: DataKind::Content,
148 disposition: Disposition::Erase,
149 description: "Blobs you sealed in your browser: we hold the encrypted \
150 bytes and the wrapped keys for them — never the plaintext, and \
151 never the key that opens it. An export lists the metadata, the \
152 wrap set and our own encrypted copy of the body; the copy you \
153 can open exists only in your browser. Erasing a blob deletes it \
154 and the internal key that unlocks our encrypted copy, after \
155 which nothing on our side can open what you stored.",
156 redacted: &[],
157 subject_via: None,
158 },
159 PersonalDataSet {
160 table: "sealed_deks",
161 subject: "subject",
162 kind: DataKind::Identifier,
163 disposition: Disposition::Erase,
164 description: "The internal, wrapped key that unlocks our encrypted copy \
165 of each of your blobs, kept in its own table so erasure deletes it \
166 first — the step that makes every remaining copy unreadable. It is \
167 key material, so an export lists it without copying it.",
168 redacted: &["wrapped_dek"],
169 subject_via: None,
170 },
171 PersonalDataSet {
172 table: "sealed_audit",
173 subject: "subject",
174 kind: DataKind::Usage,
175 disposition: Disposition::Retain(
176 "The access chain is append-only and hash-linked: its value is that \
177 no one — including an operator — can remove one person's rows \
178 without breaking the proof that nobody altered anyone else's. A \
179 row holds your account id, the blob's id, the action and the time, \
180 and never any content, because the server never has any.",
181 ),
182 description: "Which of your blobs were stored, opened, edited, rotated or \
183 erased, and when.",
184 redacted: &[],
185 subject_via: None,
186 },
187 ];
188 SETS
189 }
190
191 fn migrations(&self) -> Migrations {
192 const MIGRATIONS: [SqlMigration; 2] = [MIGRATION_INIT, MIGRATION_REVISION_BIGINT];
193 const MIGRATIONS_PG: [SqlMigration; 2] = [MIGRATION_INIT_PG, MIGRATION_REVISION_BIGINT_PG];
194 const _: () = cratefield_core::assert_migration_set(&MIGRATIONS);
195 Migrations {
196 sqlite: &MIGRATIONS,
197 postgres: &MIGRATIONS_PG,
198 }
199 }
200
201 fn validate_config(&self, _cfg: &dyn Config) -> Result<(), ConfigError> {
202 Ok(())
206 }
207
208 fn max_body_bytes(&self, _cfg: &dyn Config) -> usize {
209 MAX_RECORD_BYTES
210 }
211
212 fn router(&self, ctx: ModuleContext) -> axum::Router {
213 handlers::router(self, ctx)
214 }
215}