Port Brave Sync server to Rust
Use SQLite for persistence and generate Rust protobuf bindings at build time. Add Docker image support for containerized deployment. Co-Authored-By: codex <[email protected]>
This commit is contained in:
commit
28826af88d
93 files changed
+11666
No files matched your search
+131
@@ -0,0 +1,131 @@
|
||||
use axum::http::HeaderMap;
|
||||
use base64::{Engine, engine::general_purpose::URL_SAFE};
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use regex::Regex;
|
||||
use std::{env, sync::LazyLock};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::util::now_millis;
|
||||
|
||||
pub const TOKEN_MAX_DURATION: i64 = 86_400 * 1_000;
|
||||
const BEARER_PREFIX: &str = "Bearer ";
|
||||
|
||||
static TOKEN_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^([a-fA-F0-9]+)\|([a-fA-F0-9]+)\|([a-fA-F0-9]+)$").unwrap());
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthError {
|
||||
#[error("not a valid token")]
|
||||
InvalidToken,
|
||||
#[error("invalid token format")]
|
||||
InvalidTokenFormat,
|
||||
#[error("token is expired")]
|
||||
Expired,
|
||||
#[error("signature verification failed")]
|
||||
Signature,
|
||||
#[error("this client ID is blocked")]
|
||||
Blocked,
|
||||
#[error("{0}")]
|
||||
Decode(String),
|
||||
}
|
||||
|
||||
pub fn authenticate(token: &str) -> Result<String, AuthError> {
|
||||
let decoded = URL_SAFE
|
||||
.decode(token)
|
||||
.map_err(|err| AuthError::Decode(format!("error doing base64 decoding: {err}")))?;
|
||||
let decoded = std::str::from_utf8(&decoded)
|
||||
.map_err(|err| AuthError::Decode(format!("error decoding token as utf8: {err}")))?;
|
||||
let captures = TOKEN_RE
|
||||
.captures(decoded)
|
||||
.ok_or(AuthError::InvalidTokenFormat)?;
|
||||
|
||||
let timestamp_hex = &captures[1];
|
||||
let signed_timestamp_hex = &captures[2];
|
||||
let public_key_hex = &captures[3];
|
||||
|
||||
let public_key = hex::decode(public_key_hex)
|
||||
.map_err(|err| AuthError::Decode(format!("error decoding hex string: {err}")))?;
|
||||
let timestamp_bytes = hex::decode(timestamp_hex)
|
||||
.map_err(|err| AuthError::Decode(format!("error decoding hex string: {err}")))?;
|
||||
let signature_bytes = hex::decode(signed_timestamp_hex)
|
||||
.map_err(|err| AuthError::Decode(format!("error decoding hex string: {err}")))?;
|
||||
|
||||
let key = VerifyingKey::from_bytes(
|
||||
public_key
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| AuthError::InvalidTokenFormat)?,
|
||||
)
|
||||
.map_err(|_| AuthError::InvalidTokenFormat)?;
|
||||
let signature =
|
||||
Signature::from_slice(&signature_bytes).map_err(|_| AuthError::InvalidTokenFormat)?;
|
||||
key.verify(×tamp_bytes, &signature)
|
||||
.map_err(|_| AuthError::Signature)?;
|
||||
|
||||
let timestamp = std::str::from_utf8(×tamp_bytes)
|
||||
.map_err(|err| AuthError::Decode(format!("error parsing timestamp: {err}")))?
|
||||
.parse::<i64>()
|
||||
.map_err(|err| AuthError::Decode(format!("error parsing timestamp: {err}")))?;
|
||||
if (now_millis() - timestamp).abs() > TOKEN_MAX_DURATION {
|
||||
return Err(AuthError::Expired);
|
||||
}
|
||||
|
||||
let public_key_lc = public_key_hex.to_ascii_lowercase();
|
||||
let blocked = env::var("BLOCKED_CLIENT_IDS").unwrap_or_default();
|
||||
if blocked
|
||||
.split(',')
|
||||
.any(|id| id.trim().eq_ignore_ascii_case(&public_key_lc))
|
||||
{
|
||||
return Err(AuthError::Blocked);
|
||||
}
|
||||
|
||||
Ok(public_key_lc)
|
||||
}
|
||||
|
||||
pub fn authorize(headers: &HeaderMap) -> Result<String, AuthError> {
|
||||
let token = headers
|
||||
.get("authorization")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.ok_or(AuthError::InvalidToken)?;
|
||||
let token = token
|
||||
.strip_prefix(BEARER_PREFIX)
|
||||
.ok_or(AuthError::InvalidToken)?;
|
||||
if token.is_empty() {
|
||||
return Err(AuthError::InvalidToken);
|
||||
}
|
||||
authenticate(token).map_err(|err| AuthError::Decode(format!("error authorizing: {err}")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use base64::engine::general_purpose::URL_SAFE;
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
fn generate_token(timestamp: i64) -> (String, String) {
|
||||
let key = SigningKey::from_bytes(&[7u8; 32]);
|
||||
let timestamp_bytes = timestamp.to_string().into_bytes();
|
||||
let signature = key.sign(×tamp_bytes);
|
||||
let public_key_hex = hex::encode(key.verifying_key().to_bytes());
|
||||
let token_hex = format!(
|
||||
"{}|{}|{}",
|
||||
hex::encode(timestamp_bytes),
|
||||
hex::encode(signature.to_bytes()),
|
||||
public_key_hex
|
||||
);
|
||||
(URL_SAFE.encode(token_hex), public_key_hex)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_accepts_valid_token() {
|
||||
let (token, public_key_hex) = generate_token(now_millis());
|
||||
let client_id = authenticate(&token).expect("token should authenticate");
|
||||
assert_eq!(client_id, public_key_hex);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_rejects_expired_token() {
|
||||
let (token, _) = generate_token(now_millis() - TOKEN_MAX_DURATION - 1);
|
||||
assert!(matches!(authenticate(&token), Err(AuthError::Expired)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user