@@ -14,6 +14,8 @@ use fula_crypto::{
1414 symmetric:: { Aead , Nonce } ,
1515 private_metadata:: { PrivateMetadata , EncryptedPrivateMetadata , KeyObfuscation , obfuscate_key} ,
1616 private_forest:: { PrivateForest , EncryptedForest , ForestFileEntry , derive_index_key} ,
17+ sharing:: { ShareToken , AcceptedShare , ShareRecipient } ,
18+ rotation:: { KeyRotationManager , WrappedKeyInfo } ,
1719} ;
1820use std:: sync:: { Arc , RwLock } ;
1921use std:: collections:: HashMap ;
@@ -235,12 +237,14 @@ impl EncryptedClient {
235237 ( key. to_string ( ) , None )
236238 } ;
237239
238- // Serialize encryption metadata
240+ // Serialize encryption metadata with KEK version
241+ let kek_version = self . encryption . key_manager . version ( ) ;
239242 let mut enc_metadata = serde_json:: json!( {
240243 "version" : 2 ,
241244 "algorithm" : "AES-256-GCM" ,
242245 "nonce" : base64:: Engine :: encode( & base64:: engine:: general_purpose:: STANDARD , nonce. as_bytes( ) ) ,
243246 "wrapped_key" : serde_json:: to_value( & wrapped_dek) . unwrap( ) ,
247+ "kek_version" : kek_version,
244248 "metadata_privacy" : self . encryption. metadata_privacy,
245249 } ) ;
246250
@@ -833,12 +837,14 @@ impl EncryptedClient {
833837 let entry = ForestFileEntry :: from_metadata ( & private_meta, storage_key. clone ( ) ) ;
834838 forest. upsert_file ( entry) ;
835839
836- // Serialize encryption metadata
840+ // Serialize encryption metadata with KEK version
841+ let kek_version = self . encryption . key_manager . version ( ) ;
837842 let enc_metadata = serde_json:: json!( {
838843 "version" : 2 ,
839844 "algorithm" : "AES-256-GCM" ,
840845 "nonce" : base64:: Engine :: encode( & base64:: engine:: general_purpose:: STANDARD , nonce. as_bytes( ) ) ,
841846 "wrapped_key" : serde_json:: to_value( & wrapped_dek) . unwrap( ) ,
847+ "kek_version" : kek_version,
842848 "metadata_privacy" : true ,
843849 "obfuscation_mode" : "flat" ,
844850 "private_metadata" : encrypted_meta. to_json( ) . map_err( ClientError :: Encryption ) ?,
@@ -987,6 +993,284 @@ impl EncryptedClient {
987993 let forest = self . load_forest ( bucket) . await ?;
988994 Ok ( forest. extract_subtree ( prefix) )
989995 }
996+
997+ // ═══════════════════════════════════════════════════════════════════════════
998+ // SHARING INTEGRATION
999+ // Read objects using ShareToken - fully wired into gateway flows
1000+ // ═══════════════════════════════════════════════════════════════════════════
1001+
1002+ /// Get and decrypt an object using a ShareToken
1003+ ///
1004+ /// This allows recipients of a share to read encrypted objects without
1005+ /// having the owner's keys. The ShareToken contains a wrapped DEK that
1006+ /// was encrypted for the recipient's public key.
1007+ ///
1008+ /// # Arguments
1009+ /// * `bucket` - The bucket containing the object
1010+ /// * `storage_key` - The storage key of the object
1011+ /// * `accepted_share` - An accepted share containing the DEK
1012+ ///
1013+ /// # Returns
1014+ /// The decrypted object data
1015+ pub async fn get_object_with_share (
1016+ & self ,
1017+ bucket : & str ,
1018+ storage_key : & str ,
1019+ accepted_share : & AcceptedShare ,
1020+ ) -> Result < Bytes > {
1021+ // Validate the share is still valid
1022+ if !accepted_share. is_valid ( ) {
1023+ return Err ( ClientError :: Encryption (
1024+ fula_crypto:: CryptoError :: ShareExpired
1025+ ) ) ;
1026+ }
1027+
1028+ // Validate path scope
1029+ if !accepted_share. is_path_allowed ( storage_key) {
1030+ return Err ( ClientError :: Encryption (
1031+ fula_crypto:: CryptoError :: AccessDenied (
1032+ format ! ( "Path {} is outside share scope {}" , storage_key, accepted_share. path_scope)
1033+ )
1034+ ) ) ;
1035+ }
1036+
1037+ // Check read permission
1038+ if !accepted_share. permissions . can_read {
1039+ return Err ( ClientError :: Encryption (
1040+ fula_crypto:: CryptoError :: AccessDenied ( "Share does not grant read permission" . to_string ( ) )
1041+ ) ) ;
1042+ }
1043+
1044+ // Fetch the object
1045+ let result = self . inner . get_object_with_metadata ( bucket, storage_key) . await ?;
1046+
1047+ // Check if encrypted
1048+ let is_encrypted = result. metadata
1049+ . get ( "x-fula-encrypted" )
1050+ . map ( |v| v == "true" )
1051+ . unwrap_or ( false ) ;
1052+
1053+ if !is_encrypted {
1054+ return Ok ( result. data ) ;
1055+ }
1056+
1057+ // Parse encryption metadata
1058+ let enc_metadata_str = result. metadata
1059+ . get ( "x-fula-encryption" )
1060+ . ok_or_else ( || ClientError :: Encryption (
1061+ fula_crypto:: CryptoError :: Decryption ( "Missing encryption metadata" . to_string ( ) )
1062+ ) ) ?;
1063+
1064+ let enc_metadata: serde_json:: Value = serde_json:: from_str ( enc_metadata_str)
1065+ . map_err ( |e| ClientError :: Encryption (
1066+ fula_crypto:: CryptoError :: Decryption ( e. to_string ( ) )
1067+ ) ) ?;
1068+
1069+ // Extract nonce
1070+ let nonce_b64 = enc_metadata[ "nonce" ] . as_str ( )
1071+ . ok_or_else ( || ClientError :: Encryption (
1072+ fula_crypto:: CryptoError :: Decryption ( "Missing nonce" . to_string ( ) )
1073+ ) ) ?;
1074+ let nonce_bytes = base64:: Engine :: decode (
1075+ & base64:: engine:: general_purpose:: STANDARD ,
1076+ nonce_b64,
1077+ ) . map_err ( |e| ClientError :: Encryption (
1078+ fula_crypto:: CryptoError :: Decryption ( e. to_string ( ) )
1079+ ) ) ?;
1080+ let nonce = Nonce :: from_bytes ( & nonce_bytes)
1081+ . map_err ( ClientError :: Encryption ) ?;
1082+
1083+ // Use the DEK from the accepted share (already decrypted for recipient)
1084+ let aead = Aead :: new_default ( & accepted_share. dek ) ;
1085+ let plaintext = aead. decrypt ( & nonce, & result. data )
1086+ . map_err ( ClientError :: Encryption ) ?;
1087+
1088+ Ok ( Bytes :: from ( plaintext) )
1089+ }
1090+
1091+ /// Accept a ShareToken and get an AcceptedShare for reading objects
1092+ ///
1093+ /// This is a convenience method that combines ShareRecipient::accept_share
1094+ /// with our encryption config's secret key.
1095+ pub fn accept_share ( & self , token : & ShareToken ) -> Result < AcceptedShare > {
1096+ let recipient = ShareRecipient :: new ( self . encryption . key_manager . keypair ( ) ) ;
1097+ recipient. accept_share ( token)
1098+ . map_err ( ClientError :: Encryption )
1099+ }
1100+
1101+ /// Get object using a raw ShareToken (convenience method)
1102+ ///
1103+ /// Combines accept_share + get_object_with_share in one call.
1104+ pub async fn get_object_with_token (
1105+ & self ,
1106+ bucket : & str ,
1107+ storage_key : & str ,
1108+ token : & ShareToken ,
1109+ ) -> Result < Bytes > {
1110+ let accepted = self . accept_share ( token) ?;
1111+ self . get_object_with_share ( bucket, storage_key, & accepted) . await
1112+ }
1113+
1114+ // ═══════════════════════════════════════════════════════════════════════════
1115+ // KEY ROTATION INTEGRATION
1116+ // Re-wrap DEKs without re-encrypting content
1117+ // ═══════════════════════════════════════════════════════════════════════════
1118+
1119+ /// Create a KeyRotationManager from this client's encryption config
1120+ pub fn create_rotation_manager ( & self ) -> KeyRotationManager {
1121+ KeyRotationManager :: new ( self . encryption . key_manager . keypair ( ) . clone ( ) )
1122+ }
1123+
1124+ /// Get the KEK version stored in an object's metadata
1125+ ///
1126+ /// Returns None if the object doesn't have version info (legacy objects).
1127+ pub async fn get_object_kek_version (
1128+ & self ,
1129+ bucket : & str ,
1130+ storage_key : & str ,
1131+ ) -> Result < Option < u32 > > {
1132+ let head_result = self . inner . head_object ( bucket, storage_key) . await ?;
1133+
1134+ let enc_metadata_str = match head_result. metadata . get ( "x-fula-encryption" ) {
1135+ Some ( s) => s,
1136+ None => return Ok ( None ) ,
1137+ } ;
1138+
1139+ let enc_metadata: serde_json:: Value = serde_json:: from_str ( enc_metadata_str)
1140+ . map_err ( |e| ClientError :: Encryption (
1141+ fula_crypto:: CryptoError :: Decryption ( e. to_string ( ) )
1142+ ) ) ?;
1143+
1144+ Ok ( enc_metadata[ "kek_version" ] . as_u64 ( ) . map ( |v| v as u32 ) )
1145+ }
1146+
1147+ /// Re-wrap an object's DEK with the current KEK
1148+ ///
1149+ /// This updates the object's encryption metadata without re-encrypting
1150+ /// the content. Used during key rotation.
1151+ ///
1152+ /// # Arguments
1153+ /// * `bucket` - The bucket containing the object
1154+ /// * `storage_key` - The storage key of the object
1155+ /// * `rotation_manager` - The rotation manager with old and new KEKs
1156+ ///
1157+ /// # Returns
1158+ /// The new KEK version after re-wrapping
1159+ pub async fn rewrap_object_dek (
1160+ & self ,
1161+ bucket : & str ,
1162+ storage_key : & str ,
1163+ rotation_manager : & KeyRotationManager ,
1164+ ) -> Result < u32 > {
1165+ // Get the object with metadata
1166+ let result = self . inner . get_object_with_metadata ( bucket, storage_key) . await ?;
1167+
1168+ let enc_metadata_str = result. metadata
1169+ . get ( "x-fula-encryption" )
1170+ . ok_or_else ( || ClientError :: Encryption (
1171+ fula_crypto:: CryptoError :: Decryption ( "Missing encryption metadata" . to_string ( ) )
1172+ ) ) ?;
1173+
1174+ let mut enc_metadata: serde_json:: Value = serde_json:: from_str ( enc_metadata_str)
1175+ . map_err ( |e| ClientError :: Encryption (
1176+ fula_crypto:: CryptoError :: Decryption ( e. to_string ( ) )
1177+ ) ) ?;
1178+
1179+ // Get the wrapped key and version
1180+ let wrapped_dek: EncryptedData = serde_json:: from_value (
1181+ enc_metadata[ "wrapped_key" ] . clone ( )
1182+ ) . map_err ( |e| ClientError :: Encryption (
1183+ fula_crypto:: CryptoError :: Decryption ( e. to_string ( ) )
1184+ ) ) ?;
1185+
1186+ // Get the KEK version (default to 1 for legacy objects)
1187+ let kek_version = enc_metadata[ "kek_version" ]
1188+ . as_u64 ( )
1189+ . map ( |v| v as u32 )
1190+ . unwrap_or ( 1 ) ;
1191+
1192+ // Create a WrappedKeyInfo for the rotation manager
1193+ let wrapped_info = WrappedKeyInfo {
1194+ wrapped_dek,
1195+ kek_version,
1196+ object_path : storage_key. to_string ( ) ,
1197+ } ;
1198+
1199+ // Unwrap with old KEK and rewrap with new KEK
1200+ let new_wrapped = rotation_manager. rewrap_dek ( & wrapped_info)
1201+ . map_err ( ClientError :: Encryption ) ?;
1202+
1203+ // Update metadata
1204+ enc_metadata[ "wrapped_key" ] = serde_json:: to_value ( & new_wrapped. wrapped_dek )
1205+ . map_err ( |e| ClientError :: Encryption (
1206+ fula_crypto:: CryptoError :: Encryption ( e. to_string ( ) )
1207+ ) ) ?;
1208+ enc_metadata[ "kek_version" ] = serde_json:: Value :: Number (
1209+ new_wrapped. kek_version . into ( )
1210+ ) ;
1211+
1212+ // Re-upload with updated metadata (same ciphertext)
1213+ let metadata = ObjectMetadata :: new ( )
1214+ . with_content_type (
1215+ result. metadata . get ( "content-type" )
1216+ . map ( |s| s. as_str ( ) )
1217+ . unwrap_or ( "application/octet-stream" )
1218+ )
1219+ . with_metadata ( "x-fula-encrypted" , "true" )
1220+ . with_metadata ( "x-fula-encryption" , & enc_metadata. to_string ( ) ) ;
1221+
1222+ self . inner . put_object_with_metadata (
1223+ bucket,
1224+ storage_key,
1225+ result. data ,
1226+ Some ( metadata) ,
1227+ ) . await ?;
1228+
1229+ Ok ( rotation_manager. current_version ( ) )
1230+ }
1231+
1232+ /// Rotate all objects in a bucket to use the new KEK
1233+ ///
1234+ /// Returns the number of objects successfully rotated and any failures.
1235+ pub async fn rotate_bucket (
1236+ & self ,
1237+ bucket : & str ,
1238+ rotation_manager : & KeyRotationManager ,
1239+ ) -> Result < RotationReport > {
1240+ let objects = self . inner . list_objects ( bucket, None ) . await ?;
1241+
1242+ let mut report = RotationReport {
1243+ total : objects. objects . len ( ) ,
1244+ rotated : 0 ,
1245+ skipped : 0 ,
1246+ failed : 0 ,
1247+ failures : Vec :: new ( ) ,
1248+ } ;
1249+
1250+ for obj in objects. objects {
1251+ // Skip forest index
1252+ if obj. key . starts_with ( "Qm" ) && obj. key . len ( ) > 40 {
1253+ // Check if it's the forest index
1254+ let head = self . inner . head_object ( bucket, & obj. key ) . await ;
1255+ if let Ok ( h) = head {
1256+ if h. metadata . get ( "x-fula-forest" ) . is_some ( ) {
1257+ report. skipped += 1 ;
1258+ continue ;
1259+ }
1260+ }
1261+ }
1262+
1263+ match self . rewrap_object_dek ( bucket, & obj. key , rotation_manager) . await {
1264+ Ok ( _) => report. rotated += 1 ,
1265+ Err ( e) => {
1266+ report. failed += 1 ;
1267+ report. failures . push ( ( obj. key , e. to_string ( ) ) ) ;
1268+ }
1269+ }
1270+ }
1271+
1272+ Ok ( report)
1273+ }
9901274}
9911275
9921276/// File metadata (without file content) - optimized for file managers
@@ -1094,6 +1378,37 @@ pub struct DecryptedObjectInfo {
10941378 pub user_metadata : HashMap < String , String > ,
10951379}
10961380
1381+ /// Report from a bucket key rotation operation
1382+ #[ derive( Debug , Clone ) ]
1383+ pub struct RotationReport {
1384+ /// Total number of objects in the bucket
1385+ pub total : usize ,
1386+ /// Number of objects successfully rotated
1387+ pub rotated : usize ,
1388+ /// Number of objects skipped (e.g., forest index)
1389+ pub skipped : usize ,
1390+ /// Number of objects that failed to rotate
1391+ pub failed : usize ,
1392+ /// Details of failed rotations (path, error message)
1393+ pub failures : Vec < ( String , String ) > ,
1394+ }
1395+
1396+ impl RotationReport {
1397+ /// Check if rotation was fully successful
1398+ pub fn is_success ( & self ) -> bool {
1399+ self . failed == 0
1400+ }
1401+
1402+ /// Get success rate as a percentage
1403+ pub fn success_rate ( & self ) -> f64 {
1404+ if self . total == 0 {
1405+ 100.0
1406+ } else {
1407+ ( self . rotated as f64 / ( self . total - self . skipped ) as f64 ) * 100.0
1408+ }
1409+ }
1410+ }
1411+
10971412#[ cfg( test) ]
10981413mod tests {
10991414 use super :: * ;
0 commit comments