use std::collections::HashMap;
use bs58::decode as bs58_decode;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use reifydb_core::interface::auth::{AuthStep, AuthenticationProvider};
use reifydb_runtime::context::{clock::Clock, rng::Rng};
use reifydb_type::{Result, error::Error};
use crate::error::AuthError;
pub struct SolanaProvider {
clock: Clock,
}
impl SolanaProvider {
pub fn new(clock: Clock) -> Self {
Self {
clock,
}
}
}
impl AuthenticationProvider for SolanaProvider {
fn method(&self) -> &str {
"solana"
}
fn create(&self, _rng: &Rng, config: &HashMap<String, String>) -> Result<HashMap<String, String>> {
let public_key = config.get("public_key").ok_or_else(|| Error::from(AuthError::MissingPublicKey))?;
let bytes = bs58_decode(public_key).into_vec().map_err(|e| {
Error::from(AuthError::InvalidPublicKey {
reason: e.to_string(),
})
})?;
if bytes.len() != 32 {
return Err(Error::from(AuthError::InvalidPublicKey {
reason: format!("expected 32 bytes, got {}", bytes.len()),
}));
}
Ok(HashMap::from([("public_key".into(), public_key.clone())]))
}
fn authenticate(
&self,
stored: &HashMap<String, String>,
credentials: &HashMap<String, String>,
) -> Result<AuthStep> {
let public_key_b58 =
stored.get("public_key").ok_or_else(|| Error::from(AuthError::MissingPublicKey))?;
if let Some(signature_b58) = credentials.get("signature") {
let signed_message = credentials.get("signed_message").ok_or_else(|| {
Error::from(AuthError::InvalidSignature {
reason: "missing signed_message".to_string(),
})
})?;
let pk_bytes: [u8; 32] = bs58_decode(public_key_b58)
.into_vec()
.map_err(|e| {
Error::from(AuthError::InvalidPublicKey {
reason: e.to_string(),
})
})?
.try_into()
.map_err(|_| {
Error::from(AuthError::InvalidPublicKey {
reason: "expected 32 bytes".to_string(),
})
})?;
let verifying_key = VerifyingKey::from_bytes(&pk_bytes).map_err(|e| {
Error::from(AuthError::InvalidPublicKey {
reason: e.to_string(),
})
})?;
let sig_bytes: [u8; 64] = bs58_decode(signature_b58)
.into_vec()
.map_err(|e| {
Error::from(AuthError::InvalidSignature {
reason: e.to_string(),
})
})?
.try_into()
.map_err(|_| {
Error::from(AuthError::InvalidSignature {
reason: "expected 64 bytes".to_string(),
})
})?;
let signature = Signature::from_bytes(&sig_bytes);
match verifying_key.verify(signed_message.as_bytes(), &signature) {
Ok(()) => return Ok(AuthStep::Authenticated),
Err(_) => return Ok(AuthStep::Failed),
}
}
let nonce_bytes = Rng::Os.bytes_32();
let nonce: String = nonce_bytes.iter().map(|b| format!("{:02x}", b)).collect();
let domain = credentials.get("domain").cloned().unwrap_or_else(|| "reifydb".to_string());
let statement =
credentials.get("statement").cloned().unwrap_or_else(|| "Sign in to ReifyDB".to_string());
let issued_at =
credentials.get("issued_at").cloned().unwrap_or_else(|| self.clock.now_secs().to_string());
let message = format!(
"{domain} wants you to sign in with your Solana account:\n\
{address}\n\
\n\
{statement}\n\
\n\
Nonce: {nonce}\n\
Issued At: {issued_at}",
domain = domain,
address = public_key_b58,
statement = statement,
nonce = nonce,
issued_at = issued_at,
);
Ok(AuthStep::Challenge {
payload: HashMap::from([("message".into(), message), ("nonce".into(), nonce)]),
})
}
}
#[cfg(test)]
mod tests {
use bs58::encode as bs58_encode;
use ed25519_dalek::{Signer, SigningKey};
use reifydb_runtime::context::clock::MockClock;
use super::*;
fn test_provider() -> SolanaProvider {
let mock = MockClock::from_millis(1_700_000_000_000); SolanaProvider::new(Clock::Mock(mock))
}
fn test_keypair() -> (SigningKey, String) {
let secret: [u8; 32] = [
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,
27, 28, 29, 30, 31, 32,
];
let signing_key = SigningKey::from_bytes(&secret);
let public_key = signing_key.verifying_key();
let public_key_b58 = bs58_encode(public_key.as_bytes()).into_string();
(signing_key, public_key_b58)
}
#[test]
fn test_create_stores_public_key() {
let provider = test_provider();
let (_, public_key_b58) = test_keypair();
let config = HashMap::from([("public_key".to_string(), public_key_b58.clone())]);
let stored = provider.create(&Rng::default(), &config).unwrap();
assert_eq!(stored.get("public_key").unwrap(), &public_key_b58);
}
#[test]
fn test_create_requires_public_key() {
let provider = test_provider();
assert!(provider.create(&Rng::default(), &HashMap::new()).is_err());
}
#[test]
fn test_create_rejects_invalid_public_key() {
let provider = test_provider();
let config = HashMap::from([("public_key".to_string(), "not-valid-base58!!!".to_string())]);
assert!(provider.create(&Rng::default(), &config).is_err());
}
#[test]
fn test_create_rejects_wrong_length_key() {
let provider = test_provider();
let short_key = bs58_encode(&[0u8; 16]).into_string();
let config = HashMap::from([("public_key".to_string(), short_key)]);
assert!(provider.create(&Rng::default(), &config).is_err());
}
#[test]
fn test_challenge_response_flow() {
let provider = test_provider();
let (signing_key, public_key_b58) = test_keypair();
let stored = HashMap::from([("public_key".to_string(), public_key_b58)]);
let step1 = provider.authenticate(&stored, &HashMap::new()).unwrap();
let challenge_data = match step1 {
AuthStep::Challenge {
payload,
} => payload,
other => panic!("expected Challenge, got {:?}", other),
};
assert!(challenge_data.contains_key("message"));
assert!(challenge_data.contains_key("nonce"));
let message = challenge_data.get("message").unwrap();
assert!(message.contains("wants you to sign in with your Solana account"));
assert!(message.contains("Nonce:"));
assert!(message.contains("Issued At: 1700000000"));
let signature = signing_key.sign(message.as_bytes());
let signature_b58 = bs58_encode(signature.to_bytes()).into_string();
let credentials = HashMap::from([
("signature".to_string(), signature_b58),
("signed_message".to_string(), message.clone()),
]);
let step2 = provider.authenticate(&stored, &credentials).unwrap();
assert_eq!(step2, AuthStep::Authenticated);
}
#[test]
fn test_invalid_signature_fails() {
let provider = test_provider();
let (_, public_key_b58) = test_keypair();
let stored = HashMap::from([("public_key".to_string(), public_key_b58)]);
let wrong_key = SigningKey::from_bytes(&[99u8; 32]);
let signature = wrong_key.sign(b"some message");
let signature_b58 = bs58_encode(signature.to_bytes()).into_string();
let credentials = HashMap::from([
("signature".to_string(), signature_b58),
("signed_message".to_string(), "some message".to_string()),
]);
let step = provider.authenticate(&stored, &credentials).unwrap();
assert_eq!(step, AuthStep::Failed);
}
}