# flare.quic.protection

> A module of flare_mlake's API.

``flare.quic.protection`` -- QUIC v1 per-packet header + AEAD protection (RFC 9001 §5.3-5.4).

The :func:`unprotect_initial_packet` and :func:`protect_initial_packet`
entry points compose the three primitives already in
:mod:`flare.quic.crypto`:

- :func:`flare.quic.crypto.derive_initial_secrets` (RFC 9001 §5.2)
- :func:`flare.quic.crypto.derive_packet_keys` (RFC 9001 §5.1)
- :class:`flare.quic.crypto.OpenSslQuicCrypto` (AEAD + HP mask)

into the full inbound / outbound packet pipeline that the QUIC
server reactor wires per datagram:

* **unprotect** (inbound): strip header protection, reconstruct
  the truncated packet number per RFC 9000 §A.3, decrypt the
  AEAD payload with the unprotected header as associated data,
  return the frame bytes.
* **protect** (outbound): encrypt the frame bytes with the
  unprotected header as AAD, apply header protection to the
  packet-number bytes + the low header bits, return the wire
  bytes ready for ``sendto``.

The post-Initial encryption levels extend the Initial pair:

* :func:`unprotect_handshake_packet` / :func:`protect_handshake_packet`
  -- long-header packets at the Handshake encryption level (RFC
  9000 §17.2.4). Same AEAD + HP shape as Initial but no token
  field in the long-header extras, and the per-direction secret
  is supplied by the caller (the rustls QUIC bridge surfaces it
  through the session slab).
* :func:`unprotect_1rtt_packet` / :func:`protect_1rtt_packet` --
  short-header packets at the 1-RTT encryption level (RFC 9000
  §17.3). Short-header parsing instead of long-header; ``key_phase``
  is exposed for key-update tracking but applying a new key is
  outside this module's scope.

References:
- RFC 9001 §5.3 "AEAD Usage".
- RFC 9001 §5.4 "Header Protection".
- RFC 9000 §17.2 "Long Header Packets" + §17.3 "Short Header Packets".
- RFC 9000 §A.3 "Sample Packet Number Decoding Algorithm".
- aioquic ``aioquic.quic.crypto.CryptoPair`` / ``packet_protection``.

## Structs

### `UnprotectedPacket`

```mojo
struct UnprotectedPacket
```

The output of :func:`unprotect_initial_packet`.

``header`` is the unprotected packet header (first byte plus
the rest of the long header fields plus the packet number),
``payload`` is the AEAD-decrypted frame bytes, and
``packet_number`` is the reconstructed full packet number per
RFC 9000 §A.3. ``pn_length`` is the on-wire packet-number
encoding length in bytes (1..4) so the caller can advance
cursors without redoing the parse.

Implements: `Copyable`, `Movable`

Fields:

- `header: List[UInt8]`
- `payload: List[UInt8]`
- `packet_number: UInt64`
- `pn_length: Int`

## Functions

### `decode_packet_number`

```mojo
def decode_packet_number(truncated_pn: UInt64, pn_length: Int, largest_pn: UInt64) -> UInt64
```

RFC 9000 §A.3 "Sample Packet Number Decoding Algorithm".

Given the on-wire truncated packet number (``pn_length`` low
bytes) and the largest packet number the receiver has seen so
far, reconstruct the full packet number. The algorithm picks
the candidate that is closest to ``largest_pn + 1`` -- both
higher and lower windows are checked so out-of-order packets
decode correctly.

### `unprotect_initial_packet`

```mojo
def unprotect_initial_packet(datagram: Span[UInt8], dcid: ConnectionId, is_server: Bool, largest_received_pn: UInt64, aead_choice: Int = Int(0)) -> UnprotectedPacket
```

Strip header protection + AEAD-decrypt an Initial packet.

``dcid`` is the Destination Connection ID the *client* placed
on its first-flight Initial; both endpoints derive the initial
secrets from it (RFC 9001 §5.2). ``is_server`` picks the
reader's secret: the server reads with the client's secret
and writes with the server's secret. ``largest_received_pn``
is fed into the RFC 9000 §A.3 reconstruction so out-of-order
packets decode correctly.

Raises if the packet header is malformed, if the AEAD tag
fails, or if any of the cursors would overflow the datagram.

### `protect_initial_packet`

