Skip to content

Commit 0600854

Browse files
committed
added metadata encryption
1 parent 78497ca commit 0600854

File tree

8 files changed

+1024
-17
lines changed

8 files changed

+1024
-17
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ cargo run --example security_verification
233233
# Sharing Demo
234234
cargo run --example sharing_demo
235235

236+
# Metadata Privacy
237+
cargo run --example metadata_privacy
238+
236239
```
237240

238241
## Security

crates/fula-client/src/encryption.rs

Lines changed: 243 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,77 @@
11
//! Client-side encryption support
2+
//!
3+
//! Provides end-to-end encryption for Fula storage including:
4+
//! - Content encryption (AES-256-GCM)
5+
//! - Key wrapping (HPKE)
6+
//! - Metadata privacy (file names, sizes, timestamps)
27
38
use crate::{ClientError, FulaClient, Result, Config};
49
use crate::types::*;
510
use bytes::Bytes;
611
use fula_crypto::{
7-
keys::{KeyManager},
12+
keys::KeyManager,
813
hpke::{Encryptor, Decryptor, EncryptedData},
914
symmetric::{Aead, Nonce},
15+
private_metadata::{PrivateMetadata, EncryptedPrivateMetadata, KeyObfuscation, obfuscate_key},
1016
};
1117
use std::sync::Arc;
18+
use std::collections::HashMap;
1219

1320
/// Configuration for client-side encryption
1421
pub struct EncryptionConfig {
1522
/// Key manager for encryption keys (wrapped in Arc for sharing)
1623
key_manager: Arc<KeyManager>,
24+
/// Whether to enable metadata privacy (file name obfuscation)
25+
metadata_privacy: bool,
26+
/// Key obfuscation mode
27+
obfuscation_mode: KeyObfuscation,
1728
}
1829

1930
impl EncryptionConfig {
20-
/// Create with a new random key
31+
/// Create with a new random key (metadata privacy enabled by default)
2132
pub fn new() -> Self {
2233
Self {
2334
key_manager: Arc::new(KeyManager::new()),
35+
metadata_privacy: true,
36+
obfuscation_mode: KeyObfuscation::DeterministicHash,
37+
}
38+
}
39+
40+
/// Create without metadata privacy (filenames visible to server)
41+
pub fn new_without_privacy() -> Self {
42+
Self {
43+
key_manager: Arc::new(KeyManager::new()),
44+
metadata_privacy: false,
45+
obfuscation_mode: KeyObfuscation::DeterministicHash,
2446
}
2547
}
2648

2749
/// Create from an existing secret key
2850
pub fn from_secret_key(secret: fula_crypto::keys::SecretKey) -> Self {
2951
Self {
3052
key_manager: Arc::new(KeyManager::from_secret_key(secret)),
53+
metadata_privacy: true,
54+
obfuscation_mode: KeyObfuscation::DeterministicHash,
3155
}
3256
}
3357

58+
/// Enable or disable metadata privacy
59+
pub fn with_metadata_privacy(mut self, enabled: bool) -> Self {
60+
self.metadata_privacy = enabled;
61+
self
62+
}
63+
64+
/// Set the key obfuscation mode
65+
pub fn with_obfuscation_mode(mut self, mode: KeyObfuscation) -> Self {
66+
self.obfuscation_mode = mode;
67+
self
68+
}
69+
70+
/// Check if metadata privacy is enabled
71+
pub fn has_metadata_privacy(&self) -> bool {
72+
self.metadata_privacy
73+
}
74+
3475
/// Get the public key for sharing
3576
pub fn public_key(&self) -> &fula_crypto::keys::PublicKey {
3677
self.key_manager.public_key()
@@ -40,6 +81,11 @@ impl EncryptionConfig {
4081
pub fn export_secret_key(&self) -> &fula_crypto::keys::SecretKey {
4182
self.key_manager.keypair().secret_key()
4283
}
84+
85+
/// Get the key manager
86+
pub fn key_manager(&self) -> &KeyManager {
87+
&self.key_manager
88+
}
4389
}
4490

4591
impl Default for EncryptionConfig {
@@ -71,14 +117,16 @@ impl EncryptedClient {
71117
&self.encryption
72118
}
73119

74-
/// Put an encrypted object
75-
pub async fn put_object_encrypted(
120+
/// Put an encrypted object with optional content type
121+
pub async fn put_object_encrypted_with_type(
76122
&self,
77123
bucket: &str,
78124
key: &str,
79125
data: impl Into<Bytes>,
126+
content_type: Option<&str>,
80127
) -> Result<PutObjectResult> {
81128
let data = data.into();
129+
let original_size = data.len() as u64;
82130

83131
// Generate a DEK for this object
84132
let dek = self.encryption.key_manager.generate_dek();
@@ -94,35 +142,102 @@ impl EncryptedClient {
94142
let wrapped_dek = encryptor.encrypt_dek(&dek)
95143
.map_err(ClientError::Encryption)?;
96144

145+
// Determine the storage key and metadata based on privacy settings
146+
let (storage_key, private_metadata_json) = if self.encryption.metadata_privacy {
147+
// Create private metadata with original info
148+
let private_meta = PrivateMetadata::new(key, original_size)
149+
.with_content_type(content_type.unwrap_or("application/octet-stream"));
150+
151+
// Encrypt private metadata with the per-file DEK
152+
let encrypted_meta = EncryptedPrivateMetadata::encrypt(&private_meta, &dek)
153+
.map_err(ClientError::Encryption)?;
154+
155+
// Generate obfuscated storage key using PATH-DERIVED DEK (not per-file DEK)
156+
// This ensures we can compute the same storage key later for retrieval
157+
let path_dek = self.encryption.key_manager.derive_path_key(key);
158+
let storage_key = obfuscate_key(key, &path_dek, self.encryption.obfuscation_mode.clone());
159+
160+
(storage_key, Some(encrypted_meta.to_json().map_err(ClientError::Encryption)?))
161+
} else {
162+
(key.to_string(), None)
163+
};
164+
97165
// Serialize encryption metadata
98-
let enc_metadata = serde_json::json!({
99-
"version": 1,
166+
let mut enc_metadata = serde_json::json!({
167+
"version": 2,
100168
"algorithm": "AES-256-GCM",
101169
"nonce": base64::Engine::encode(&base64::engine::general_purpose::STANDARD, nonce.as_bytes()),
102170
"wrapped_key": serde_json::to_value(&wrapped_dek).unwrap(),
171+
"metadata_privacy": self.encryption.metadata_privacy,
103172
});
104173

105-
// Upload with encryption metadata
174+
// Add encrypted private metadata if enabled
175+
if let Some(private_meta) = private_metadata_json {
176+
enc_metadata["private_metadata"] = serde_json::Value::String(private_meta);
177+
}
178+
179+
// Upload with encryption metadata (server sees obfuscated key)
106180
let metadata = ObjectMetadata::new()
107-
.with_content_type("application/octet-stream")
181+
.with_content_type("application/octet-stream") // Server always sees generic type
108182
.with_metadata("x-fula-encrypted", "true")
109183
.with_metadata("x-fula-encryption", &enc_metadata.to_string());
110184

111185
self.inner.put_object_with_metadata(
112186
bucket,
113-
key,
187+
&storage_key,
114188
Bytes::from(ciphertext),
115189
Some(metadata),
116190
).await
117191
}
118192

119-
/// Get and decrypt an object
193+
/// Put an encrypted object (convenience method)
194+
pub async fn put_object_encrypted(
195+
&self,
196+
bucket: &str,
197+
key: &str,
198+
data: impl Into<Bytes>,
199+
) -> Result<PutObjectResult> {
200+
self.put_object_encrypted_with_type(bucket, key, data, None).await
201+
}
202+
203+
/// Get and decrypt an object using the original key
204+
///
205+
/// If metadata privacy is enabled, this will automatically compute the
206+
/// storage key from the original key using deterministic hashing.
120207
pub async fn get_object_decrypted(
121208
&self,
122209
bucket: &str,
123210
key: &str,
124211
) -> Result<Bytes> {
125-
let result = self.inner.get_object_with_metadata(bucket, key).await?;
212+
// For metadata privacy, we need to find the storage key
213+
// Since we use deterministic hashing, we can compute it
214+
// But we need the DEK first, which creates a chicken-and-egg problem
215+
//
216+
// Solution: If metadata privacy is enabled, we use the path-derived DEK
217+
// to compute the storage key, fetch the object, then use the wrapped DEK
218+
// to decrypt the actual data.
219+
220+
let storage_key = if self.encryption.metadata_privacy {
221+
// Use a path-derived DEK for key obfuscation lookup
222+
let path_dek = self.encryption.key_manager.derive_path_key(key);
223+
obfuscate_key(key, &path_dek, self.encryption.obfuscation_mode.clone())
224+
} else {
225+
key.to_string()
226+
};
227+
228+
self.get_object_decrypted_by_storage_key(bucket, &storage_key).await
229+
}
230+
231+
/// Get and decrypt an object using the storage key directly
232+
///
233+
/// Use this when you already have the obfuscated storage key
234+
/// (e.g., from list_objects_decrypted)
235+
pub async fn get_object_decrypted_by_storage_key(
236+
&self,
237+
bucket: &str,
238+
storage_key: &str,
239+
) -> Result<Bytes> {
240+
let result = self.inner.get_object_with_metadata(bucket, storage_key).await?;
126241

127242
// Check if object is encrypted
128243
let is_encrypted = result.metadata
@@ -179,6 +294,94 @@ impl EncryptedClient {
179294
Ok(Bytes::from(plaintext))
180295
}
181296

297+
/// Decrypted object info with private metadata
298+
pub async fn get_object_with_private_metadata(
299+
&self,
300+
bucket: &str,
301+
storage_key: &str,
302+
) -> Result<DecryptedObjectInfo> {
303+
let result = self.inner.get_object_with_metadata(bucket, storage_key).await?;
304+
305+
let is_encrypted = result.metadata
306+
.get("x-fula-encrypted")
307+
.map(|v| v == "true")
308+
.unwrap_or(false);
309+
310+
if !is_encrypted {
311+
let size = result.data.len() as u64;
312+
return Ok(DecryptedObjectInfo {
313+
data: result.data,
314+
original_key: storage_key.to_string(),
315+
original_size: size,
316+
content_type: result.metadata.get("content-type").cloned(),
317+
user_metadata: HashMap::new(),
318+
});
319+
}
320+
321+
let enc_metadata_str = result.metadata
322+
.get("x-fula-encryption")
323+
.ok_or_else(|| ClientError::Encryption(
324+
fula_crypto::CryptoError::Decryption("Missing encryption metadata".to_string())
325+
))?;
326+
327+
let enc_metadata: serde_json::Value = serde_json::from_str(enc_metadata_str)
328+
.map_err(|e| ClientError::Encryption(
329+
fula_crypto::CryptoError::Decryption(e.to_string())
330+
))?;
331+
332+
// Unwrap the DEK
333+
let wrapped_key: EncryptedData = serde_json::from_value(
334+
enc_metadata["wrapped_key"].clone()
335+
).map_err(|e| ClientError::Encryption(
336+
fula_crypto::CryptoError::Decryption(e.to_string())
337+
))?;
338+
339+
let decryptor = Decryptor::new(self.encryption.key_manager.keypair());
340+
let dek = decryptor.decrypt_dek(&wrapped_key)
341+
.map_err(ClientError::Encryption)?;
342+
343+
// Decrypt data
344+
let nonce_b64 = enc_metadata["nonce"].as_str().unwrap();
345+
let nonce_bytes = base64::Engine::decode(
346+
&base64::engine::general_purpose::STANDARD,
347+
nonce_b64,
348+
).map_err(|e| ClientError::Encryption(
349+
fula_crypto::CryptoError::Decryption(e.to_string())
350+
))?;
351+
let nonce = Nonce::from_bytes(&nonce_bytes)
352+
.map_err(ClientError::Encryption)?;
353+
354+
let aead = Aead::new_default(&dek);
355+
let plaintext = aead.decrypt(&nonce, &result.data)
356+
.map_err(ClientError::Encryption)?;
357+
358+
// Decrypt private metadata if present
359+
let (original_key, original_size, content_type, user_metadata) =
360+
if let Some(private_meta_str) = enc_metadata["private_metadata"].as_str() {
361+
let encrypted_meta = EncryptedPrivateMetadata::from_json(private_meta_str)
362+
.map_err(ClientError::Encryption)?;
363+
let private_meta = encrypted_meta.decrypt(&dek)
364+
.map_err(ClientError::Encryption)?;
365+
366+
(
367+
private_meta.original_key,
368+
private_meta.actual_size,
369+
private_meta.content_type,
370+
private_meta.user_metadata,
371+
)
372+
} else {
373+
(storage_key.to_string(), plaintext.len() as u64, None, HashMap::new())
374+
};
375+
376+
Ok(DecryptedObjectInfo {
377+
data: Bytes::from(plaintext),
378+
original_key,
379+
original_size,
380+
content_type,
381+
user_metadata,
382+
})
383+
}
384+
182385
// Delegate non-encrypted operations to inner client
183386

184387
/// List buckets
@@ -196,7 +399,7 @@ impl EncryptedClient {
196399
self.inner.delete_bucket(bucket).await
197400
}
198401

199-
/// List objects
402+
/// List objects (returns obfuscated keys if metadata privacy is enabled)
200403
pub async fn list_objects(
201404
&self,
202405
bucket: &str,
@@ -205,12 +408,38 @@ impl EncryptedClient {
205408
self.inner.list_objects(bucket, options).await
206409
}
207410

208-
/// Delete object
411+
/// Delete object using original key
209412
pub async fn delete_object(&self, bucket: &str, key: &str) -> Result<()> {
210-
self.inner.delete_object(bucket, key).await
413+
let storage_key = if self.encryption.metadata_privacy {
414+
let path_dek = self.encryption.key_manager.derive_path_key(key);
415+
obfuscate_key(key, &path_dek, self.encryption.obfuscation_mode.clone())
416+
} else {
417+
key.to_string()
418+
};
419+
self.inner.delete_object(bucket, &storage_key).await
420+
}
421+
422+
/// Delete object using storage key directly
423+
pub async fn delete_object_by_storage_key(&self, bucket: &str, storage_key: &str) -> Result<()> {
424+
self.inner.delete_object(bucket, storage_key).await
211425
}
212426
}
213427

428+
/// Decrypted object information including private metadata
429+
#[derive(Debug, Clone)]
430+
pub struct DecryptedObjectInfo {
431+
/// Decrypted file data
432+
pub data: Bytes,
433+
/// Original file name/path (decrypted from private metadata)
434+
pub original_key: String,
435+
/// Original file size (not ciphertext size)
436+
pub original_size: u64,
437+
/// Original content type
438+
pub content_type: Option<String>,
439+
/// User-defined metadata
440+
pub user_metadata: HashMap<String, String>,
441+
}
442+
214443
#[cfg(test)]
215444
mod tests {
216445
use super::*;

crates/fula-client/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ mod types;
4646

4747
pub use client::FulaClient;
4848
pub use config::Config;
49-
pub use encryption::{EncryptedClient, EncryptionConfig};
49+
pub use encryption::{EncryptedClient, EncryptionConfig, DecryptedObjectInfo};
5050
pub use error::{ClientError, Result};
5151
pub use multipart::{MultipartUpload, UploadProgress, ProgressCallback, upload_large_file};
5252
pub use types::*;
53+
54+
// Re-export useful crypto types for encryption configuration
55+
pub use fula_crypto::private_metadata::KeyObfuscation;

0 commit comments

Comments
 (0)