use curve25519_dalek::ristretto::CompressedRistretto;
use curve25519_dalek::{RistrettoPoint, Scalar};
use sha2::{Digest, Sha256, Sha512};
const OPRF_DOMAIN: &[u8] = b"quipu/v2/oprf";
fn random_scalar() -> Scalar {
let mut b = [0u8; 64];
getrandom::getrandom(&mut b).expect("RNG del sistema");
Scalar::from_bytes_mod_order_wide(&b)
}
pub struct Server {
key: Scalar,
remaining: u32,
}
pub struct BlindState {
r_inv: Scalar,
}
impl Server {
pub fn new(max_requests: u32) -> Self {
Self {
key: random_scalar(),
remaining: max_requests,
}
}
pub fn from_seed(seed: &[u8; 32], max_requests: u32) -> Self {
let mut hasher = Sha512::new();
hasher.update(b"quipu/v2/oprf-server-key");
hasher.update(seed);
let wide: [u8; 64] = hasher.finalize().into();
Self {
key: Scalar::from_bytes_mod_order_wide(&wide),
remaining: max_requests,
}
}
pub fn evaluate(&mut self, blinded: &[u8; 32]) -> Option<[u8; 32]> {
if self.remaining == 0 {
return None; }
let out = self.evaluate_raw(blinded)?;
self.remaining -= 1; Some(out)
}
pub fn evaluate_raw(&self, blinded: &[u8; 32]) -> Option<[u8; 32]> {
let point = CompressedRistretto::from_slice(blinded).ok()?.decompress()?;
Some((self.key * point).compress().to_bytes())
}
}
pub fn blind(password: &[u8]) -> (BlindState, [u8; 32]) {
let h = RistrettoPoint::hash_from_bytes::<Sha512>(password);
let r = random_scalar();
let blinded = r * h;
(
BlindState { r_inv: r.invert() },
blinded.compress().to_bytes(),
)
}
pub fn finalize(password: &[u8], state: &BlindState, evaluated: &[u8; 32]) -> Option<[u8; 32]> {
let ev = CompressedRistretto::from_slice(evaluated)
.ok()?
.decompress()?;
let unblinded = state.r_inv * ev;
let mut hasher = Sha256::new();
hasher.update(OPRF_DOMAIN);
hasher.update((password.len() as u64).to_be_bytes());
hasher.update(password);
hasher.update(unblinded.compress().to_bytes());
Some(hasher.finalize().into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn oprf_output_is_deterministic_in_password_and_key() {
let mut server = Server::new(100);
let pw = b"passphrase del usuario";
let (s1, b1) = blind(pw);
let e1 = server.evaluate(&b1).unwrap();
let o1 = finalize(pw, &s1, &e1).unwrap();
let (s2, b2) = blind(pw);
assert_ne!(b1, b2, "el cegado debe ser aleatorio");
let e2 = server.evaluate(&b2).unwrap();
let o2 = finalize(pw, &s2, &e2).unwrap();
assert_eq!(o1, o2, "mismo password+clave -> mismo output OPRF");
}
#[test]
fn different_passwords_yield_different_outputs() {
let mut server = Server::new(100);
let (sa, ba) = blind(b"password-A");
let ea = server.evaluate(&ba).unwrap();
let oa = finalize(b"password-A", &sa, &ea).unwrap();
let (sb, bb) = blind(b"password-B");
let eb = server.evaluate(&bb).unwrap();
let ob = finalize(b"password-B", &sb, &eb).unwrap();
assert_ne!(oa, ob);
}
#[test]
fn from_seed_is_deterministic() {
let seed = [42u8; 32];
let s1 = Server::from_seed(&seed, 10);
let s2 = Server::from_seed(&seed, 10);
let (_st, b) = blind(b"x");
assert_eq!(s1.evaluate_raw(&b), s2.evaluate_raw(&b));
}
#[test]
fn evaluate_raw_ignores_budget() {
let server = Server::new(0); let (_s, b) = blind(b"x");
assert!(server.evaluate_raw(&b).is_some());
}
#[test]
fn server_enforces_rate_limit() {
let mut server = Server::new(2);
let (_s, b) = blind(b"x");
assert!(server.evaluate(&b).is_some());
assert!(server.evaluate(&b).is_some());
assert!(server.evaluate(&b).is_none(), "presupuesto agotado");
}
}