@@ -1014,4 +1014,162 @@ mod tests {
10141014 assert_eq ! ( binding. content_hash, "hash" ) ;
10151015 assert_eq ! ( binding. size, 512 ) ;
10161016 }
1017+
1018+ /// Test that simulates the exact FxFiles → Web UI share flow:
1019+ /// 1. Generate random bytes (like Dart's X25519().newKeyPair().extractPrivateKeyBytes())
1020+ /// 2. Create public key from those bytes (Rust derives public key)
1021+ /// 3. Create share token with that public key
1022+ /// 4. Serialize to JSON, transmit, deserialize (simulating URL transmission)
1023+ /// 5. Accept share using SecretKey from same bytes
1024+ /// 6. Verify DEK matches
1025+ /// 7. Use DEK to decrypt test content
1026+ #[ test]
1027+ fn test_fxfiles_to_webui_share_flow ( ) {
1028+ use crate :: symmetric:: { Aead , Nonce } ;
1029+
1030+ // === STEP 1: FxFiles side - generate disposable keypair ===
1031+ // This simulates: final x25519 = X25519(); final keyPair = await x25519.newKeyPair();
1032+ let mut disposable_secret_bytes = [ 0u8 ; 32 ] ;
1033+ rand:: RngCore :: fill_bytes ( & mut rand:: rngs:: OsRng , & mut disposable_secret_bytes) ;
1034+
1035+ // === STEP 2: Create SecretKey and derive PublicKey ===
1036+ // This simulates: final publicKeyBytes = Uint8List.fromList(publicKeyData.bytes);
1037+ let disposable_secret = SecretKey :: from_bytes ( & disposable_secret_bytes) . unwrap ( ) ;
1038+ let disposable_public = disposable_secret. public_key ( ) ;
1039+
1040+ println ! ( "Disposable secret key: {:?}" , hex:: encode( & disposable_secret_bytes) ) ;
1041+ println ! ( "Derived public key: {:?}" , hex:: encode( disposable_public. as_bytes( ) ) ) ;
1042+
1043+ // === STEP 3: Owner creates share token ===
1044+ // Original file was encrypted with this DEK
1045+ let owner_keypair = KekKeyPair :: generate ( ) ;
1046+ let original_dek = DekKey :: generate ( ) ;
1047+
1048+ // Encrypt some test content
1049+ let plaintext = b"This is a test JPEG file. Should start with FF D8 FF in real case." ;
1050+ let nonce = Nonce :: generate ( ) ;
1051+ let aead = Aead :: new_default ( & original_dek) ;
1052+ let ciphertext = aead. encrypt ( & nonce, plaintext) . unwrap ( ) ;
1053+
1054+ // Create share token with disposable public key
1055+ let token = ShareBuilder :: new ( & owner_keypair, & disposable_public, & original_dek)
1056+ . path_scope ( "/test/file.jpg" )
1057+ . build ( )
1058+ . unwrap ( ) ;
1059+
1060+ // === STEP 4: Serialize token for transmission ===
1061+ // This simulates: base64Encode(jsonEncode(token))
1062+ let token_json = serde_json:: to_string ( & token) . unwrap ( ) ;
1063+ println ! ( "Share token JSON length: {}" , token_json. len( ) ) ;
1064+
1065+ // === STEP 5: Web UI side - deserialize token ===
1066+ // This simulates: jsonDecode(base64Decode(payload.t))
1067+ let received_token: ShareToken = serde_json:: from_str ( & token_json) . unwrap ( ) ;
1068+
1069+ // === STEP 6: Web UI creates SecretKey from URL's sk bytes ===
1070+ // This simulates: createEncryptedClient({ secretKey: base64Decode(payload.sk) })
1071+ let web_secret = SecretKey :: from_bytes ( & disposable_secret_bytes) . unwrap ( ) ;
1072+ let web_derived_public = web_secret. public_key ( ) ;
1073+
1074+ println ! ( "Web derived public key: {:?}" , hex:: encode( web_derived_public. as_bytes( ) ) ) ;
1075+
1076+ // CRITICAL CHECK: The derived public key must match!
1077+ assert_eq ! (
1078+ disposable_public. as_bytes( ) ,
1079+ web_derived_public. as_bytes( ) ,
1080+ "Public key derived on web side must match the one used for token creation"
1081+ ) ;
1082+
1083+ // === STEP 7: Accept the share ===
1084+ let recipient = ShareRecipient :: from_secret_key ( web_secret) ;
1085+ let accepted = recipient. accept_share ( & received_token) . unwrap ( ) ;
1086+
1087+ println ! ( "Original DEK first 4 bytes: {:02x}{:02x}{:02x}{:02x}" ,
1088+ original_dek. as_bytes( ) [ 0 ] , original_dek. as_bytes( ) [ 1 ] ,
1089+ original_dek. as_bytes( ) [ 2 ] , original_dek. as_bytes( ) [ 3 ] ) ;
1090+ println ! ( "Accepted DEK first 4 bytes: {:02x}{:02x}{:02x}{:02x}" ,
1091+ accepted. dek. as_bytes( ) [ 0 ] , accepted. dek. as_bytes( ) [ 1 ] ,
1092+ accepted. dek. as_bytes( ) [ 2 ] , accepted. dek. as_bytes( ) [ 3 ] ) ;
1093+
1094+ // CRITICAL CHECK: DEK must match!
1095+ assert_eq ! (
1096+ original_dek. as_bytes( ) ,
1097+ accepted. dek. as_bytes( ) ,
1098+ "Decrypted DEK must match the original DEK"
1099+ ) ;
1100+
1101+ // === STEP 8: Decrypt the content ===
1102+ let recipient_aead = Aead :: new_default ( & accepted. dek ) ;
1103+ let decrypted = recipient_aead. decrypt ( & nonce, & ciphertext) . unwrap ( ) ;
1104+
1105+ assert_eq ! (
1106+ plaintext. as_slice( ) ,
1107+ decrypted. as_slice( ) ,
1108+ "Decrypted content must match original plaintext"
1109+ ) ;
1110+
1111+ println ! ( "SUCCESS: Full share flow works correctly!" ) ;
1112+ }
1113+
1114+ /// Test share with SecretKey bytes that look like what Dart might produce
1115+ /// (testing various edge cases in key format)
1116+ #[ test]
1117+ fn test_share_with_various_key_formats ( ) {
1118+ // Test with bytes that have various patterns
1119+ let test_cases: Vec < [ u8 ; 32 ] > = vec ! [
1120+ // All zeros (will be clamped)
1121+ [ 0u8 ; 32 ] ,
1122+ // All ones (will be clamped)
1123+ [ 0xFFu8 ; 32 ] ,
1124+ // Sequential bytes
1125+ {
1126+ let mut arr = [ 0u8 ; 32 ] ;
1127+ for ( i, b) in arr. iter_mut( ) . enumerate( ) {
1128+ * b = i as u8 ;
1129+ }
1130+ arr
1131+ } ,
1132+ // Random bytes (typical case)
1133+ {
1134+ let mut arr = [ 0u8 ; 32 ] ;
1135+ rand:: RngCore :: fill_bytes( & mut rand:: rngs:: OsRng , & mut arr) ;
1136+ arr
1137+ } ,
1138+ ] ;
1139+
1140+ for ( i, secret_bytes) in test_cases. iter ( ) . enumerate ( ) {
1141+ println ! ( "\n === Test case {} ===" , i) ;
1142+
1143+ let owner = KekKeyPair :: generate ( ) ;
1144+ let dek = DekKey :: generate ( ) ;
1145+
1146+ // Create disposable keypair from raw bytes
1147+ let disposable_secret = SecretKey :: from_bytes ( secret_bytes) . unwrap ( ) ;
1148+ let disposable_public = disposable_secret. public_key ( ) ;
1149+
1150+ // Create share token
1151+ let token = ShareBuilder :: new ( & owner, & disposable_public, & dek)
1152+ . path_scope ( "/test" )
1153+ . build ( )
1154+ . unwrap ( ) ;
1155+
1156+ // Serialize and deserialize (round-trip)
1157+ let json = serde_json:: to_string ( & token) . unwrap ( ) ;
1158+ let restored_token: ShareToken = serde_json:: from_str ( & json) . unwrap ( ) ;
1159+
1160+ // Accept share using same secret bytes
1161+ let recipient_secret = SecretKey :: from_bytes ( secret_bytes) . unwrap ( ) ;
1162+ let recipient = ShareRecipient :: from_secret_key ( recipient_secret) ;
1163+ let accepted = recipient. accept_share ( & restored_token) . unwrap ( ) ;
1164+
1165+ // DEK must match
1166+ assert_eq ! (
1167+ dek. as_bytes( ) ,
1168+ accepted. dek. as_bytes( ) ,
1169+ "Test case {}: DEK mismatch" , i
1170+ ) ;
1171+
1172+ println ! ( "Test case {}: PASSED" , i) ;
1173+ }
1174+ }
10171175}
0 commit comments