1use super::{FileStorageProvider, StorageError, StorageResult};
14use appcore_contracts::ApplicationId;
15use appcore_dnt::{
16 inspect_header, open_owned, rekey, seal, verify, ContentType, DntCodec, DntKeyProvider,
17 DntOpenOptions, DntSealOptions, KeyId, DNT_FLAG_PAYLOAD_DEFLATE,
18};
19use appcore_types::TenantId;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct SealedStoragePolicy {
24 pub application_id: ApplicationId,
26 pub tenant_id: Option<TenantId>,
28 pub content_type: ContentType,
30 pub schema_version: u32,
32 pub key_id: KeyId,
34 pub max_payload_bytes: u64,
36 pub public_metadata: Vec<u8>,
38}
39
40impl SealedStoragePolicy {
41 pub fn open_options(&self) -> DntOpenOptions {
43 DntOpenOptions {
44 application_id: self.application_id.clone(),
45 tenant_id: self.tenant_id.clone(),
46 content_type: self.content_type.clone(),
47 max_payload_bytes: Some(self.max_payload_bytes),
48 }
49 }
50
51 fn seal_options(&self, created_at_ms: u64, encrypted_metadata: Vec<u8>) -> DntSealOptions {
52 DntSealOptions {
53 application_id: self.application_id.clone(),
54 tenant_id: self.tenant_id.clone(),
55 content_type: self.content_type.clone(),
56 schema_version: self.schema_version,
57 key_id: self.key_id.clone(),
58 created_at_ms,
59 public_metadata: self.public_metadata.clone(),
60 encrypted_metadata,
61 flags: 0,
62 max_payload_bytes: Some(self.max_payload_bytes),
63 }
64 }
65}
66
67pub trait SealedObjectStore {
69 fn write_object(&self, path: &str, payload: &[u8]) -> StorageResult<()>;
71 fn read_object(&self, path: &str) -> StorageResult<Vec<u8>>;
73 fn verify_object(&self, path: &str) -> StorageResult<()>;
75 fn rekey_object(&self, path: &str, new_key_id: KeyId) -> StorageResult<()>;
77}
78
79pub trait SealedSnapshotStore: SealedObjectStore {}
81
82pub trait SealedSecretStore: SealedObjectStore {}
84
85#[derive(Debug)]
87pub struct DntFileObjectStore<'a, K, C> {
88 provider: &'a FileStorageProvider,
89 key_provider: &'a K,
90 codec: C,
91 policy: SealedStoragePolicy,
92}
93
94impl<'a, K, C> DntFileObjectStore<'a, K, C>
95where
96 K: DntKeyProvider,
97 C: DntCodec,
98{
99 pub fn new(
101 provider: &'a FileStorageProvider,
102 key_provider: &'a K,
103 codec: C,
104 policy: SealedStoragePolicy,
105 ) -> Self {
106 Self {
107 provider,
108 key_provider,
109 codec,
110 policy,
111 }
112 }
113
114 pub fn write_object_compact(&self, path: &str, payload: &[u8]) -> StorageResult<()> {
120 self.write_object_with_flags(path, payload, DNT_FLAG_PAYLOAD_DEFLATE)
121 }
122
123 fn write_object_with_flags(&self, path: &str, payload: &[u8], flags: u32) -> StorageResult<()> {
124 let mut options = self.policy.seal_options(now_ms(), Vec::new());
125 options.flags = flags;
126 let envelope =
127 seal(payload, self.key_provider, &self.codec, options).map_err(map_dnt_error)?;
128 verify(
129 &envelope,
130 self.key_provider,
131 &self.codec,
132 &self.policy.open_options(),
133 )
134 .map_err(map_dnt_error)?;
135 self.provider.write_bytes_atomic(path, &envelope)
136 }
137}
138
139impl<K, C> SealedObjectStore for DntFileObjectStore<'_, K, C>
140where
141 K: DntKeyProvider,
142 C: DntCodec,
143{
144 fn write_object(&self, path: &str, payload: &[u8]) -> StorageResult<()> {
145 self.write_object_with_flags(path, payload, 0)
146 }
147
148 fn read_object(&self, path: &str) -> StorageResult<Vec<u8>> {
149 let options = self.policy.open_options();
150 let max_envelope_bytes = options.max_envelope_bytes().map_err(map_dnt_error)?;
151 let bytes = self.provider.read_bytes_bounded(path, max_envelope_bytes)?;
152 let opened =
153 open_owned(bytes, self.key_provider, &self.codec, &options).map_err(map_dnt_error)?;
154 Ok(opened.payload)
155 }
156
157 fn verify_object(&self, path: &str) -> StorageResult<()> {
158 let options = self.policy.open_options();
159 let max_envelope_bytes = options.max_envelope_bytes().map_err(map_dnt_error)?;
160 let bytes = self.provider.read_bytes_bounded(path, max_envelope_bytes)?;
161 verify(&bytes, self.key_provider, &self.codec, &options).map_err(map_dnt_error)?;
162 Ok(())
163 }
164
165 fn rekey_object(&self, path: &str, new_key_id: KeyId) -> StorageResult<()> {
166 let options = self.policy.open_options();
167 let max_envelope_bytes = options.max_envelope_bytes().map_err(map_dnt_error)?;
168 let bytes = self.provider.read_bytes_bounded(path, max_envelope_bytes)?;
169 inspect_header(&bytes).map_err(map_dnt_error)?;
170 let rotated = rekey(&bytes, self.key_provider, &self.codec, &options, new_key_id)
171 .map_err(map_dnt_error)?;
172 verify(&rotated, self.key_provider, &self.codec, &options).map_err(map_dnt_error)?;
173 self.provider.write_bytes_atomic(path, &rotated)
174 }
175}
176
177#[derive(Debug)]
179pub struct DntFileSnapshotStore<'a, K, C>(DntFileObjectStore<'a, K, C>);
180
181impl<'a, K, C> DntFileSnapshotStore<'a, K, C>
182where
183 K: DntKeyProvider,
184 C: DntCodec,
185{
186 pub fn new(inner: DntFileObjectStore<'a, K, C>) -> Self {
188 Self(inner)
189 }
190}
191
192impl<K, C> SealedObjectStore for DntFileSnapshotStore<'_, K, C>
193where
194 K: DntKeyProvider,
195 C: DntCodec,
196{
197 fn write_object(&self, path: &str, payload: &[u8]) -> StorageResult<()> {
198 self.0.write_object(path, payload)
199 }
200
201 fn read_object(&self, path: &str) -> StorageResult<Vec<u8>> {
202 self.0.read_object(path)
203 }
204
205 fn verify_object(&self, path: &str) -> StorageResult<()> {
206 self.0.verify_object(path)
207 }
208
209 fn rekey_object(&self, path: &str, new_key_id: KeyId) -> StorageResult<()> {
210 self.0.rekey_object(path, new_key_id)
211 }
212}
213
214impl<K, C> SealedSnapshotStore for DntFileSnapshotStore<'_, K, C>
215where
216 K: DntKeyProvider,
217 C: DntCodec,
218{
219}
220
221#[derive(Debug)]
223pub struct DntFileSecretStore<'a, K, C>(DntFileObjectStore<'a, K, C>);
224
225impl<'a, K, C> DntFileSecretStore<'a, K, C>
226where
227 K: DntKeyProvider,
228 C: DntCodec,
229{
230 pub fn new(inner: DntFileObjectStore<'a, K, C>) -> Self {
232 Self(inner)
233 }
234}
235
236impl<K, C> SealedObjectStore for DntFileSecretStore<'_, K, C>
237where
238 K: DntKeyProvider,
239 C: DntCodec,
240{
241 fn write_object(&self, path: &str, payload: &[u8]) -> StorageResult<()> {
242 self.0.write_object(path, payload)
243 }
244
245 fn read_object(&self, path: &str) -> StorageResult<Vec<u8>> {
246 self.0.read_object(path)
247 }
248
249 fn verify_object(&self, path: &str) -> StorageResult<()> {
250 self.0.verify_object(path)
251 }
252
253 fn rekey_object(&self, path: &str, new_key_id: KeyId) -> StorageResult<()> {
254 self.0.rekey_object(path, new_key_id)
255 }
256}
257
258impl<K, C> SealedSecretStore for DntFileSecretStore<'_, K, C>
259where
260 K: DntKeyProvider,
261 C: DntCodec,
262{
263}
264
265fn now_ms() -> u64 {
266 std::time::SystemTime::now()
267 .duration_since(std::time::UNIX_EPOCH)
268 .map(|duration| duration.as_millis() as u64)
269 .unwrap_or(0)
270}
271
272fn map_dnt_error(error: appcore_dnt::DntError) -> StorageError {
273 match error {
274 appcore_dnt::DntError::Io => StorageError::TransactionFailed("dnt".to_string()),
275 appcore_dnt::DntError::KeyUnavailable
276 | appcore_dnt::DntError::AuthenticationFailed
277 | appcore_dnt::DntError::ContextMismatch => StorageError::SecurityFailed("dnt".to_string()),
278 _ => StorageError::InvalidPath("dnt".to_string()),
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use appcore_dnt::{
286 BytesCodec, SecretKey, StaticDntKeyProvider, DNT_CONTENT_OCTET_STREAM, DNT_CONTENT_SECRET,
287 };
288
289 fn provider() -> StaticDntKeyProvider {
290 StaticDntKeyProvider::new()
291 .with_key(KeyId::new("key-a").unwrap(), SecretKey::new([3; 32]))
292 .with_key(KeyId::new("key-b").unwrap(), SecretKey::new([4; 32]))
293 }
294
295 fn storage() -> FileStorageProvider {
296 let root = std::env::temp_dir().join(format!("appcore-storage-dnt-{}", unique()));
297 let data = root.join("data");
298 let backups = root.join("backups");
299 let provider = FileStorageProvider::new(data, backups);
300 provider.create_dirs().unwrap();
301 provider
302 }
303
304 fn policy(content_type: &str) -> SealedStoragePolicy {
305 SealedStoragePolicy {
306 application_id: ApplicationId::new("app-a").unwrap(),
307 tenant_id: Some(TenantId::new("tenant-a").unwrap()),
308 content_type: ContentType::new(content_type).unwrap(),
309 schema_version: 1,
310 key_id: KeyId::new("key-a").unwrap(),
311 max_payload_bytes: 1024 * 1024,
312 public_metadata: b"store=sealed".to_vec(),
313 }
314 }
315
316 #[test]
317 fn sealed_object_store_keeps_plaintext_off_disk() {
318 let storage = storage();
319 let keys = provider();
320 let store = DntFileObjectStore::new(
321 &storage,
322 &keys,
323 BytesCodec,
324 policy(DNT_CONTENT_OCTET_STREAM),
325 );
326
327 store
328 .write_object("objects/a.dntb", b"secret payload")
329 .unwrap();
330 let raw = storage.read_bytes("objects/a.dntb").unwrap();
331
332 assert!(!raw
333 .windows(b"secret payload".len())
334 .any(|w| w == b"secret payload"));
335 assert_eq!(
336 store.read_object("objects/a.dntb").unwrap(),
337 b"secret payload"
338 );
339 assert_eq!(store.verify_object("objects/a.dntb"), Ok(()));
340 }
341
342 #[test]
343 fn sealed_object_store_can_write_compact_dnt() {
344 let storage = storage();
345 let keys = provider();
346 let store = DntFileObjectStore::new(
347 &storage,
348 &keys,
349 BytesCodec,
350 policy(DNT_CONTENT_OCTET_STREAM),
351 );
352 let payload = b"snapshot-line=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n".repeat(2048);
353
354 store.write_object("objects/normal.dntb", &payload).unwrap();
355 store
356 .write_object_compact("objects/compact.dntb", &payload)
357 .unwrap();
358 let normal = storage.read_bytes("objects/normal.dntb").unwrap();
359 let compact = storage.read_bytes("objects/compact.dntb").unwrap();
360
361 assert!(compact.len() < normal.len() / 10);
362 assert_eq!(store.read_object("objects/compact.dntb").unwrap(), payload);
363 assert_eq!(store.verify_object("objects/compact.dntb"), Ok(()));
364 }
365
366 #[test]
367 fn sealed_object_rekey_preserves_payload() {
368 let storage = storage();
369 let keys = provider();
370 let store = DntFileObjectStore::new(
371 &storage,
372 &keys,
373 BytesCodec,
374 policy(DNT_CONTENT_OCTET_STREAM),
375 );
376
377 store.write_object("objects/a.dntb", b"payload").unwrap();
378 store
379 .rekey_object("objects/a.dntb", KeyId::new("key-b").unwrap())
380 .unwrap();
381
382 let raw = storage.read_bytes("objects/a.dntb").unwrap();
383 assert_eq!(inspect_header(&raw).unwrap().key_id.as_str(), "key-b");
384 assert_eq!(store.read_object("objects/a.dntb").unwrap(), b"payload");
385 }
386
387 #[test]
388 fn sealed_object_rejects_oversized_envelope_before_opening() {
389 let storage = storage();
390 let keys = provider();
391 let policy = policy(DNT_CONTENT_OCTET_STREAM);
392 let max_envelope = policy.open_options().max_envelope_bytes().unwrap();
393 let store = DntFileObjectStore::new(&storage, &keys, BytesCodec, policy);
394 storage
395 .write_bytes_atomic(
396 "objects/oversized.dntb",
397 &vec![0; max_envelope as usize + 1],
398 )
399 .unwrap();
400
401 assert!(matches!(
402 store.read_object("objects/oversized.dntb"),
403 Err(StorageError::TransactionFailed(_))
404 ));
405 assert!(matches!(
406 store.verify_object("objects/oversized.dntb"),
407 Err(StorageError::TransactionFailed(_))
408 ));
409 }
410
411 #[test]
412 fn sealed_secret_store_rejects_wrong_content_type() {
413 let storage = storage();
414 let keys = provider();
415 let object_store = DntFileObjectStore::new(
416 &storage,
417 &keys,
418 BytesCodec,
419 policy(DNT_CONTENT_OCTET_STREAM),
420 );
421 object_store
422 .write_object("secrets/a.dnt", b"payload")
423 .unwrap();
424
425 let secret_store =
426 DntFileObjectStore::new(&storage, &keys, BytesCodec, policy(DNT_CONTENT_SECRET));
427 assert!(matches!(
428 secret_store.read_object("secrets/a.dnt"),
429 Err(StorageError::SecurityFailed(_))
430 ));
431 }
432
433 fn unique() -> u64 {
434 static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
436 let count = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
437 ((std::process::id() as u64) << 32) | count
438 }
439}