//! Hybrid OPRF using Kyber KEM + HMAC-SHA512 //! //! Since pure lattice-based OPRFs don't have practical implementations, //! we use a hybrid construction: //! //! 1. Client generates ephemeral Kyber keypair and hashes password //! 2. Client sends blinded_element = (password_hash, ephemeral_pk) //! 3. Server encapsulates to ephemeral_pk, computes PRF with its secret //! 4. Server returns (ciphertext, encrypted_prf_output) //! 5. Client decapsulates, decrypts, derives randomized password use sha2::{Digest, Sha512}; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::ake::{ KyberCiphertext, KyberPublicKey, KyberSecretKey, decapsulate, encapsulate, generate_kem_keypair, }; use crate::error::{OpaqueError, Result}; use crate::kdf::{HASH_LEN, hkdf_expand_fixed}; use crate::mac; use crate::types::{KYBER_CT_LEN, KYBER_PK_LEN, OprfSeed}; const OPRF_CONTEXT: &[u8] = b"OPAQUE-Lattice-OPRF-v1"; const BLIND_LABEL: &[u8] = b"OPRF-Blind"; const FINALIZE_LABEL: &[u8] = b"OPRF-Finalize"; #[derive(Clone)] pub struct BlindedElement { pub password_hash: [u8; HASH_LEN], pub ephemeral_pk: Vec, } impl BlindedElement { #[must_use] pub fn to_bytes(&self) -> Vec { let mut bytes = Vec::with_capacity(HASH_LEN + KYBER_PK_LEN); bytes.extend_from_slice(&self.password_hash); bytes.extend_from_slice(&self.ephemeral_pk); bytes } pub fn from_bytes(bytes: &[u8]) -> Result { if bytes.len() != HASH_LEN + KYBER_PK_LEN { return Err(OpaqueError::InvalidOprfInput); } let mut password_hash = [0u8; HASH_LEN]; password_hash.copy_from_slice(&bytes[..HASH_LEN]); let ephemeral_pk = bytes[HASH_LEN..].to_vec(); Ok(Self { password_hash, ephemeral_pk, }) } } #[derive(Clone)] pub struct EvaluatedElement { pub ciphertext: Vec, pub encrypted_output: [u8; HASH_LEN], } impl EvaluatedElement { #[must_use] pub fn to_bytes(&self) -> Vec { let mut bytes = Vec::with_capacity(KYBER_CT_LEN + HASH_LEN); bytes.extend_from_slice(&self.ciphertext); bytes.extend_from_slice(&self.encrypted_output); bytes } pub fn from_bytes(bytes: &[u8]) -> Result { if bytes.len() != KYBER_CT_LEN + HASH_LEN { return Err(OpaqueError::InvalidOprfOutput); } let ciphertext = bytes[..KYBER_CT_LEN].to_vec(); let mut encrypted_output = [0u8; HASH_LEN]; encrypted_output.copy_from_slice(&bytes[KYBER_CT_LEN..]); Ok(Self { ciphertext, encrypted_output, }) } } #[derive(Zeroize, ZeroizeOnDrop)] pub struct OprfClient { password_hash: [u8; HASH_LEN], ephemeral_sk: Vec, } impl OprfClient { pub fn blind(password: &[u8]) -> (Self, BlindedElement) { let password_hash: [u8; HASH_LEN] = Sha512::digest(password).into(); let (ephemeral_pk, ephemeral_sk) = generate_kem_keypair(); let blinded = BlindedElement { password_hash, ephemeral_pk: ephemeral_pk.as_bytes(), }; let client = Self { password_hash, ephemeral_sk: ephemeral_sk.as_bytes(), }; (client, blinded) } pub fn finalize(self, evaluated: &EvaluatedElement) -> Result<[u8; HASH_LEN]> { let sk = KyberSecretKey::from_bytes(&self.ephemeral_sk)?; let ct = KyberCiphertext::from_bytes(&evaluated.ciphertext)?; let shared_secret = decapsulate(&ct, &sk)?; let decryption_key: [u8; HASH_LEN] = hkdf_expand_fixed(Some(OPRF_CONTEXT), shared_secret.as_bytes(), BLIND_LABEL)?; let mut prf_output = [0u8; HASH_LEN]; for i in 0..HASH_LEN { prf_output[i] = evaluated.encrypted_output[i] ^ decryption_key[i]; } let mut finalize_input = Vec::with_capacity(HASH_LEN * 2); finalize_input.extend_from_slice(&self.password_hash); finalize_input.extend_from_slice(&prf_output); let result: [u8; HASH_LEN] = hkdf_expand_fixed(Some(OPRF_CONTEXT), &finalize_input, FINALIZE_LABEL)?; Ok(result) } } pub struct OprfServer { seed: OprfSeed, } impl OprfServer { #[must_use] pub fn new(seed: OprfSeed) -> Self { Self { seed } } pub fn evaluate(&self, blinded: &BlindedElement) -> Result { let ephemeral_pk = KyberPublicKey::from_bytes(&blinded.ephemeral_pk)?; let (shared_secret, ciphertext) = encapsulate(&ephemeral_pk)?; let prf_output = mac::compute(self.seed.as_bytes(), &blinded.password_hash); let encryption_key: [u8; HASH_LEN] = hkdf_expand_fixed(Some(OPRF_CONTEXT), shared_secret.as_bytes(), BLIND_LABEL)?; let mut encrypted_output = [0u8; HASH_LEN]; for i in 0..HASH_LEN { encrypted_output[i] = prf_output[i] ^ encryption_key[i]; } Ok(EvaluatedElement { ciphertext: ciphertext.as_bytes(), encrypted_output, }) } pub fn evaluate_with_credential_id( &self, blinded: &BlindedElement, credential_id: &[u8], ) -> Result { let ephemeral_pk = KyberPublicKey::from_bytes(&blinded.ephemeral_pk)?; let (shared_secret, ciphertext) = encapsulate(&ephemeral_pk)?; let mut prf_input = Vec::with_capacity(blinded.password_hash.len() + credential_id.len()); prf_input.extend_from_slice(&blinded.password_hash); prf_input.extend_from_slice(credential_id); let prf_output = mac::compute(self.seed.as_bytes(), &prf_input); let encryption_key: [u8; HASH_LEN] = hkdf_expand_fixed(Some(OPRF_CONTEXT), shared_secret.as_bytes(), BLIND_LABEL)?; let mut encrypted_output = [0u8; HASH_LEN]; for i in 0..HASH_LEN { encrypted_output[i] = prf_output[i] ^ encryption_key[i]; } Ok(EvaluatedElement { ciphertext: ciphertext.as_bytes(), encrypted_output, }) } } pub fn server_evaluate(seed: &OprfSeed, blinded: &BlindedElement) -> Result { let server = OprfServer::new(seed.clone()); server.evaluate(blinded) } pub fn client_finalize(client: OprfClient, evaluated: &EvaluatedElement) -> Result<[u8; HASH_LEN]> { client.finalize(evaluated) } #[cfg(test)] mod tests { use super::*; fn random_seed() -> OprfSeed { use crate::types::OPRF_SEED_LEN; use rand::RngCore; let mut bytes = [0u8; OPRF_SEED_LEN]; rand::thread_rng().fill_bytes(&mut bytes); OprfSeed::new(bytes) } #[test] fn test_oprf_roundtrip() { let seed = random_seed(); let password = b"correct horse battery staple"; let (client, blinded) = OprfClient::blind(password); let evaluated = server_evaluate(&seed, &blinded).unwrap(); let output = client_finalize(client, &evaluated).unwrap(); assert_eq!(output.len(), HASH_LEN); } #[test] fn test_oprf_deterministic() { let seed = random_seed(); let password = b"test password"; let (client1, blinded1) = OprfClient::blind(password); let evaluated1 = server_evaluate(&seed, &blinded1).unwrap(); let output1 = client_finalize(client1, &evaluated1).unwrap(); let (client2, blinded2) = OprfClient::blind(password); let evaluated2 = server_evaluate(&seed, &blinded2).unwrap(); let output2 = client_finalize(client2, &evaluated2).unwrap(); assert_eq!(output1, output2); } #[test] fn test_oprf_different_passwords() { let seed = random_seed(); let (client1, blinded1) = OprfClient::blind(b"password1"); let evaluated1 = server_evaluate(&seed, &blinded1).unwrap(); let output1 = client_finalize(client1, &evaluated1).unwrap(); let (client2, blinded2) = OprfClient::blind(b"password2"); let evaluated2 = server_evaluate(&seed, &blinded2).unwrap(); let output2 = client_finalize(client2, &evaluated2).unwrap(); assert_ne!(output1, output2); } #[test] fn test_oprf_different_seeds() { let seed1 = random_seed(); let seed2 = random_seed(); let password = b"same password"; let (client1, blinded1) = OprfClient::blind(password); let evaluated1 = server_evaluate(&seed1, &blinded1).unwrap(); let output1 = client_finalize(client1, &evaluated1).unwrap(); let (client2, blinded2) = OprfClient::blind(password); let evaluated2 = server_evaluate(&seed2, &blinded2).unwrap(); let output2 = client_finalize(client2, &evaluated2).unwrap(); assert_ne!(output1, output2); } #[test] fn test_blinded_element_serialization() { let (_, blinded) = OprfClient::blind(b"password"); let bytes = blinded.to_bytes(); let restored = BlindedElement::from_bytes(&bytes).unwrap(); assert_eq!(blinded.password_hash, restored.password_hash); assert_eq!(blinded.ephemeral_pk, restored.ephemeral_pk); } #[test] fn test_evaluated_element_serialization() { let seed = random_seed(); let (_, blinded) = OprfClient::blind(b"password"); let evaluated = server_evaluate(&seed, &blinded).unwrap(); let bytes = evaluated.to_bytes(); let restored = EvaluatedElement::from_bytes(&bytes).unwrap(); assert_eq!(evaluated.ciphertext, restored.ciphertext); assert_eq!(evaluated.encrypted_output, restored.encrypted_output); } #[test] fn test_evaluate_with_credential_id() { let seed = random_seed(); let password = b"password"; let cred_id1 = b"user@example.com"; let cred_id2 = b"other@example.com"; let server = OprfServer::new(seed); let (client1, blinded1) = OprfClient::blind(password); let evaluated1 = server .evaluate_with_credential_id(&blinded1, cred_id1) .unwrap(); let output1 = client_finalize(client1, &evaluated1).unwrap(); let (client2, blinded2) = OprfClient::blind(password); let evaluated2 = server .evaluate_with_credential_id(&blinded2, cred_id2) .unwrap(); let output2 = client_finalize(client2, &evaluated2).unwrap(); assert_ne!(output1, output2); } }