orama-vault/src/crypto/hybrid.zig
2026-02-27 06:53:06 +02:00

173 lines
6.0 KiB
Zig

/// Hybrid Key Exchange — X25519 + ML-KEM-768.
///
/// Combines classical X25519 ECDH with post-quantum ML-KEM-768
/// to provide security against both classical and quantum adversaries.
///
/// The shared secret is: HKDF-SHA256(X25519_SS || ML-KEM_SS, "orama-hybrid-v1")
///
/// This ensures that:
/// - If X25519 is broken (quantum), ML-KEM still protects the key
/// - If ML-KEM is broken (unknown attack), X25519 still protects the key
/// - Both must be broken simultaneously to compromise the shared secret
const std = @import("std");
const pq_kem = @import("pq_kem.zig");
const hkdf = @import("hkdf.zig");
const secure_mem = @import("secure_mem.zig");
const X25519 = std.crypto.dh.X25519;
pub const SHARED_SECRET_SIZE: usize = 32;
pub const X25519_PK_SIZE: usize = 32;
pub const X25519_SK_SIZE: usize = 32;
/// Combined hybrid public key (X25519 + ML-KEM-768).
pub const HybridPublicKey = struct {
x25519: [X25519_PK_SIZE]u8,
ml_kem: [pq_kem.PK_SIZE]u8,
};
/// Combined hybrid secret key (X25519 + ML-KEM-768).
pub const HybridSecretKey = struct {
x25519: [X25519_SK_SIZE]u8,
ml_kem: [pq_kem.SK_SIZE]u8,
pub fn deinit(self: *HybridSecretKey) void {
secure_mem.secureZero(&self.x25519);
secure_mem.secureZero(&self.ml_kem);
}
};
/// Combined hybrid keypair.
pub const HybridKeypair = struct {
public_key: HybridPublicKey,
secret_key: HybridSecretKey,
pub fn deinit(self: *HybridKeypair) void {
self.secret_key.deinit();
}
};
/// Hybrid encapsulation result — sent to the responder.
pub const HybridEncapsResult = struct {
x25519_ephemeral_pk: [X25519_PK_SIZE]u8,
ml_kem_ciphertext: [pq_kem.CT_SIZE]u8,
shared_secret: [SHARED_SECRET_SIZE]u8,
pub fn deinit(self: *HybridEncapsResult) void {
secure_mem.secureZero(&self.shared_secret);
}
};
pub const HybridError = error{
X25519Failed,
MLKEMFailed,
HKDFFailed,
};
const HYBRID_INFO = "orama-hybrid-v1";
const HYBRID_SALT = [_]u8{0} ** 32; // fixed salt for domain separation
/// Generate a hybrid keypair (X25519 + ML-KEM-768).
pub fn keygen() HybridError!HybridKeypair {
// X25519 keypair
var x25519_sk: [X25519_SK_SIZE]u8 = undefined;
std.crypto.random.bytes(&x25519_sk);
const x25519_pk = X25519.recoverPublicKey(x25519_sk) catch return HybridError.X25519Failed;
// ML-KEM-768 keypair
const ml_kem_kp = pq_kem.keygen() catch return HybridError.MLKEMFailed;
return HybridKeypair{
.public_key = HybridPublicKey{
.x25519 = x25519_pk,
.ml_kem = ml_kem_kp.public_key,
},
.secret_key = HybridSecretKey{
.x25519 = x25519_sk,
.ml_kem = ml_kem_kp.secret_key,
},
};
}
/// Initiator side: encapsulate to a responder's hybrid public key.
/// Returns the shared secret and the data to send to the responder.
pub fn encapsulate(responder_pk: HybridPublicKey) HybridError!HybridEncapsResult {
// X25519: generate ephemeral keypair and compute DH
var eph_sk: [X25519_SK_SIZE]u8 = undefined;
std.crypto.random.bytes(&eph_sk);
const eph_pk = X25519.recoverPublicKey(eph_sk) catch return HybridError.X25519Failed;
const x25519_ss = X25519.scalarmult(eph_sk, responder_pk.x25519) catch return HybridError.X25519Failed;
secure_mem.secureZero(&eph_sk);
// ML-KEM-768: encapsulate to responder's PQ public key
const ml_kem_result = pq_kem.encaps(responder_pk.ml_kem) catch return HybridError.MLKEMFailed;
// Combine: HKDF(X25519_SS || ML-KEM_SS)
var combined: [64]u8 = undefined;
@memcpy(combined[0..32], &x25519_ss);
@memcpy(combined[32..64], &ml_kem_result.shared_secret);
var shared_secret: [SHARED_SECRET_SIZE]u8 = undefined;
hkdf.deriveKey(&shared_secret, &combined, &HYBRID_SALT, HYBRID_INFO);
secure_mem.secureZero(&combined);
return HybridEncapsResult{
.x25519_ephemeral_pk = eph_pk,
.ml_kem_ciphertext = ml_kem_result.ciphertext,
.shared_secret = shared_secret,
};
}
/// Responder side: decapsulate using own secret key.
/// Returns the same shared secret that the initiator computed.
pub fn decapsulate(
x25519_ephemeral_pk: [X25519_PK_SIZE]u8,
ml_kem_ciphertext: [pq_kem.CT_SIZE]u8,
own_sk: HybridSecretKey,
) HybridError![SHARED_SECRET_SIZE]u8 {
// X25519: compute DH with initiator's ephemeral public key
const x25519_ss = X25519.scalarmult(own_sk.x25519, x25519_ephemeral_pk) catch return HybridError.X25519Failed;
// ML-KEM-768: decapsulate
const ml_kem_ss = pq_kem.decaps(ml_kem_ciphertext, own_sk.ml_kem) catch return HybridError.MLKEMFailed;
// Combine: same HKDF as encapsulate
var combined: [64]u8 = undefined;
@memcpy(combined[0..32], &x25519_ss);
@memcpy(combined[32..64], &ml_kem_ss);
var shared_secret: [SHARED_SECRET_SIZE]u8 = undefined;
hkdf.deriveKey(&shared_secret, &combined, &HYBRID_SALT, HYBRID_INFO);
secure_mem.secureZero(&combined);
return shared_secret;
}
// ── Tests ────────────────────────────────────────────────────────────────────
test "hybrid: keygen produces valid keypair" {
var kp = try keygen();
defer kp.deinit();
// X25519 public key should be 32 bytes
try std.testing.expectEqual(@as(usize, 32), kp.public_key.x25519.len);
// ML-KEM public key should be 1184 bytes
try std.testing.expectEqual(@as(usize, pq_kem.PK_SIZE), kp.public_key.ml_kem.len);
}
test "hybrid: encapsulate produces valid result" {
var kp = try keygen();
defer kp.deinit();
var result = try encapsulate(kp.public_key);
defer result.deinit();
try std.testing.expectEqual(@as(usize, 32), result.x25519_ephemeral_pk.len);
try std.testing.expectEqual(@as(usize, pq_kem.CT_SIZE), result.ml_kem_ciphertext.len);
try std.testing.expectEqual(@as(usize, 32), result.shared_secret.len);
}
test "hybrid: shared secret size is 32 bytes" {
try std.testing.expectEqual(@as(usize, 32), SHARED_SECRET_SIZE);
}