Skip to content

Commit c6f3052

Browse files
committed
full implementation of sharing and key rotation
1 parent 11d1c50 commit c6f3052

4 files changed

Lines changed: 743 additions & 55 deletions

File tree

crates/fula-client/src/encryption.rs

Lines changed: 317 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
};
1820
use std::sync::{Arc, RwLock};
1921
use 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)]
10981413
mod tests {
10991414
use super::*;

0 commit comments

Comments
 (0)