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
38use crate :: { ClientError , FulaClient , Result , Config } ;
49use crate :: types:: * ;
510use bytes:: Bytes ;
611use 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} ;
1117use std:: sync:: Arc ;
18+ use std:: collections:: HashMap ;
1219
1320/// Configuration for client-side encryption
1421pub 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
1930impl 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
4591impl 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) ]
215444mod tests {
216445 use super :: * ;
0 commit comments