From 96da3d6eb81addec35a825ff1b864fd2f708b953 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 1 Oct 2026 12:45:45 -0400 Subject: [PATCH] Create the header protection cryptor once per key `SecFramerAESGCM.headerProtection` computed every mask with one-shot `CCCrypt`, which creates and releases a cryptor per call. That is three allocations each time a packet is sealed or opened, six per packet for a connection that both sends and receives, and the largest single source of allocations on the QUIC data path. Header protection uses AES-ECB, which carries no state from one block to the next, so `SecFramerKeys` now creates a `HeaderProtectionCryptor` when the keys are installed and every packet reuses it. Creating the cryptors costs a connection pair 48 allocations up front, against the 39 its handshake packets used to spend, so a connection that does nothing but handshake pays about 9 more. Measured on my Mac against `main` with the package's QUIC benchmark tools. Allocation counts come from full malloc stack logging, which records every allocation: QUICTransfer -size 1200, per message 24.37 -> 18.09 QUICTransfer, per 500 KB transfer 6,187.6 -> 3,597.6 QUICHandshake, per connection 1,947.4 -> 1,955.1 QUICStreamLoad, per stream 111.8 -> 100.2 Wall-clock time comes from running `main`, this change and the other changes measured alongside it in a rotating order for 9 rounds, and comparing each run with `main`'s in the same round. Changes moved paths they do not touch by up to about 1.3%, so differences that size count as noise. The 500 KB transfers took 2.1% less time and the stream load 4.8% less, faster in all 9 rounds each; the 1,200-byte messages and the handshakes did not change beyond noise. --- Sources/SwiftNetwork/QUIC/Protector.swift | 94 +++++++++++++++++------ 1 file changed, 71 insertions(+), 23 deletions(-) diff --git a/Sources/SwiftNetwork/QUIC/Protector.swift b/Sources/SwiftNetwork/QUIC/Protector.swift index 649f5cbe..3d35311c 100644 --- a/Sources/SwiftNetwork/QUIC/Protector.swift +++ b/Sources/SwiftNetwork/QUIC/Protector.swift @@ -155,6 +155,64 @@ enum TLSCipherSuite: CaseIterable { } } +#if canImport(CommonCrypto) +/// An AES-ECB cryptor for computing header protection masks, created once per key. +/// +/// Creating a cryptor costs several allocations, which `CCCrypt` would pay on every packet. ECB carries no state from +/// one block to the next, so a single cryptor can encrypt every sample its key protects. +struct HeaderProtectionCryptor: ~Copyable { + private let cryptor: CCCryptorRef? + + /// Creates a cryptor for `key`, or one that fails every operation when `key` is `nil` or unusable. + @available(Network 0.1.0, *) + init(key: SymmetricKey?) { + guard let key else { + self.cryptor = nil + return + } + var cryptor: CCCryptorRef? + let status = key.withUnsafeBytes { keyBuffer in + CCCryptorCreate( + CCOperation(kCCEncrypt), + CCAlgorithm(kCCAlgorithmAES), + CCOptions(kCCOptionECBMode), + keyBuffer.baseAddress, + keyBuffer.count, + nil, + &cryptor + ) + } + self.cryptor = status == kCCSuccess ? cryptor : nil + } + + deinit { + if let cryptor { + CCCryptorRelease(cryptor) + } + } + + /// Encrypts the 16-byte block at `input` into `output`. + func encryptBlock(_ input: UnsafeRawPointer, into output: UnsafeMutableRawPointer) -> CCCryptorStatus { + guard let cryptor else { + return CCCryptorStatus(kCCParamError) + } + var bytesEncrypted = 0 + let status = CCCryptorUpdate( + cryptor, + input, + kCCBlockSizeAES128, + output, + kCCBlockSizeAES128, + &bytesEncrypted + ) + guard status == kCCSuccess else { + return status + } + return bytesEncrypted == kCCBlockSizeAES128 ? status : CCCryptorStatus(kCCAlignmentError) + } +} +#endif + @available(Network 0.1.0, *) struct SecFramerKeys: ~Copyable { enum KeyType { @@ -169,6 +227,9 @@ struct SecFramerKeys: ~Copyable { let type: KeyType let isEmpty: Bool let log: LogPrefixer + #if canImport(CommonCrypto) + let headerProtectionCryptor: HeaderProtectionCryptor + #endif init( key: SymmetricKey, @@ -188,6 +249,11 @@ struct SecFramerKeys: ~Copyable { self.type = type self.log = log self.isEmpty = isEmpty + #if canImport(CommonCrypto) + self.headerProtectionCryptor = HeaderProtectionCryptor( + key: type == .aesGCM && !isEmpty ? headerProtectionKey : nil + ) + #endif } var size: Int { key.bitCount @@ -329,29 +395,11 @@ struct SecFramerAESGCM: ~Copyable, SecFramerProtocol { #if canImport(CommonCrypto) let packetBuffer = buffer.withUnsafeMutableBytes { $0 } - let result = keys.headerProtectionKey.withUnsafeBytes { headerKeyBuffer in - Swift.withUnsafeBytes(of: keys.iv) { ivBuffer in - mask.withUnsafeMutableBytes { maskBuffer in - let operation = CCOperation(kCCEncrypt) - let algorithm = CCAlgorithm(kCCAlgorithmAES) - let options = CCOptions(kCCOptionECBMode) - var bytesEncrypted = 0 - - return CCCrypt( - operation, - algorithm, - options, - headerKeyBuffer.baseAddress!, - keys.headerProtectionKey.bitCount / 8, - ivBuffer.baseAddress!, - packetBuffer.baseAddress! + packet.sampleRange.lowerBound, - packet.sampleRange.count, - maskBuffer.baseAddress!, - kCCBlockSizeAES128, - &bytesEncrypted - ) - } - } + let result = mask.withUnsafeMutableBytes { maskBuffer in + keys.headerProtectionCryptor.encryptBlock( + packetBuffer.baseAddress! + packet.sampleRange.lowerBound, + into: maskBuffer.baseAddress! + ) } guard result == kCCSuccess else { keys.log.error("Unable to \(loggingOperation) header: \(result)")