reifydb_auth/
challenge.rs1use std::collections::HashMap;
5
6use reifydb_runtime::{
7 context::{
8 clock::{Clock, Instant},
9 rng::Rng,
10 },
11 sync::rwlock::RwLock,
12};
13use reifydb_value::value::duration::Duration;
14use uuid::Builder;
15
16struct ChallengeEntry {
17 pub identifier: String,
18 pub method: String,
19 pub payload: HashMap<String, String>,
20 pub pending_public_key: Option<String>,
21 pub created_at: Instant,
22}
23
24pub struct ChallengeInfo {
25 pub identifier: String,
26 pub method: String,
27 pub payload: HashMap<String, String>,
28 pub pending_public_key: Option<String>,
29}
30
31pub struct ChallengeStore {
32 entries: RwLock<HashMap<String, ChallengeEntry>>,
33 ttl: Duration,
34}
35
36impl ChallengeStore {
37 pub fn new(ttl: Duration) -> Self {
38 Self {
39 entries: RwLock::new(HashMap::new()),
40 ttl,
41 }
42 }
43
44 pub fn create(
45 &self,
46 identifier: String,
47 method: String,
48 payload: HashMap<String, String>,
49 pending_public_key: Option<String>,
50 clock: &Clock,
51 rng: &Rng,
52 ) -> String {
53 let millis =
54 u64::try_from(clock.now().to_millis()).expect("system clock is never before the Unix epoch");
55 let random_bytes = rng.infra_bytes_10();
56 let challenge_id = Builder::from_unix_timestamp_millis(millis, &random_bytes).into_uuid().to_string();
57 let entry = ChallengeEntry {
58 identifier,
59 method,
60 payload,
61 pending_public_key,
62 created_at: clock.instant(),
63 };
64 let mut entries = self.entries.write();
65 entries.insert(challenge_id.clone(), entry);
66 challenge_id
67 }
68
69 pub fn consume(&self, challenge_id: &str) -> Option<ChallengeInfo> {
70 let mut entries = self.entries.write();
71 let entry = entries.remove(challenge_id)?;
72
73 if entry.created_at.elapsed() > self.ttl.to_std() {
74 return None;
75 }
76
77 Some(ChallengeInfo {
78 identifier: entry.identifier,
79 method: entry.method,
80 payload: entry.payload,
81 pending_public_key: entry.pending_public_key,
82 })
83 }
84
85 pub fn cleanup_expired(&self) {
86 let ttl = self.ttl.to_std();
87 let mut entries = self.entries.write();
88 entries.retain(|_, e| e.created_at.elapsed() <= ttl);
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use reifydb_runtime::context::clock::MockClock;
95
96 use super::*;
97
98 fn test_clock_and_rng() -> (Clock, MockClock, Rng) {
99 let mock = MockClock::from_millis(1000);
100 (Clock::Mock(mock.clone()), mock, Rng::seeded(42))
101 }
102
103 #[test]
104 fn test_create_and_consume() {
105 let (clock, _, rng) = test_clock_and_rng();
106 let store = ChallengeStore::new(Duration::from_seconds(60).unwrap());
107 let data = HashMap::from([("nonce".to_string(), "abc123".to_string())]);
108
109 let id = store.create("alice".to_string(), "solana".to_string(), data, None, &clock, &rng);
110 let info = store.consume(&id).unwrap();
111
112 assert_eq!(info.identifier, "alice");
113 assert_eq!(info.method, "solana");
114 assert_eq!(info.payload.get("nonce").unwrap(), "abc123");
115 }
116
117 #[test]
118 fn test_one_time_use() {
119 let (clock, _, rng) = test_clock_and_rng();
120 let store = ChallengeStore::new(Duration::from_seconds(60).unwrap());
121 let id = store.create("alice".to_string(), "solana".to_string(), HashMap::new(), None, &clock, &rng);
122
123 assert!(store.consume(&id).is_some());
124 assert!(store.consume(&id).is_none()); }
126
127 #[test]
128 fn test_unknown_challenge() {
129 let store = ChallengeStore::new(Duration::from_seconds(60).unwrap());
130 assert!(store.consume("nonexistent").is_none());
131 }
132
133 #[test]
134 fn test_pending_public_key_round_trips_outside_the_payload() {
135 let (clock, _, rng) = test_clock_and_rng();
137 let store = ChallengeStore::new(Duration::from_seconds(60).unwrap());
138
139 let id = store.create(
140 "wallet".to_string(),
141 "solana".to_string(),
142 HashMap::from([("message".to_string(), "sign me".to_string())]),
143 Some("PubKey111".to_string()),
144 &clock,
145 &rng,
146 );
147 let info = store.consume(&id).unwrap();
148
149 assert_eq!(info.pending_public_key.as_deref(), Some("PubKey111"));
150 assert!(
151 !info.payload.contains_key("public_key"),
152 "the pending key must never reach the client payload"
153 );
154 }
155
156 #[test]
157 fn test_expired_challenge() {
158 let (clock, mock, rng) = test_clock_and_rng();
159 let store = ChallengeStore::new(Duration::from_milliseconds(1).unwrap());
160 let id = store.create("alice".to_string(), "solana".to_string(), HashMap::new(), None, &clock, &rng);
161
162 mock.advance_millis(10);
163 assert!(store.consume(&id).is_none());
164 }
165}