initial
This commit is contained in:
329
src/oprf/hybrid.rs
Normal file
329
src/oprf/hybrid.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
//! 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<u8>,
|
||||
}
|
||||
|
||||
impl BlindedElement {
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
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<Self> {
|
||||
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<u8>,
|
||||
pub encrypted_output: [u8; HASH_LEN],
|
||||
}
|
||||
|
||||
impl EvaluatedElement {
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
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<Self> {
|
||||
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<u8>,
|
||||
}
|
||||
|
||||
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<EvaluatedElement> {
|
||||
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<EvaluatedElement> {
|
||||
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<EvaluatedElement> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user