```mojo
def protect_initial_packet(unprotected_header_prefix: Span[UInt8], packet_number: UInt64, pn_length: Int, plaintext: Span[UInt8], dcid: ConnectionId, is_server: Bool, aead_choice: Int = Int(0)) -> List[UInt8]
```

Build a fully protected Initial packet ready for ``sendto``.

``unprotected_header_prefix`` is the long-header bytes from
the first byte through the payload-length varint (i.e. the
output of :func:`flare.quic.packet.encode_long_header` plus
the encoded token + payload-length). The function appends:

1. The packet-number bytes (truncated to ``pn_length``).
2. The AEAD ciphertext + 16-byte tag.

Then it applies header protection to the packet-number bytes
and the low 4 bits of the first byte per RFC 9001 §5.4.

``packet_number`` is reused as the AEAD nonce input
(RFC 9001 §5.3). The caller is responsible for sequencing
packet numbers per the RFC's monotonic-per-key rules.

### `unprotect_handshake_packet`

```mojo
def unprotect_handshake_packet(datagram: Span[UInt8], reader_secret: Span[UInt8], largest_received_pn: UInt64, aead_choice: Int = Int(0)) -> UnprotectedPacket
```

Strip header protection + AEAD-decrypt a Handshake packet.

Identical RFC 9001 §5.3-§5.4 shape as
:func:`unprotect_initial_packet`, but:

* The long-header extras carry only a payload-length varint
  (RFC 9000 §17.2.4 -- no token field, unlike Initial).
* The per-direction secret comes from the TLS handshake (the
  rustls KeyChange the Q9-W bridge surfaces) rather than the
  DCID-derived Initial secret.

``reader_secret`` is the 32-byte (HKDF-SHA-256) handshake
traffic secret for the *peer's* direction: on a server reading
a client Handshake packet this is the client_handshake_secret.

### `protect_handshake_packet`

```mojo
def protect_handshake_packet(unprotected_header_prefix: Span[UInt8], packet_number: UInt64, pn_length: Int, plaintext: Span[UInt8], writer_secret: Span[UInt8], aead_choice: Int = Int(0)) -> List[UInt8]
```

Build a fully protected Handshake packet ready for ``sendto``.

``unprotected_header_prefix`` is the long-header bytes from
the first byte through the payload-length varint -- i.e. the
output of :func:`flare.quic.packet.encode_long_header` plus
the encoded payload-length (no token field for Handshake per
RFC 9000 §17.2.4). The function appends the packet-number
bytes, AEAD-encrypts the plaintext with the unprotected
header as AAD, then applies header protection.

``writer_secret`` is the local-side handshake traffic secret
(server_handshake_secret on the server send path); the rustls
KeyChange surfaces it through Q9-W's session slab.

### `unprotect_1rtt_packet`

```mojo
def unprotect_1rtt_packet(datagram: Span[UInt8], reader_secret: Span[UInt8], largest_received_pn: UInt64, dcid_length: Int, aead_choice: Int = Int(0)) -> UnprotectedPacket
```

Strip header protection + AEAD-decrypt a 1-RTT (short header) packet.

``dcid_length`` is the connection's pinned local CID length
(the short header does not encode the DCID length on the
wire, per RFC 9000 §17.3). ``reader_secret`` is the peer's
application traffic secret (client_application_traffic_secret
on the server read path).

The HP sample is taken at the four-byte offset after the
packet number per RFC 9001 §5.4.2; that requires the
ciphertext to span at least pn_offset + 4 + 16 bytes. The
first byte's bits 0-4 (reserved + key-phase + pn_length) are
cleared by header protection.

### `protect_1rtt_packet`

```mojo
def protect_1rtt_packet(short_header_prefix: Span[UInt8], packet_number: UInt64, pn_length: Int, plaintext: Span[UInt8], writer_secret: Span[UInt8], aead_choice: Int = Int(0)) -> List[UInt8]
```

Build a fully protected 1-RTT packet ready for ``sendto``.

``short_header_prefix`` is the unprotected short-header bytes
from the first byte through the DCID (i.e. the output of
:func:`flare.quic.packet.encode_short_header`). The function
appends the packet-number bytes, AEAD-encrypts the plaintext
with the unprotected short header as AAD, then applies header
protection.

Header protection masks five bits of the first byte per
RFC 9001 §5.4.1 (reserved 4-3 + key-phase 2 + pn-length 1-0),
versus the long header's four bits.
