removeHeaderProtection function

int removeHeaderProtection({
  1. required Uint8List array,
  2. required int pnOffset,
  3. required Uint8List hpKey,
})

Reverses the Header Protection mechanism, unmasking the first header byte and the Packet Number field.

The array is modified in place. Returns the Packet Number Length (1, 2, 3, or 4 bytes).

Implementation

int removeHeaderProtection({
  required Uint8List array,
  required int pnOffset,
  required Uint8List hpKey,
}) {
  if (array.isEmpty) {
    throw StateError("Packet is empty");
  }

  // Header form bit is NOT header-protected, so this is safe now.
  final isShort = (array[0] & 0x80) == 0;

  const sampleLength = 16;
  final sampleOffset = pnOffset + 4;

  if (sampleOffset + sampleLength > array.length) {
    throw StateError(
      "Not enough bytes for header protection sample "
      "(need ${sampleOffset + sampleLength}, have ${array.length})",
    );
  }

  final sample = array.sublist(sampleOffset, sampleOffset + sampleLength);

  // IMPORTANT:
  // Use the SAME helper/signature as the encrypt path.
  final maskFull = aesEcbEncrypt(hpKey, sample);
  final mask = maskFull.sublist(0, 5);

  // ------------------------------------------------------------
  // Unmask first byte
  // ------------------------------------------------------------
  if (isShort) {
    // Short header: unmask low 5 bits
    array[0] ^= (mask[0] & 0x1f);

    // Reserved bits for short header are bits 4-3 and MUST be zero
    final reservedBits = (array[0] >> 3) & 0x03;
    if (reservedBits != 0) {
      throw StateError(
        "Invalid short-header reserved bits after HP removal: "
        "${reservedBits.toRadixString(2).padLeft(2, '0')} "
        "(firstByte=0x${array[0].toRadixString(16)})",
      );
    }
  } else {
    // Long header: unmask low 4 bits
    array[0] ^= (mask[0] & 0x0f);

    // Reserved bits for long header are bits 3-2 and MUST be zero
    final reservedBits = (array[0] >> 2) & 0x03;
    if (reservedBits != 0) {
      throw StateError(
        "Invalid long-header reserved bits after HP removal: "
        "${reservedBits.toRadixString(2).padLeft(2, '0')} "
        "(firstByte=0x${array[0].toRadixString(16)})",
      );
    }
  }

  // ------------------------------------------------------------
  // Determine packet number length from unmasked first byte
  // ------------------------------------------------------------
  final pnLength = (array[0] & 0x03) + 1;

  if (pnOffset + pnLength > array.length) {
    throw StateError(
      "Packet number field extends beyond packet length after HP removal "
      "(pnOffset=$pnOffset, pnLength=$pnLength, packetLen=${array.length})",
    );
  }

  // ------------------------------------------------------------
  // Unmask packet number bytes
  // ------------------------------------------------------------
  for (int i = 0; i < pnLength; i++) {
    array[pnOffset + i] ^= mask[1 + i];
  }

  return pnLength;
}