import CryptoKit import Foundation /// The subset of STUN (RFC 5389) an ICE-lite agent needs: answer Binding requests /// with short-term credentials (RFC 8445), MESSAGE-INTEGRITY and FINGERPRINT. enum STUN { static let magicCookie: UInt32 = 0x2112_A442 static let bindingRequest: UInt16 = 0x0001 static let bindingSuccess: UInt16 = 0x0101 enum Attribute: UInt16 { case username = 0x0006 case messageIntegrity = 0x0008 case xorMappedAddress = 0x0020 case priority = 0x0024 case useCandidate = 0x0025 case fingerprint = 0x8028 case iceControlled = 0x8029 case iceControlling = 0x802A } struct Message { var type: UInt16 var transactionID: [UInt8] /// Attributes in wire order, with the offset of each attribute header in the packet. var attributes: [(type: UInt16, value: [UInt8], offset: Int)] func value(_ attribute: Attribute) -> [UInt8]? { attributes.first { $0.type == attribute.rawValue }?.value } var username: String? { value(.username).flatMap { String(bytes: $0, encoding: .utf8) } } } /// RFC 7983 demultiplexing: first byte 0–3 and the magic cookie. static func looksLikeSTUN(_ packet: [UInt8]) -> Bool { packet.count >= 20 && packet[0] < 4 && packet[4] == 0x21 && packet[5] == 0x12 && packet[6] == 0xA4 && packet[7] == 0x42 } static func parse(_ packet: [UInt8]) -> Message? { guard looksLikeSTUN(packet) else { return nil } var reader = ByteReader(packet) do { let type = try reader.u16() let length = Int(try reader.u16()) guard length + 20 == packet.count, length.isMultiple(of: 4) else { return nil } try reader.skip(4) let transactionID = try reader.take(12) var attributes: [(UInt16, [UInt8], Int)] = [] while !reader.isAtEnd { let offset = reader.offset let attrType = try reader.u16() let attrLength = Int(try reader.u16()) let value = try reader.take(attrLength) try reader.skip((4 - attrLength % 4) % 4) attributes.append((attrType, value, offset)) } return Message(type: type, transactionID: transactionID, attributes: attributes) } catch { return nil } } /// Checks MESSAGE-INTEGRITY (HMAC-SHA1 with `key`) and, if present, FINGERPRINT. static func verify(_ packet: [UInt8], message: Message, key: [UInt8]) -> Bool { if let fp = message.attributes.first(where: { $0.type == Attribute.fingerprint.rawValue }) { guard fp.value == fingerprint(packet, upTo: fp.offset) else { return false } } guard let mi = message.attributes.first(where: { $0.type == Attribute.messageIntegrity.rawValue }) else { return false } return constantTimeEqual(mi.value, integrity(packet, upTo: mi.offset, key: key)) } /// Builds a Binding success response with XOR-MAPPED-ADDRESS, MESSAGE-INTEGRITY and FINGERPRINT. static func bindingResponse(transactionID: [UInt8], address: [UInt8], port: UInt16, key: [UInt8]) -> [UInt8] { var w = ByteWriter(capacity: 80) w.u16(bindingSuccess) w.u16(0) // length, patched below w.u32(magicCookie) w.append(transactionID) let cookie: [UInt8] = [0x21, 0x12, 0xA4, 0x42] w.u16(Attribute.xorMappedAddress.rawValue) w.u16(address.count == 4 ? 8 : 20) w.u8(0) w.u8(address.count == 4 ? 0x01 : 0x02) w.u16(port ^ 0x2112) let mask = cookie + transactionID w.append(address.enumerated().map { $0.element ^ mask[$0.offset] }) var bytes = w.bytes let miOffset = bytes.count let mac = integrity(bytes, upTo: miOffset, key: key) var tail = ByteWriter() tail.u16(Attribute.messageIntegrity.rawValue) tail.u16(20) tail.append(mac) bytes += tail.bytes let fpOffset = bytes.count let crc = fingerprint(bytes, upTo: fpOffset) bytes += [0x80, 0x28, 0x00, 0x04] + crc setLength(&bytes, bytes.count - 20) return bytes } /// HMAC-SHA1 over the message up to `offset`, with the header length covering the MESSAGE-INTEGRITY attribute. private static func integrity(_ packet: [UInt8], upTo offset: Int, key: [UInt8]) -> [UInt8] { var prefix = Array(packet[0...authenticationCode(for: prefix, using: SymmetricKey(data: key)) return Array(mac) } /// CRC-32 over the message up to `offset`, length covering FINGERPRINT, XOR 0x5354554E. private static func fingerprint(_ packet: [UInt8], upTo offset: Int) -> [UInt8] { var prefix = Array(packet[0..> 24), UInt8((value >> 16) & 0xFF), UInt8((value >> 8) & 0xFF), UInt8(value & 0xFF)] } private static func setLength(_ bytes: inout [UInt8], _ length: Int) { bytes[2] = UInt8(length >> 8) bytes[3] = UInt8(length & 0xFF) } } enum CRC32 { private static let table: [UInt32] = (0..<256).map { n in var c = UInt32(n) for _ in 0..<8 { c = (c & 1) != 0 ? 0xEDB8_8320 ^ (c >> 1) : c >> 1 } return c } static func checksum(_ bytes: [UInt8]) -> UInt32 { var crc: UInt32 = 0xFFFF_FFFF for b in bytes { crc = table[Int((crc ^ UInt32(b)) & 0xFF)] ^ (crc >> 8) } return crc ^ 0xFFFF_FFFF } }