Rename the entire workspace:
- Crate packages: quicnprotochat-{core,proto,server,client,gui,p2p,mobile} -> quicproquo-*
- Binary names: quicnprotochat -> qpq, quicnprotochat-server -> qpq-server,
quicnprotochat-gui -> qpq-gui
- Default files: *-state.bin -> qpq-state.bin, *-server.toml -> qpq-server.toml,
*.db -> qpq.db
- Environment variable prefix: QUICNPROTOCHAT_* -> QPQ_*
- App identifier: chat.quicnproto.gui -> chat.quicproquo.gui
- Proto package: quicnprotochat.bench -> quicproquo.bench
- All documentation, Docker, CI, and script references updated
HKDF domain-separation strings and P2P ALPN remain unchanged for
backward compatibility with existing encrypted state and wire protocol.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
5.1 KiB
Rust
153 lines
5.1 KiB
Rust
//! Benchmark: Hybrid KEM (X25519 + ML-KEM-768) vs classical-only encryption.
|
|
//!
|
|
//! Compares keypair generation, encryption, and decryption times for the
|
|
//! hybrid post-quantum scheme against classical X25519 + ChaCha20-Poly1305.
|
|
|
|
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
|
|
|
use quicproquo_core::{hybrid_encrypt, hybrid_decrypt, HybridKeypair};
|
|
|
|
// ── Classical baseline (X25519 + ChaCha20-Poly1305) ─────────────────────────
|
|
|
|
use chacha20poly1305::{
|
|
aead::{Aead, KeyInit},
|
|
ChaCha20Poly1305, Key, Nonce,
|
|
};
|
|
use hkdf::Hkdf;
|
|
use rand::{rngs::OsRng, RngCore};
|
|
use sha2::Sha256;
|
|
use x25519_dalek::{EphemeralSecret, PublicKey as X25519Public, StaticSecret};
|
|
|
|
struct ClassicalKeypair {
|
|
secret: StaticSecret,
|
|
public: X25519Public,
|
|
}
|
|
|
|
impl ClassicalKeypair {
|
|
fn generate() -> Self {
|
|
let secret = StaticSecret::random_from_rng(OsRng);
|
|
let public = X25519Public::from(&secret);
|
|
Self { secret, public }
|
|
}
|
|
}
|
|
|
|
fn classical_encrypt(recipient_pk: &X25519Public, plaintext: &[u8]) -> Vec<u8> {
|
|
let eph_secret = EphemeralSecret::random_from_rng(OsRng);
|
|
let eph_public = X25519Public::from(&eph_secret);
|
|
let shared = eph_secret.diffie_hellman(recipient_pk);
|
|
|
|
let hk = Hkdf::<Sha256>::new(None, shared.as_bytes());
|
|
let mut key_bytes = [0u8; 32];
|
|
hk.expand(b"classical-bench", &mut key_bytes).unwrap();
|
|
|
|
let mut nonce_bytes = [0u8; 12];
|
|
OsRng.fill_bytes(&mut nonce_bytes);
|
|
|
|
let cipher = ChaCha20Poly1305::new(Key::from_slice(&key_bytes));
|
|
let ct = cipher
|
|
.encrypt(Nonce::from_slice(&nonce_bytes), plaintext)
|
|
.unwrap();
|
|
|
|
// Wire: eph_pk(32) || nonce(12) || ciphertext
|
|
let mut out = Vec::with_capacity(32 + 12 + ct.len());
|
|
out.extend_from_slice(eph_public.as_bytes());
|
|
out.extend_from_slice(&nonce_bytes);
|
|
out.extend_from_slice(&ct);
|
|
out
|
|
}
|
|
|
|
fn classical_decrypt(keypair: &ClassicalKeypair, envelope: &[u8]) -> Vec<u8> {
|
|
let eph_pk = X25519Public::from(<[u8; 32]>::try_from(&envelope[..32]).unwrap());
|
|
let nonce_bytes: [u8; 12] = envelope[32..44].try_into().unwrap();
|
|
let ct = &envelope[44..];
|
|
|
|
let shared = keypair.secret.diffie_hellman(&eph_pk);
|
|
|
|
let hk = Hkdf::<Sha256>::new(None, shared.as_bytes());
|
|
let mut key_bytes = [0u8; 32];
|
|
hk.expand(b"classical-bench", &mut key_bytes).unwrap();
|
|
|
|
let cipher = ChaCha20Poly1305::new(Key::from_slice(&key_bytes));
|
|
cipher
|
|
.decrypt(Nonce::from_slice(&nonce_bytes), ct)
|
|
.unwrap()
|
|
}
|
|
|
|
// ── Benchmarks ──────────────────────────────────────────────────────────────
|
|
|
|
fn bench_keygen(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("kem_keygen");
|
|
group.bench_function("hybrid", |b| {
|
|
b.iter(|| black_box(HybridKeypair::generate()));
|
|
});
|
|
group.bench_function("classical", |b| {
|
|
b.iter(|| black_box(ClassicalKeypair::generate()));
|
|
});
|
|
group.finish();
|
|
}
|
|
|
|
fn bench_encrypt(c: &mut Criterion) {
|
|
let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096), ("64KB", 65536)];
|
|
let mut group = c.benchmark_group("kem_encrypt");
|
|
|
|
let hybrid_kp = HybridKeypair::generate();
|
|
let hybrid_pk = hybrid_kp.public_key();
|
|
let classical_kp = ClassicalKeypair::generate();
|
|
|
|
for (label, size) in sizes {
|
|
let payload = vec![0xABu8; *size];
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("hybrid", label),
|
|
&payload,
|
|
|b, payload| {
|
|
b.iter(|| hybrid_encrypt(&hybrid_pk, black_box(payload), b"", b"").unwrap());
|
|
},
|
|
);
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("classical", label),
|
|
&payload,
|
|
|b, payload| {
|
|
b.iter(|| classical_encrypt(&classical_kp.public, black_box(payload)));
|
|
},
|
|
);
|
|
}
|
|
group.finish();
|
|
}
|
|
|
|
fn bench_decrypt(c: &mut Criterion) {
|
|
let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096), ("64KB", 65536)];
|
|
let mut group = c.benchmark_group("kem_decrypt");
|
|
|
|
let hybrid_kp = HybridKeypair::generate();
|
|
let hybrid_pk = hybrid_kp.public_key();
|
|
let classical_kp = ClassicalKeypair::generate();
|
|
|
|
for (label, size) in sizes {
|
|
let payload = vec![0xABu8; *size];
|
|
let hybrid_ct = hybrid_encrypt(&hybrid_pk, &payload, b"", b"").unwrap();
|
|
let classical_ct = classical_encrypt(&classical_kp.public, &payload);
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("hybrid", label),
|
|
&hybrid_ct,
|
|
|b, ct| {
|
|
b.iter(|| hybrid_decrypt(&hybrid_kp, black_box(ct), b"", b"").unwrap());
|
|
},
|
|
);
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("classical", label),
|
|
&classical_ct,
|
|
|b, ct| {
|
|
b.iter(|| classical_decrypt(&classical_kp, black_box(ct)));
|
|
},
|
|
);
|
|
}
|
|
group.finish();
|
|
}
|
|
|
|
criterion_group!(benches, bench_keygen, bench_encrypt, bench_decrypt);
|
|
criterion_main!(benches);
|