use base64::{engine::general_purpose, Engine as _};
use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
use secp256k1::{Message, Secp256k1};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BetaAccessConfig {
pub enabled: bool,
pub authorized_pubkeys: Vec<String>,
pub token_expiration: u64,
pub auth_url: Option<String>,
}
impl Default for BetaAccessConfig {
fn default() -> Self {
Self {
enabled: false,
authorized_pubkeys: Vec::new(),
token_expiration: 86400, auth_url: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthSession {
pub id: String,
pub k1: String,
pub created_at: u64,
pub expires_at: u64,
pub authenticated: bool,
pub pubkey: Option<String>,
}
impl AuthSession {
pub fn new(expiration_secs: u64) -> Self {
let id = Uuid::new_v4().to_string();
let k1 = Uuid::new_v4().to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
Self {
id,
k1,
created_at: now,
expires_at: now + expiration_secs,
authenticated: false,
pubkey: None,
}
}
pub fn is_expired(&self) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
self.expires_at < now
}
}
#[derive(Debug)]
pub enum BetaAccessError {
AuthError(String),
InvalidSession(String),
ExpiredSession,
NotAuthorized,
ConfigError(String),
}
impl fmt::Display for BetaAccessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BetaAccessError::AuthError(msg) => write!(f, "Authentication error: {msg}"),
BetaAccessError::InvalidSession(msg) => write!(f, "Invalid session: {msg}"),
BetaAccessError::ExpiredSession => write!(f, "Expired session"),
BetaAccessError::NotAuthorized => write!(f, "Not authorized"),
BetaAccessError::ConfigError(msg) => write!(f, "Configuration error: {msg}"),
}
}
}
impl Error for BetaAccessError {}
pub struct BetaAccessManager {
config: BetaAccessConfig,
sessions: Arc<Mutex<HashMap<String, AuthSession>>>,
secp: Secp256k1<secp256k1::All>,
}
impl BetaAccessManager {
pub fn new(config: BetaAccessConfig) -> Self {
Self {
config,
sessions: Arc::new(Mutex::new(HashMap::new())),
secp: Secp256k1::new(),
}
}
pub fn create_session(&self) -> AuthSession {
let session = AuthSession::new(self.config.token_expiration);
let mut sessions = self.sessions.lock().unwrap();
sessions.insert(session.id.clone(), session.clone());
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
sessions.retain(|_, s| s.expires_at > now);
session
}
pub fn get_auth_url(&self, session: &AuthSession) -> Option<String> {
if !self.config.enabled {
return None;
}
self.config
.auth_url
.as_ref()
.map(|url| format!("{}?tag=login&k1={}", url, session.k1))
}
pub fn verify_auth(
&self,
session_id: &str,
signature: &str,
pubkey: &str,
) -> Result<AuthSession, BetaAccessError> {
if !self.config.enabled {
return Err(BetaAccessError::ConfigError(
"Beta access auth not enabled".to_string(),
));
}
let mut sessions = self.sessions.lock().unwrap();
let session = sessions
.get_mut(session_id)
.ok_or_else(|| BetaAccessError::InvalidSession("Session not found".to_string()))?;
if session.is_expired() {
return Err(BetaAccessError::ExpiredSession);
}
let signature_bytes = match general_purpose::STANDARD.decode(signature) {
Ok(bytes) => bytes,
Err(e) => {
return Err(BetaAccessError::AuthError(format!(
"Invalid signature: {e}"
)))
}
};
let pubkey_bytes = match hex::decode(pubkey) {
Ok(bytes) => bytes,
Err(e) => return Err(BetaAccessError::AuthError(format!("Invalid pubkey: {e}"))),
};
let mut hasher = Sha256::new();
hasher.update(session.k1.as_bytes());
let message_hash = hasher.finalize();
let message = match Message::from_digest_slice(&message_hash) {
Ok(msg) => msg,
Err(e) => return Err(BetaAccessError::AuthError(format!("Invalid message: {e}"))),
};
let pubkey = match secp256k1::PublicKey::from_slice(&pubkey_bytes) {
Ok(pk) => pk,
Err(e) => {
return Err(BetaAccessError::AuthError(format!(
"Invalid public key: {e}"
)))
}
};
let recovery_id = RecoveryId::from_i32(signature_bytes[0] as i32 - 31)
.map_err(|e| BetaAccessError::AuthError(format!("Invalid recovery ID: {e}")))?;
let signature = match RecoverableSignature::from_compact(&signature_bytes[1..], recovery_id)
{
Ok(sig) => sig,
Err(e) => {
return Err(BetaAccessError::AuthError(format!(
"Invalid signature: {e}"
)))
}
};
let standard_signature = signature.to_standard();
match self
.secp
.verify_ecdsa(&message, &standard_signature, &pubkey)
{
Ok(_) => {
if !self.is_authorized(pubkey.to_string()) {
return Err(BetaAccessError::NotAuthorized);
}
session.authenticated = true;
session.pubkey = Some(pubkey.to_string());
Ok(session.clone())
}
Err(e) => Err(BetaAccessError::AuthError(format!(
"Signature verification failed: {e}"
))),
}
}
fn is_authorized(&self, pubkey: String) -> bool {
self.config.authorized_pubkeys.contains(&pubkey)
}
pub fn is_authenticated(&self, session_id: &str) -> bool {
let sessions = self.sessions.lock().unwrap();
if let Some(session) = sessions.get(session_id) {
!session.is_expired() && session.authenticated
} else {
false
}
}
pub fn add_authorized_pubkey(&mut self, pubkey: String) {
if !self.config.authorized_pubkeys.contains(&pubkey) {
self.config.authorized_pubkeys.push(pubkey);
}
}
pub fn remove_authorized_pubkey(&mut self, pubkey: &str) {
self.config.authorized_pubkeys.retain(|p| p != pubkey);
}
pub fn get_config(&self) -> &BetaAccessConfig {
&self.config
}
pub fn update_config(&mut self, config: BetaAccessConfig) {
self.config = config;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BetaAuthToken {
pub session_id: String,
pub expires_at: u64,
pub created_at: u64,
}
impl BetaAuthToken {
pub fn new(session_id: String, expires_at: u64) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
Self {
session_id,
expires_at,
created_at: now,
}
}
pub fn is_expired(&self) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
self.expires_at < now
}
pub fn encode(&self) -> String {
let json = serde_json::to_string(self).unwrap_or_default();
general_purpose::STANDARD.encode(json.as_bytes())
}
pub fn decode(token: &str) -> Result<Self, BetaAccessError> {
let bytes = match general_purpose::STANDARD.decode(token) {
Ok(bytes) => bytes,
Err(e) => return Err(BetaAccessError::AuthError(format!("Invalid token: {e}"))),
};
let json = match String::from_utf8(bytes) {
Ok(json) => json,
Err(e) => {
return Err(BetaAccessError::AuthError(format!(
"Invalid token data: {e}"
)))
}
};
let token = match serde_json::from_str::<Self>(&json) {
Ok(token) => token,
Err(e) => {
return Err(BetaAccessError::AuthError(format!(
"Invalid token format: {e}"
)))
}
};
if token.is_expired() {
return Err(BetaAccessError::ExpiredSession);
}
Ok(token)
}
}