# oxicrypto-kex — Pure-Rust key exchange and HPKE for OxiCrypto

[![Crates.io](https://img.shields.io/crates/v/oxicrypto-kex.svg)](https://crates.io/crates/oxicrypto-kex)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)

`oxicrypto-kex` is the key-exchange layer of the OxiCrypto stack. It exposes Diffie–Hellman key agreement over X25519, X448, and ECDH on NIST P-256 / P-384 / P-521 behind the `oxicrypto_core::KeyAgreement` trait, plus a complete RFC 9180 **HPKE** (Hybrid Public Key Encryption) implementation in the [`hpke`](#hpke--rfc-9180) module.

The crate is Pure Rust (`#![forbid(unsafe_code)]`), building on `x25519-dalek`, `x448`, and the RustCrypto `p256`/`p384`/`p521` curves. Every `agree()` implementation **rejects all-zero shared secrets** with a constant-time check — an all-zero output signals a low-order (small-subgroup) public-key attack, returning `CryptoError::Kex`. X448 additionally relies on the `x448` crate's RFC 7748 clamping and low-order point rejection.

## Installation

```toml
[dependencies]
oxicrypto-kex = "0.1.0"
```

## Quick Start

X25519 Diffie–Hellman with key generation:

```rust
use oxicrypto_core::KeyAgreement;
use oxicrypto_kex::{x25519_generate_keypair, X25519};
use rand_chacha::ChaCha20Rng;
use rand_core::SeedableRng;

fn main() -> Result<(), oxicrypto_core::CryptoError> {
    let mut rng = ChaCha20Rng::from_seed([7u8; 32]);
    let (alice_sk, alice_pk) = x25519_generate_keypair(&mut rng)?;
    let (bob_sk, bob_pk) = x25519_generate_keypair(&mut rng)?;

    let kex = X25519;
    let mut alice_shared = [0u8; 32];
    kex.agree(alice_sk.as_bytes(), &bob_pk, &mut alice_shared)?;

    let mut bob_shared = [0u8; 32];
    kex.agree(bob_sk.as_bytes(), &alice_pk, &mut bob_shared)?;

    assert_eq!(alice_shared, bob_shared);
    Ok(())
}
```

Single-shot HPKE seal / open (mode Base):

```rust
use oxicrypto_kex::hpke::{AeadId, HpkeSuite, KdfId, KemId};
use rand_chacha::ChaCha20Rng;
use rand_core::SeedableRng;

fn main() -> Result<(), oxicrypto_core::CryptoError> {
    let suite = HpkeSuite::new(
        KemId::DhkemX25519HkdfSha256,
        KdfId::HkdfSha256,
        AeadId::Aes128Gcm,
    );
    let mut rng = ChaCha20Rng::from_seed([7u8; 32]);

    let (sk_r, pk_r) = suite.generate_key_pair(&mut rng)?;
    let (enc, ct) = suite.seal_base(&pk_r, b"info", b"aad", b"secret", &mut rng)?;
    let pt = suite.open_base(&enc, sk_r.as_bytes(), b"info", b"aad", &ct)?;

    assert_eq!(pt, b"secret");
    Ok(())
}
```

## API Overview

### Key-agreement schemes (`KeyAgreement` trait)

Each is a zero-size unit struct implementing `oxicrypto_core::KeyAgreement` (`name`, `scalar_len`, `point_len`, `shared_secret_len`, `agree`, `agree_to_vec`).

| Type | Algorithm | Scalar (secret) | Public point | Shared secret |
|------|-----------|-----------------|--------------|---------------|
| `X25519` | X25519 DH (RFC 7748 §6.1) | 32 bytes | 32 bytes (Montgomery-u) | 32 bytes |
| `X448` | X448 DH (RFC 7748 §6.2) | 56 bytes (clamped) | 56 bytes (Montgomery-u) | 56 bytes |
| `EcdhP256` | ECDH P-256 (secp256r1) | 32 bytes | SEC1: 33 compressed / 65 uncompressed | 32 bytes (x-coordinate) |
| `EcdhP384` | ECDH P-384 (secp384r1) | 48 bytes | SEC1: 49 compressed / 97 uncompressed | 48 bytes (x-coordinate) |
| `EcdhP521` | ECDH P-521 (secp521r1) | 66 bytes | SEC1: 67 compressed / 133 uncompressed | 66 bytes (x-coordinate) |

> NIST P-521 key pairs generated by this crate use **uncompressed** SEC1 encoding (133 bytes), because `p521` sets `COMPRESS_POINTS = false`. Both compressed and uncompressed inputs are accepted by `agree()`.

### Public-key wrappers

| Type | Description |
|------|-------------|
| `X25519PublicKey([u8; 32])` | Type-safe 32-byte X25519 public key; `as_bytes`, `to_bytes`, `From<[u8; 32]>`, `AsRef<[u8]>` |
| `X448PublicKey([u8; 56])` | Type-safe 56-byte X448 public key; `as_bytes`, `to_bytes`, `From<[u8; 56]>`, `AsRef<[u8]>` |

### Key-generation helpers

All accept any `R: rand_core::TryCryptoRng + ?Sized`.

| Function | Returns | Notes |
|----------|---------|-------|
| `x25519_generate_keypair(rng)` | `(SecretKey<32>, [u8; 32])` | Secret scalar + 32-byte public point |
| `x448_generate_keypair(rng)` | `(SecretKey<56>, [u8; 56])` | RFC 7748 clamped scalar + 56-byte public point |
| `ecdh_p256_generate_keypair(rng)` | `(SecretVec, Vec<u8>)` | 32-byte scalar + SEC1 compressed (33-byte) public key |
| `ecdh_p384_generate_keypair(rng)` | `(SecretVec, Vec<u8>)` | 48-byte scalar + SEC1 compressed (49-byte) public key |
| `ecdh_p521_generate_keypair(rng)` | `(SecretVec, Vec<u8>)` | 66-byte scalar + SEC1 uncompressed (133-byte) public key |

`SecretKey<N>` / `SecretVec` come from `oxicrypto-core` and zeroize their contents on drop.

## HPKE — RFC 9180

The [`hpke`](src/hpke) module implements the full Hybrid Public Key Encryption construction (DHKEM × HKDF × AEAD) used by TLS Encrypted ClientHello, MLS, and OHTTP.

### Algorithm identifiers

| Enum | Variants (with wire id) |
|------|--------------------------|
| `KemId` (`#[non_exhaustive]`) | `DhkemP256HkdfSha256` (`0x0010`), `DhkemX25519HkdfSha256` (`0x0020`) |
| `KdfId` | `HkdfSha256` (`0x0001`), `HkdfSha384` (`0x0002`), `HkdfSha512` (`0x0003`) |
| `AeadId` | `Aes128Gcm` (`0x0001`), `Aes256Gcm` (`0x0002`), `ChaCha20Poly1305` (`0x0003`), `ExportOnly` (`0xFFFF`) |

Each enum exposes `id()` plus length accessors (`KemId`: `n_secret`, `n_enc`, `n_pk`, `n_sk`; `KdfId`: `n_h`; `AeadId`: `n_k`, `n_n`, `n_t`). The `ids` module also exports `i2osp(n, w)`, `kem_suite_id(kem)`, and `hpke_suite_id(kem, kdf, aead)`.

### `HpkeSuite`

`HpkeSuite::new(kem, kdf, aead)` bundles the three identifiers.

| Method group | Methods |
|--------------|---------|
| Accessors | `kem()`, `kdf()`, `aead()`, `n_enc()`, `n_pk()`, `n_sk()`, `n_k()`, `n_n()`, `n_t()` |
| Key management | `derive_key_pair(ikm)`, `generate_key_pair(rng)`, `serialize_public_key(pk)`, `deserialize_public_key(enc)` |
| Mode Base | `setup_base_s(pk_r, info, rng)`, `setup_base_r(enc, sk_r, info)` |
| Mode PSK | `setup_psk_s(pk_r, info, psk, psk_id, rng)`, `setup_psk_r(enc, sk_r, info, psk, psk_id)` |
| Mode Auth | `setup_auth_s(pk_r, info, sk_s, rng)`, `setup_auth_r(enc, sk_r, info, pk_s)` |
| Mode AuthPSK | `setup_auth_psk_s(pk_r, info, psk, psk_id, sk_s, rng)`, `setup_auth_psk_r(enc, sk_r, info, psk, psk_id, pk_s)` |
| Single-shot (§6.1) | `seal_base(pk_r, info, aad, pt, rng) -> (enc, ct)`, `open_base(enc, sk_r, info, aad, ct) -> pt` |

The `setup_*_s` methods return `(enc, HpkeContextS)`; `setup_*_r` return `HpkeContextR`.

### Encryption contexts

| Type | Methods | Role |
|------|---------|------|
| `HpkeContextS` | `seal(aad, pt) -> Vec<u8>`, `export(ctx, L) -> Vec<u8>`, `sequence_number()` | Sender: encrypt + export secrets |
| `HpkeContextR` | `open(aad, ct) -> Vec<u8>`, `export(ctx, L) -> Vec<u8>`, `sequence_number()` | Recipient: decrypt + export secrets |

Contexts are directional at the type level (a recipient calling `seal` is a compile error). Nonces follow `base_nonce XOR I2OSP(seq, Nn)` with a strictly increasing sequence number and the RFC 9180 overflow guard. `HpkeMode` (`Base`, `Psk`, `Auth`, `AuthPsk`) is also re-exported from the module root.

> The `ExportOnly` AEAD supports `Export` only; calling `seal`/`open` on an export-only suite returns `CryptoError::UnsupportedAlgorithm`.

## Feature Flags

| Feature | Default | Description |
|---------|---------|-------------|
| `std` | off | Propagates `std` to `oxicrypto-core`, `oxicrypto-kdf`, and `oxicrypto-aead` |

## Error Variants

All fallible operations return `oxicrypto_core::CryptoError`:

| Variant | When |
|---------|------|
| `BufferTooSmall` | `shared_out` is shorter than the algorithm's shared-secret length |
| `InvalidKey` | A scalar/point has the wrong length or is malformed (also malformed HPKE `enc`) |
| `Kex` | All-zero / low-order shared secret rejected, or an HPKE sequence number would overflow |
| `InvalidTag` | HPKE `open` authentication failure |
| `UnsupportedAlgorithm` | `seal`/`open` requested on an export-only HPKE suite |
| `Rng` | RNG failure during key generation or HPKE setup |
| `BadInput` | Invalid HPKE PSK inputs, or `Export` with `L == 0` |

## Cross-References

- [`oxicrypto-core`](../oxicrypto-core) — defines `KeyAgreement`, `SecretKey`, `SecretVec`, and `CryptoError`.
- [`oxicrypto-kdf`](../oxicrypto-kdf) — HKDF used by the HPKE labeled key schedule.
- [`oxicrypto-aead`](../oxicrypto-aead) — AES-GCM and ChaCha20Poly1305 used by HPKE encryption contexts.
- [`oxicrypto-pq`](../oxicrypto-pq) — post-quantum and hybrid KEMs (X-Wing, ML-KEM + P-384), which compose with this crate's X25519/ECDH.
- [`oxicrypto`](../oxicrypto) — the facade re-exports `EcdhP256` / `EcdhP384` and the `hpke` module.

## License

Apache-2.0 — COOLJAPAN OU (Team Kitasan)
