1+ use std:: time:: Duration ;
2+
13use anyhow:: bail;
24use bc_components:: { EncapsulationScheme , PrivateKeys , PublicKeys , SignatureScheme , ARID } ;
35use bc_envelope:: {
46 prelude:: { CBORCase , CBOR } ,
57 Envelope , EventBehavior , Expression , ExpressionBehavior , Function ,
68} ;
79use bc_xid:: XIDDocument ;
10+ use chrono:: { DateTime , Utc } ;
11+ use dcbor:: Date ;
812use flutter_rust_bridge:: frb;
913use gstp:: { SealedEvent , SealedEventBehavior } ;
1014
1115use crate :: message:: { EnvoyMessage , PassportMessage } ;
1216
1317pub const QUANTUM_LINK : Function = Function :: new_static_named ( "quantumLink" ) ;
18+ pub const EXPIRATION_DURATION : Duration = Duration :: from_secs ( 60 ) ;
19+
20+ /// Storage for tracking received ARIDs to prevent replay attacks
21+ #[ derive( Debug , Default , Clone ) ]
22+ pub struct ARIDCache {
23+ cache : Vec < ( ARID , DateTime < Utc > ) > ,
24+ }
25+
26+ impl ARIDCache {
27+ pub fn new ( ) -> Self {
28+ Self { cache : Vec :: new ( ) }
29+ }
30+
31+ /// Check if ARID has been seen before and store it if not
32+ /// Returns true if this is a replay attack (ARID already exists)
33+ pub fn check_and_store (
34+ & mut self ,
35+ arid : & ARID ,
36+ sent_at : DateTime < Utc > ,
37+ now : DateTime < Utc > ,
38+ ) -> bool {
39+ // Clean up expired entries first
40+ self . cache
41+ . retain ( |( _, date) | now < ( * date + EXPIRATION_DURATION ) ) ;
42+
43+ // Check if ARID already exists (replay attack)
44+ if self . cache . iter ( ) . any ( |( id, _) | id == arid) {
45+ return true ; // Replay attack detected
46+ }
47+
48+ if now < ( sent_at + EXPIRATION_DURATION ) {
49+ self . cache . push ( ( arid. clone ( ) , sent_at) ) ;
50+ }
51+ false // Not a replay attack
52+ }
53+
54+ /// Get the number of stored ARIDs
55+ pub fn len ( & self ) -> usize {
56+ self . cache . len ( )
57+ }
58+
59+ pub fn is_empty ( & self ) -> bool {
60+ self . cache . len ( ) == 0
61+ }
62+
63+ /// Clear all stored ARIDs
64+ pub fn clear ( & mut self ) {
65+ self . cache . clear ( ) ;
66+ }
67+ }
68+
1469pub trait QuantumLink < C > : minicbor:: Encode < C > {
1570 fn encode ( & self ) -> Expression
1671 where
@@ -46,11 +101,14 @@ pub trait QuantumLink<C>: minicbor::Encode<C> {
46101 where
47102 Self : minicbor:: Encode < ( ) > ,
48103 {
104+ let valid_until = Date :: with_duration_from_now ( EXPIRATION_DURATION ) ;
105+
49106 let event: SealedEvent < Expression > =
50- SealedEvent :: new ( QuantumLink :: encode ( self ) , ARID :: new ( ) , sender. xid_document ) ;
107+ SealedEvent :: new ( QuantumLink :: encode ( self ) , ARID :: new ( ) , sender. xid_document )
108+ . with_date ( & valid_until) ;
51109 event
52110 . to_envelope (
53- None ,
111+ Some ( & valid_until ) ,
54112 Some ( & sender. private_keys . unwrap ( ) ) ,
55113 Some ( & recipient. xid_document ) ,
56114 )
@@ -65,7 +123,30 @@ pub trait QuantumLink<C>: minicbor::Encode<C> {
65123 Self : for < ' a > minicbor:: Decode < ' a , ( ) > ,
66124 {
67125 let event: SealedEvent < Expression > =
68- SealedEvent :: try_from_envelope ( envelope, None , None , private_keys) ?;
126+ SealedEvent :: try_from_envelope ( envelope, None , Some ( & Date :: now ( ) ) , private_keys) ?;
127+ let expression = event. content ( ) . clone ( ) ;
128+ Ok ( ( expression, event. sender ( ) . clone ( ) ) )
129+ }
130+
131+ fn unseal_with_replay_check (
132+ envelope : & Envelope ,
133+ private_keys : & PrivateKeys ,
134+ arid_cache : & mut ARIDCache ,
135+ ) -> anyhow:: Result < ( Expression , XIDDocument ) >
136+ where
137+ Self : for < ' a > minicbor:: Decode < ' a , ( ) > ,
138+ {
139+ let now = Utc :: now ( ) ;
140+ let event: SealedEvent < Expression > =
141+ SealedEvent :: try_from_envelope ( envelope, None , Some ( & Date :: from ( now) ) , private_keys) ?;
142+
143+ // Check for replay attack
144+ let arid = event. id ( ) ;
145+ let event_date = event. date ( ) . unwrap ( ) . datetime ( ) ;
146+ if arid_cache. check_and_store ( arid, event_date, now) {
147+ bail ! ( "Replay attack detected: ARID has been seen before" ) ;
148+ }
149+
69150 let expression = event. content ( ) . clone ( ) ;
70151 Ok ( ( expression, event. sender ( ) . clone ( ) ) )
71152 }
@@ -78,13 +159,33 @@ pub trait QuantumLink<C>: minicbor::Encode<C> {
78159 Ok ( ( PassportMessage :: decode ( & expression) ?, sender) )
79160 }
80161
162+ fn unseal_passport_message_with_replay_check (
163+ envelope : & Envelope ,
164+ private_keys : & PrivateKeys ,
165+ arid_cache : & mut ARIDCache ,
166+ ) -> anyhow:: Result < ( PassportMessage , XIDDocument ) > {
167+ let ( expression, sender) =
168+ PassportMessage :: unseal_with_replay_check ( envelope, private_keys, arid_cache) ?;
169+ Ok ( ( PassportMessage :: decode ( & expression) ?, sender) )
170+ }
171+
81172 fn unseal_envoy_message (
82173 envelope : & Envelope ,
83174 private_keys : & PrivateKeys ,
84175 ) -> anyhow:: Result < ( EnvoyMessage , XIDDocument ) > {
85176 let ( expression, sender) = EnvoyMessage :: unseal ( envelope, private_keys) ?;
86177 Ok ( ( EnvoyMessage :: decode ( & expression) ?, sender) )
87178 }
179+
180+ fn unseal_envoy_message_with_replay_check (
181+ envelope : & Envelope ,
182+ private_keys : & PrivateKeys ,
183+ arid_cache : & mut ARIDCache ,
184+ ) -> anyhow:: Result < ( EnvoyMessage , XIDDocument ) > {
185+ let ( expression, sender) =
186+ EnvoyMessage :: unseal_with_replay_check ( envelope, private_keys, arid_cache) ?;
187+ Ok ( ( EnvoyMessage :: decode ( & expression) ?, sender) )
188+ }
88189}
89190
90191#[ derive( Debug , Clone ) ]
@@ -149,7 +250,7 @@ mod tests {
149250 api:: { message:: QuantumLinkMessage , quantum_link:: QuantumLink } ,
150251 fx:: ExchangeRate ,
151252 message:: EnvoyMessage ,
152- quantum_link:: QuantumLinkIdentity ,
253+ quantum_link:: { ARIDCache , QuantumLinkIdentity } ,
153254 } ;
154255
155256 #[ test]
@@ -212,4 +313,133 @@ mod tests {
212313 deserialized_identity. private_keys. unwrap( )
213314 ) ;
214315 }
316+
317+ #[ test]
318+ fn test_replay_attack_prevention ( ) {
319+ let envoy = QuantumLinkIdentity :: generate ( ) ;
320+ let passport = QuantumLinkIdentity :: generate ( ) ;
321+ let mut arid_cache = ARIDCache :: new ( ) ;
322+
323+ let fx_rate = ExchangeRate :: new ( "USD" , 0.85 ) ;
324+ let original_message = EnvoyMessage {
325+ message : QuantumLinkMessage :: ExchangeRate ( fx_rate. clone ( ) ) ,
326+ timestamp : 123456 ,
327+ } ;
328+
329+ // Seal the message
330+ let envelope = QuantumLink :: seal ( & original_message, envoy. clone ( ) , passport. clone ( ) ) ;
331+
332+ // First unseal should succeed
333+ let result1 = EnvoyMessage :: unseal_envoy_message_with_replay_check (
334+ & envelope,
335+ & passport. private_keys . clone ( ) . unwrap ( ) ,
336+ & mut arid_cache,
337+ ) ;
338+ assert ! ( result1. is_ok( ) ) ;
339+
340+ // Second unseal of the same message should fail (replay attack)
341+ let result2 = EnvoyMessage :: unseal_envoy_message_with_replay_check (
342+ & envelope,
343+ & passport. private_keys . unwrap ( ) ,
344+ & mut arid_cache,
345+ ) ;
346+ assert ! ( result2. is_err( ) ) ;
347+ assert ! ( result2
348+ . unwrap_err( )
349+ . to_string( )
350+ . contains( "Replay attack detected" ) ) ;
351+ }
352+
353+ #[ test]
354+ fn test_arid_cache_cleanup ( ) {
355+ let mut arid_cache = ARIDCache :: new ( ) ;
356+ let envoy = QuantumLinkIdentity :: generate ( ) ;
357+ let passport = QuantumLinkIdentity :: generate ( ) ;
358+
359+ // Create and seal multiple messages
360+ let fx_rate = ExchangeRate :: new ( "USD" , 0.85 ) ;
361+ let message1 = EnvoyMessage {
362+ message : QuantumLinkMessage :: ExchangeRate ( fx_rate. clone ( ) ) ,
363+ timestamp : 123456 ,
364+ } ;
365+ let message2 = EnvoyMessage {
366+ message : QuantumLinkMessage :: ExchangeRate ( fx_rate. clone ( ) ) ,
367+ timestamp : 123457 ,
368+ } ;
369+
370+ let envelope1 = QuantumLink :: seal ( & message1, envoy. clone ( ) , passport. clone ( ) ) ;
371+ let envelope2 = QuantumLink :: seal ( & message2, envoy. clone ( ) , passport. clone ( ) ) ;
372+
373+ // Unseal both messages
374+ let _result1 = EnvoyMessage :: unseal_envoy_message_with_replay_check (
375+ & envelope1,
376+ & passport. private_keys . clone ( ) . unwrap ( ) ,
377+ & mut arid_cache,
378+ )
379+ . unwrap ( ) ;
380+ let _result2 = EnvoyMessage :: unseal_envoy_message_with_replay_check (
381+ & envelope2,
382+ & passport. private_keys . unwrap ( ) ,
383+ & mut arid_cache,
384+ )
385+ . unwrap ( ) ;
386+
387+ // Should have 2 ARIDs stored
388+ assert_eq ! ( arid_cache. len( ) , 2 ) ;
389+
390+ // Clear cache manually to test cleanup
391+ arid_cache. clear ( ) ;
392+ assert_eq ! ( arid_cache. len( ) , 0 ) ;
393+ }
394+ }
395+
396+ #[ test]
397+ fn test_time_based_eviction ( ) {
398+ let mut cache = ARIDCache :: new ( ) ;
399+ let arid1 = ARID :: new ( ) ;
400+ let arid2 = ARID :: new ( ) ;
401+
402+ let expiration = chrono:: Duration :: from_std ( EXPIRATION_DURATION ) . unwrap ( ) ;
403+ let start = chrono:: Utc :: now ( ) ;
404+
405+ cache. check_and_store ( & arid1, start, start) ;
406+ assert_eq ! ( cache. len( ) , 1 ) ;
407+
408+ // evict the old time
409+ // evict the old time by advancing 'now' past expiration
410+ let future = start + expiration + chrono:: Duration :: seconds ( 1 ) ;
411+ cache. check_and_store ( & arid2, future, future) ;
412+ assert_eq ! ( cache. len( ) , 1 ) ;
413+ assert ! ( !cache. cache. iter( ) . any( |( id, _) | id == & arid1) ) ;
414+ }
415+
416+ #[ test]
417+ fn test_replay_check ( ) {
418+ use crate :: { fx:: ExchangeRate , message:: QuantumLinkMessage } ;
419+
420+ let mut arid_cache = ARIDCache :: new ( ) ;
421+ let envoy = QuantumLinkIdentity :: generate ( ) ;
422+ let passport = QuantumLinkIdentity :: generate ( ) ;
423+
424+ let fx_rate = ExchangeRate :: new ( "USD" , 0.85 ) ;
425+ let message = EnvoyMessage {
426+ message : QuantumLinkMessage :: ExchangeRate ( fx_rate) ,
427+ timestamp : 123456 ,
428+ } ;
429+
430+ let envelope = QuantumLink :: seal ( & message, envoy. clone ( ) , passport. clone ( ) ) ;
431+
432+ let result1 = EnvoyMessage :: unseal_envoy_message_with_replay_check (
433+ & envelope,
434+ & passport. private_keys . clone ( ) . unwrap ( ) ,
435+ & mut arid_cache,
436+ ) ;
437+ assert ! ( result1. is_ok( ) ) ;
438+
439+ let result2 = EnvoyMessage :: unseal_envoy_message_with_replay_check (
440+ & envelope,
441+ & passport. private_keys . unwrap ( ) ,
442+ & mut arid_cache,
443+ ) ;
444+ assert ! ( result2. is_err( ) ) ;
215445}
0 commit comments