You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

142 lines
5.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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..<offset])
setLength(&prefix, offset - 20 + 24)
let mac = HMAC<Insecure.SHA1>.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..<offset])
setLength(&prefix, offset - 20 + 8)
let value = CRC32.checksum(prefix) ^ 0x5354_554E
return [UInt8(value >> 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
}
}

Powered by TurnKey Linux.