1use std::collections::HashMap;
11
12use bs58::decode as bs58_decode;
13use ed25519_dalek::{Signature, Verifier, VerifyingKey};
14use reifydb_core::interface::auth::{AuthStep, AuthenticationProvider};
15use reifydb_runtime::context::{clock::Clock, rng::Rng};
16use reifydb_type::{Result, error::Error};
17
18use crate::error::AuthError;
19
20pub struct SolanaProvider {
21 clock: Clock,
22}
23
24impl SolanaProvider {
25 pub fn new(clock: Clock) -> Self {
26 Self {
27 clock,
28 }
29 }
30}
31
32impl AuthenticationProvider for SolanaProvider {
33 fn method(&self) -> &str {
34 "solana"
35 }
36
37 fn create(&self, _rng: &Rng, config: &HashMap<String, String>) -> Result<HashMap<String, String>> {
38 let public_key = config.get("public_key").ok_or_else(|| Error::from(AuthError::MissingPublicKey))?;
39
40 let bytes = bs58_decode(public_key).into_vec().map_err(|e| {
42 Error::from(AuthError::InvalidPublicKey {
43 reason: e.to_string(),
44 })
45 })?;
46
47 if bytes.len() != 32 {
48 return Err(Error::from(AuthError::InvalidPublicKey {
49 reason: format!("expected 32 bytes, got {}", bytes.len()),
50 }));
51 }
52
53 Ok(HashMap::from([("public_key".into(), public_key.clone())]))
54 }
55
56 fn authenticate(
57 &self,
58 stored: &HashMap<String, String>,
59 credentials: &HashMap<String, String>,
60 ) -> Result<AuthStep> {
61 let public_key_b58 =
62 stored.get("public_key").ok_or_else(|| Error::from(AuthError::MissingPublicKey))?;
63
64 if let Some(signature_b58) = credentials.get("signature") {
66 let signed_message = credentials.get("signed_message").ok_or_else(|| {
67 Error::from(AuthError::InvalidSignature {
68 reason: "missing signed_message".to_string(),
69 })
70 })?;
71
72 let pk_bytes: [u8; 32] = bs58_decode(public_key_b58)
74 .into_vec()
75 .map_err(|e| {
76 Error::from(AuthError::InvalidPublicKey {
77 reason: e.to_string(),
78 })
79 })?
80 .try_into()
81 .map_err(|_| {
82 Error::from(AuthError::InvalidPublicKey {
83 reason: "expected 32 bytes".to_string(),
84 })
85 })?;
86
87 let verifying_key = VerifyingKey::from_bytes(&pk_bytes).map_err(|e| {
88 Error::from(AuthError::InvalidPublicKey {
89 reason: e.to_string(),
90 })
91 })?;
92
93 let sig_bytes: [u8; 64] = bs58_decode(signature_b58)
95 .into_vec()
96 .map_err(|e| {
97 Error::from(AuthError::InvalidSignature {
98 reason: e.to_string(),
99 })
100 })?
101 .try_into()
102 .map_err(|_| {
103 Error::from(AuthError::InvalidSignature {
104 reason: "expected 64 bytes".to_string(),
105 })
106 })?;
107
108 let signature = Signature::from_bytes(&sig_bytes);
109
110 match verifying_key.verify(signed_message.as_bytes(), &signature) {
112 Ok(()) => return Ok(AuthStep::Authenticated),
113 Err(_) => return Ok(AuthStep::Failed),
114 }
115 }
116
117 let nonce_bytes = Rng::Os.bytes_32();
119 let nonce: String = nonce_bytes.iter().map(|b| format!("{:02x}", b)).collect();
120
121 let domain = credentials.get("domain").cloned().unwrap_or_else(|| "reifydb".to_string());
123 let statement =
124 credentials.get("statement").cloned().unwrap_or_else(|| "Sign in to ReifyDB".to_string());
125
126 let issued_at =
127 credentials.get("issued_at").cloned().unwrap_or_else(|| self.clock.now_secs().to_string());
128
129 let message = format!(
131 "{domain} wants you to sign in with your Solana account:\n\
132 {address}\n\
133 \n\
134 {statement}\n\
135 \n\
136 Nonce: {nonce}\n\
137 Issued At: {issued_at}",
138 domain = domain,
139 address = public_key_b58,
140 statement = statement,
141 nonce = nonce,
142 issued_at = issued_at,
143 );
144
145 Ok(AuthStep::Challenge {
146 payload: HashMap::from([("message".into(), message), ("nonce".into(), nonce)]),
147 })
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use bs58::encode as bs58_encode;
154 use ed25519_dalek::{Signer, SigningKey};
155 use reifydb_runtime::context::clock::MockClock;
156
157 use super::*;
158
159 fn test_provider() -> SolanaProvider {
160 let mock = MockClock::from_millis(1_700_000_000_000); SolanaProvider::new(Clock::Mock(mock))
162 }
163
164 fn test_keypair() -> (SigningKey, String) {
165 let secret: [u8; 32] = [
166 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
167 27, 28, 29, 30, 31, 32,
168 ];
169 let signing_key = SigningKey::from_bytes(&secret);
170 let public_key = signing_key.verifying_key();
171 let public_key_b58 = bs58_encode(public_key.as_bytes()).into_string();
172 (signing_key, public_key_b58)
173 }
174
175 #[test]
176 fn test_create_stores_public_key() {
177 let provider = test_provider();
178 let (_, public_key_b58) = test_keypair();
179 let config = HashMap::from([("public_key".to_string(), public_key_b58.clone())]);
180
181 let stored = provider.create(&Rng::default(), &config).unwrap();
182 assert_eq!(stored.get("public_key").unwrap(), &public_key_b58);
183 }
184
185 #[test]
186 fn test_create_requires_public_key() {
187 let provider = test_provider();
188 assert!(provider.create(&Rng::default(), &HashMap::new()).is_err());
189 }
190
191 #[test]
192 fn test_create_rejects_invalid_public_key() {
193 let provider = test_provider();
194 let config = HashMap::from([("public_key".to_string(), "not-valid-base58!!!".to_string())]);
195 assert!(provider.create(&Rng::default(), &config).is_err());
196 }
197
198 #[test]
199 fn test_create_rejects_wrong_length_key() {
200 let provider = test_provider();
201 let short_key = bs58_encode(&[0u8; 16]).into_string();
203 let config = HashMap::from([("public_key".to_string(), short_key)]);
204 assert!(provider.create(&Rng::default(), &config).is_err());
205 }
206
207 #[test]
208 fn test_challenge_response_flow() {
209 let provider = test_provider();
210 let (signing_key, public_key_b58) = test_keypair();
211 let stored = HashMap::from([("public_key".to_string(), public_key_b58)]);
212
213 let step1 = provider.authenticate(&stored, &HashMap::new()).unwrap();
215 let challenge_data = match step1 {
216 AuthStep::Challenge {
217 payload,
218 } => payload,
219 other => panic!("expected Challenge, got {:?}", other),
220 };
221
222 assert!(challenge_data.contains_key("message"));
223 assert!(challenge_data.contains_key("nonce"));
224
225 let message = challenge_data.get("message").unwrap();
226 assert!(message.contains("wants you to sign in with your Solana account"));
227 assert!(message.contains("Nonce:"));
228 assert!(message.contains("Issued At: 1700000000"));
229
230 let signature = signing_key.sign(message.as_bytes());
232 let signature_b58 = bs58_encode(signature.to_bytes()).into_string();
233
234 let credentials = HashMap::from([
235 ("signature".to_string(), signature_b58),
236 ("signed_message".to_string(), message.clone()),
237 ]);
238
239 let step2 = provider.authenticate(&stored, &credentials).unwrap();
240 assert_eq!(step2, AuthStep::Authenticated);
241 }
242
243 #[test]
244 fn test_invalid_signature_fails() {
245 let provider = test_provider();
246 let (_, public_key_b58) = test_keypair();
247 let stored = HashMap::from([("public_key".to_string(), public_key_b58)]);
248
249 let wrong_key = SigningKey::from_bytes(&[99u8; 32]);
251 let signature = wrong_key.sign(b"some message");
252 let signature_b58 = bs58_encode(signature.to_bytes()).into_string();
253
254 let credentials = HashMap::from([
255 ("signature".to_string(), signature_b58),
256 ("signed_message".to_string(), "some message".to_string()),
257 ]);
258
259 let step = provider.authenticate(&stored, &credentials).unwrap();
260 assert_eq!(step, AuthStep::Failed);
261 }
262}