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