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.

140 lines
6.7 KiB

import CommonCrypto
import CryptoKit
import Foundation
/// AES-128 in ECB mode, used as the block function for counter mode (CryptoKit has no AES-CTR).
final class AESBlock: @unchecked Sendable {
private var cryptor: CCCryptorRef?
init(key: [UInt8]) {
precondition(key.count == 16)
CCCryptorCreate(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES), CCOptions(kCCOptionECBMode),
key, key.count, nil, &cryptor)
}
deinit { if let cryptor { CCCryptorRelease(cryptor) } }
/// Counter-mode keystream: AES(iv), AES(iv+1), … (counter in the last 16 bits, RFC 3711 4.1.1).
func keystream(iv: [UInt8], count: Int) -> [UInt8] {
let blocks = (count + 15) / 16
var input = [UInt8](repeating: 0, count: blocks * 16)
for b in 0..<blocks {
var block = iv
let counter = (UInt32(block[14]) << 8 | UInt32(block[15])) &+ UInt32(b)
block[14] = UInt8((counter >> 8) & 0xFF)
block[15] = UInt8(counter & 0xFF)
input.replaceSubrange(b * 16..<(b + 1) * 16, with: block)
}
var output = [UInt8](repeating: 0, count: input.count)
var moved = 0
CCCryptorUpdate(cryptor, input, input.count, &output, output.count, &moved)
return Array(output.prefix(count))
}
}
/// SRTP/SRTCP with the profile SRTP_AES128_CM_HMAC_SHA1_80 (RFC 3711, RFC 5764).
/// One context per direction; this side only sends RTP and reads RTCP feedback.
final class SRTPContext: @unchecked Sendable {
struct SessionKeys {
let cipher: AESBlock
let salt: [UInt8]
let auth: SymmetricKey
}
static let tagLength = 10
let rtp: SessionKeys
let rtcp: SessionKeys
private var rolloverCounters: [UInt32: (roc: UInt32, lastSeq: UInt16)] = [:]
private var srtcpIndex: UInt32 = 0
init(masterKey: [UInt8], masterSalt: [UInt8]) {
rtp = Self.derive(masterKey: masterKey, masterSalt: masterSalt, labels: (0, 1, 2))
rtcp = Self.derive(masterKey: masterKey, masterSalt: masterSalt, labels: (3, 4, 5))
}
/// Key derivation (RFC 3711 4.3) with key_derivation_rate 0.
static func deriveKey(masterKey: [UInt8], masterSalt: [UInt8], label: UInt8, length: Int) -> [UInt8] {
var x = masterSalt + [0, 0]
x[7] ^= label
return AESBlock(key: masterKey).keystream(iv: x, count: length)
}
private static func derive(masterKey: [UInt8], masterSalt: [UInt8], labels: (UInt8, UInt8, UInt8)) -> SessionKeys {
SessionKeys(
cipher: AESBlock(key: deriveKey(masterKey: masterKey, masterSalt: masterSalt, label: labels.0, length: 16)),
salt: deriveKey(masterKey: masterKey, masterSalt: masterSalt, label: labels.2, length: 14),
auth: SymmetricKey(data: deriveKey(masterKey: masterKey, masterSalt: masterSalt, label: labels.1, length: 20)))
}
/// IV = salt·2^16 ⊕ SSRC·2^64 ⊕ index·2^16.
private static func iv(salt: [UInt8], ssrc: UInt32, index: UInt64) -> [UInt8] {
var iv = salt + [0, 0]
for i in 0..<4 { iv[4 + i] ^= UInt8((ssrc >> UInt32(24 - 8 * i)) & 0xFF) }
for i in 0..<6 { iv[8 + i] ^= UInt8((index >> UInt64(40 - 8 * i)) & 0xFF) }
return iv
}
/// Encrypts and authenticates one RTP packet. Packets per SSRC must be passed in sending order.
func protectRTP(_ packet: [UInt8]) -> [UInt8] {
guard packet.count >= 12 else { return packet }
let headerLength = Self.rtpHeaderLength(packet)
let seq = UInt16(packet[2]) << 8 | UInt16(packet[3])
let ssrc = UInt32(packet[8]) << 24 | UInt32(packet[9]) << 16 | UInt32(packet[10]) << 8 | UInt32(packet[11])
var state = rolloverCounters[ssrc] ?? (0, seq)
if seq < state.lastSeq && state.lastSeq - seq > 0x8000 { state.roc &+= 1 }
state.lastSeq = seq
rolloverCounters[ssrc] = state
let index = UInt64(state.roc) << 16 | UInt64(seq)
var out = packet
let payloadCount = packet.count - headerLength
if payloadCount > 0 {
let stream = rtp.cipher.keystream(iv: Self.iv(salt: rtp.salt, ssrc: ssrc, index: index), count: payloadCount)
for i in 0..<payloadCount { out[headerLength + i] ^= stream[i] }
}
let roc = [UInt8(state.roc >> 24), UInt8((state.roc >> 16) & 0xFF), UInt8((state.roc >> 8) & 0xFF), UInt8(state.roc & 0xFF)]
let tag = HMAC<Insecure.SHA1>.authenticationCode(for: out + roc, using: rtp.auth)
return out + Array(tag).prefix(Self.tagLength)
}
/// Encrypts and authenticates one (compound) RTCP packet.
func protectRTCP(_ packet: [UInt8]) -> [UInt8] {
guard packet.count >= 8 else { return packet }
srtcpIndex = (srtcpIndex + 1) & 0x7FFF_FFFF
let ssrc = UInt32(packet[4]) << 24 | UInt32(packet[5]) << 16 | UInt32(packet[6]) << 8 | UInt32(packet[7])
var out = packet
let stream = rtcp.cipher.keystream(iv: Self.iv(salt: rtcp.salt, ssrc: ssrc, index: UInt64(srtcpIndex)), count: packet.count - 8)
for i in 0..<stream.count { out[8 + i] ^= stream[i] }
let e = srtcpIndex | 0x8000_0000
out += [UInt8(e >> 24), UInt8((e >> 16) & 0xFF), UInt8((e >> 8) & 0xFF), UInt8(e & 0xFF)]
let tag = HMAC<Insecure.SHA1>.authenticationCode(for: out, using: rtcp.auth)
return out + Array(tag).prefix(Self.tagLength)
}
/// Verifies and decrypts one SRTCP packet; nil if the tag does not match.
func unprotectRTCP(_ packet: [UInt8]) -> [UInt8]? {
guard packet.count >= 8 + 4 + Self.tagLength else { return nil }
let authEnd = packet.count - Self.tagLength
let tag = Array(packet[authEnd...])
let expected = Array(HMAC<Insecure.SHA1>.authenticationCode(for: packet[0..<authEnd], using: rtcp.auth)).prefix(Self.tagLength)
guard constantTimeEqual(tag, Array(expected)) else { return nil }
let e = UInt32(packet[authEnd - 4]) << 24 | UInt32(packet[authEnd - 3]) << 16 | UInt32(packet[authEnd - 2]) << 8 | UInt32(packet[authEnd - 1])
var out = Array(packet[0..<authEnd - 4])
guard e & 0x8000_0000 != 0 else { return out }
let ssrc = UInt32(packet[4]) << 24 | UInt32(packet[5]) << 16 | UInt32(packet[6]) << 8 | UInt32(packet[7])
let stream = rtcp.cipher.keystream(iv: Self.iv(salt: rtcp.salt, ssrc: ssrc, index: UInt64(e & 0x7FFF_FFFF)), count: out.count - 8)
for i in 0..<stream.count { out[8 + i] ^= stream[i] }
return out
}
static func rtpHeaderLength(_ packet: [UInt8]) -> Int {
var length = 12 + Int(packet[0] & 0x0F) * 4
if packet[0] & 0x10 != 0, packet.count >= length + 4 {
length += 4 + (Int(packet[length + 2]) << 8 | Int(packet[length + 3])) * 4
}
return min(length, packet.count)
}
}

Powered by TurnKey Linux.