stremio/apps/ios/VMC/Playback/CencMp4Decryptor.swift

756 lines
31 KiB
Swift

import Foundation
/// In-place CENC/CBCS decryptie van gefragmenteerde MP4 (init + media segments).
enum CencMp4Decryptor {
struct TrackCrypto {
var kid: Data
var perSampleIvSize: Int
var constantIv: Data?
var scheme: String // cenc | cbcs | cbc1 | cens
var cryptByteBlock: Int
var skipByteBlock: Int
}
struct SessionState {
var byTrackId: [UInt32: TrackCrypto] = [:]
var fallback: TrackCrypto?
}
/// Diagnose-hook (alleen aanroepen onder de crypto-lock van de proxy).
static var diagTag = ""
static var diagSink: ((String) -> Void)?
private static var diagCounts: [String: Int] = [:]
private static func diag(_ message: @autoclosure () -> String) {
guard let sink = diagSink else { return }
let n = diagCounts[diagTag, default: 0]
guard n < 8 else { return }
diagCounts[diagTag] = n + 1
sink("decrypt[\(diagTag)] \(message())")
}
static func resetDiag() {
diagCounts.removeAll()
}
private static func hex(_ d: Data) -> String {
d.map { String(format: "%02x", $0) }.joined()
}
@discardableResult
static func process(data: inout Data, keys: ClearKeyKeyMap, state: inout SessionState) -> Bool {
guard !data.isEmpty, !keys.isEmpty else { return false }
// Nooit crashen op corrupte segmenten — liever ongedecrypt doorgeven dan app-kill.
var touched = false
if containsBox(data, type: "moov") {
harvestAndClearInit(&data, keys: keys, state: &state)
touched = true
}
if containsBox(data, type: "moof") {
decryptFragments(&data, keys: keys, state: state)
touched = true
}
return touched
}
// MARK: - Box helpers
private struct Box {
let type: String
let start: Int
let headerSize: Int
let size: Int
var payloadStart: Int { start + headerSize }
var payloadEnd: Int { start + size }
var payloadLength: Int { size - headerSize }
}
private static func containsBox(_ data: Data, type: String) -> Bool {
findBoxes(data, type: type, recursive: true).isEmpty == false
}
private static func findBoxes(_ data: Data, type: String, recursive: Bool) -> [Box] {
var out: [Box] = []
walk(data, from: 0, to: data.count, recursive: recursive) { box in
if box.type == type { out.append(box) }
return true
}
return out
}
private static func walk(
_ data: Data,
from: Int,
to: Int,
recursive: Bool,
visit: (Box) -> Bool
) {
var offset = from
let containers: Set<String> = [
"moov", "trak", "mdia", "minf", "stbl", "moof", "traf",
"mvex", "edts", "sinf", "schi", "dinf", "udta", "meta",
// Sample-entries bevatten sinf/avcC; stsd is FullBox met entry_count.
"stsd", "dref",
"encv", "enca",
"avc1", "avc3", "hev1", "hvc1", "dvh1", "mp4a",
]
while offset + 8 <= to {
guard let box = readBox(data, at: offset, limit: to) else { break }
let cont = visit(box)
if !cont { return }
if recursive, containers.contains(box.type), box.payloadLength > 0 {
let childFrom = walkChildrenStart(box)
if childFrom < box.payloadEnd {
walk(data, from: childFrom, to: box.payloadEnd, recursive: true, visit: visit)
}
}
offset = box.payloadEnd
if box.size <= 0 { break }
}
}
/// Skip FullBox / SampleEntry vaste headers zodat child-boxes (sinf, tenc, avcC) bereikbaar zijn.
private static func walkChildrenStart(_ box: Box) -> Int {
switch box.type {
case "stsd", "dref":
// FullBox (4) + entry_count (4)
return box.payloadStart + 8
case "meta":
return box.payloadStart + 4
case "encv", "avc1", "avc3", "hev1", "hvc1", "dvh1":
// VisualSampleEntry: boxhdr(8) + SampleEntry(8) + visual(70) = 86
return box.start + 86
case "enca", "mp4a":
// AudioSampleEntry: boxhdr(8) + SampleEntry(8) + audio(20) = 36
return box.start + 36
default:
return box.payloadStart
}
}
private static func readBox(_ data: Data, at offset: Int, limit: Int) -> Box? {
guard offset + 8 <= limit else { return nil }
var size = Int(readU32(data, offset))
let type = readFourCC(data, offset + 4)
var header = 8
if size == 1 {
guard offset + 16 <= limit else { return nil }
let large = readU64(data, offset + 8)
size = Int(large)
header = 16
} else if size == 0 {
size = limit - offset
}
guard size >= header, offset + size <= limit else { return nil }
return Box(type: type, start: offset, headerSize: header, size: size)
}
private static func readU16(_ data: Data, _ o: Int) -> UInt16 {
guard o >= 0, o + 2 <= data.count else { return 0 }
return UInt16(data[o]) << 8 | UInt16(data[o + 1])
}
private static func readU32(_ data: Data, _ o: Int) -> UInt32 {
guard o >= 0, o + 4 <= data.count else { return 0 }
return UInt32(data[o]) << 24 | UInt32(data[o + 1]) << 16 | UInt32(data[o + 2]) << 8 | UInt32(data[o + 3])
}
private static func readU64(_ data: Data, _ o: Int) -> UInt64 {
guard o >= 0, o + 8 <= data.count else { return 0 }
return (UInt64(readU32(data, o)) << 32) | UInt64(readU32(data, o + 4))
}
private static func readFourCC(_ data: Data, _ o: Int) -> String {
guard o >= 0, o + 4 <= data.count else { return "????" }
return String(bytes: data[o..<(o + 4)], encoding: .ascii) ?? "????"
}
private static func writeFourCC(_ data: inout Data, _ o: Int, _ s: String) {
let chars = Array(s.utf8)
guard chars.count == 4, o + 4 <= data.count else { return }
for i in 0..<4 { data[o + i] = chars[i] }
}
// MARK: - Init segment
private static func harvestAndClearInit(_ data: inout Data, keys: ClearKeyKeyMap, state: inout SessionState) {
// Elke init is de bron van waarheid voor deze lane (CMAF track_ID is vaak 1 voor A én V).
state.byTrackId.removeAll(keepingCapacity: true)
state.fallback = nil
// Track id from tkhd, crypto from sinf/tenc/frma/schm inside same trak.
for trak in findBoxes(data, type: "trak", recursive: true) {
var trackId: UInt32?
walk(data, from: trak.payloadStart, to: trak.payloadEnd, recursive: true) { box in
if box.type == "tkhd", box.payloadLength >= 8 {
let version = Int(data[box.payloadStart])
let idOff = box.payloadStart + (version == 1 ? 20 : 12)
if idOff + 4 <= box.payloadEnd {
trackId = readU32(data, idOff)
}
}
return true
}
var crypto: TrackCrypto?
var originalFormat: String?
walk(data, from: trak.payloadStart, to: trak.payloadEnd, recursive: true) { box in
if box.type == "frma", box.payloadLength >= 4 {
originalFormat = readFourCC(data, box.payloadStart)
}
if box.type == "schm", box.payloadLength >= 8 {
// version/flags (4) + scheme_type (4)
let scheme = readFourCC(data, box.payloadStart + 4)
if crypto == nil {
crypto = TrackCrypto(
kid: Data(count: 16),
perSampleIvSize: 8,
constantIv: nil,
scheme: scheme,
cryptByteBlock: 0,
skipByteBlock: 0
)
} else {
crypto?.scheme = scheme
}
}
if box.type == "tenc", box.payloadLength >= 20 {
let version = Int(data[box.payloadStart])
let base = box.payloadStart + 4 // na version+flags
var o = base
o += 1 // reserved
var cryptBlocks = 0
var skipBlocks = 0
if version >= 1 {
let pattern = data[o]
cryptBlocks = Int((pattern >> 4) & 0x0F)
skipBlocks = Int(pattern & 0x0F)
}
o += 1 // reserved (v0) of pattern (v1)
let isProtectedIndex = o
let isProtected = Int(data[o]); o += 1
let ivSize = Int(data[o]); o += 1
let kid = data.subdata(in: o..<(o + 16)); o += 16
_ = isProtected
var constantIv: Data?
if ivSize == 0, o < box.payloadEnd {
let constSize = Int(data[o]); o += 1
if constSize > 0, o + constSize <= box.payloadEnd {
constantIv = data.subdata(in: o..<(o + constSize))
}
}
crypto = TrackCrypto(
kid: kid,
perSampleIvSize: ivSize,
constantIv: constantIv,
scheme: crypto?.scheme ?? "cenc",
cryptByteBlock: cryptBlocks,
skipByteBlock: skipBlocks
)
data[isProtectedIndex] = 0
}
return true
}
// Replace encv/enca with original format (same 4 bytes → no size rewrite).
// Mark sinf as 'free' zodat VLC geen DRM-sample-entry meer ziet (anders: meteen Gestopt).
if let originalFormat {
for stsd in findBoxes(data, type: "stsd", recursive: true) {
guard stsd.start >= trak.start, stsd.payloadEnd <= trak.payloadEnd else { continue }
var o = stsd.payloadStart + 8 // FullBox (4) + entry_count (4)
while o + 8 <= stsd.payloadEnd {
guard let entry = readBox(data, at: o, limit: stsd.payloadEnd) else { break }
if entry.type == "encv" || entry.type == "enca" {
writeFourCC(&data, entry.start + 4, originalFormat)
}
o = entry.payloadEnd
}
}
}
walk(data, from: trak.payloadStart, to: trak.payloadEnd, recursive: true) { box in
if box.type == "sinf" {
writeFourCC(&data, box.start + 4, "free")
}
return true
}
if let crypto {
diag("init track=\(trackId ?? 0) fmt=\(originalFormat ?? "-") scheme=\(crypto.scheme) pat=\(crypto.cryptByteBlock):\(crypto.skipByteBlock) iv=\(crypto.perSampleIvSize) civ=\(crypto.constantIv?.count ?? 0) kid=\(hex(crypto.kid)) exact=\(keys.hasExactKey(forKid: crypto.kid)) keys=\(keys.uniqueKeyCount)")
if let trackId {
state.byTrackId[trackId] = crypto
}
// Fallback: prefer video-achtige crypto (cbcs / pattern) boven pure audio-cenc.
let looksVideo = crypto.scheme == "cbcs" || crypto.scheme == "cbc1"
|| crypto.cryptByteBlock > 0 || crypto.constantIv != nil
if state.fallback == nil || looksVideo {
state.fallback = crypto
}
_ = keys.key(forKid: crypto.kid)
} else {
diag("init track=\(trackId ?? 0) fmt=\(originalFormat ?? "-") clear (geen tenc)")
}
}
}
// MARK: - Media fragments
private static func decryptFragments(_ data: inout Data, keys: ClearKeyKeyMap, state: SessionState) {
// Pair each moof with the following mdat.
var offset = 0
while offset + 8 <= data.count {
guard let box = readBox(data, at: offset, limit: data.count) else { break }
if box.type == "moof" {
let moof = box
var mdat: Box?
var scan = moof.payloadEnd
while scan + 8 <= data.count {
guard let next = readBox(data, at: scan, limit: data.count) else { break }
if next.type == "mdat" {
mdat = next
break
}
if next.type == "moof" { break }
scan = next.payloadEnd
}
if let mdat {
decryptMoofMdat(moof: moof, mdat: mdat, data: &data, keys: keys, state: state)
}
}
offset = box.payloadEnd
}
}
private static func decryptMoofMdat(
moof: Box,
mdat: Box,
data: inout Data,
keys: ClearKeyKeyMap,
state: SessionState
) {
let trafs = findBoxesInRange(data, type: "traf", from: moof.payloadStart, to: moof.payloadEnd)
var mdatCursor = mdat.payloadStart
for traf in trafs {
var trackId: UInt32 = 0
var defaultSampleSize: UInt32 = 0
var baseDataOffset: Int64 = Int64(mdat.payloadStart)
var dataOffsetPresent = false
var sampleSizes: [Int] = []
var sencSamples: [SencSample] = []
var perSampleIvSize = 8
var tfhdFlagsSeen: UInt32 = 0
var trunFlagsSeen: UInt32 = 0
if let known = state.byTrackId.values.first, state.byTrackId.count == 1 {
perSampleIvSize = known.perSampleIvSize
}
// tfhd
for tfhd in findBoxesInRange(data, type: "tfhd", from: traf.payloadStart, to: traf.payloadEnd) {
guard tfhd.payloadLength >= 8 else { continue }
let versionFlags = readU32(data, tfhd.payloadStart)
let flags = versionFlags & 0x00FF_FFFF
var o = tfhd.payloadStart + 4
trackId = readU32(data, o); o += 4
if let crypto = state.byTrackId[trackId] {
perSampleIvSize = crypto.perSampleIvSize
}
if flags & 0x000001 != 0 { // base-data-offset
guard o + 8 <= tfhd.payloadEnd else { break }
baseDataOffset = Int64(readU64(data, o)); o += 8
dataOffsetPresent = true
}
if flags & 0x000002 != 0 { // sample-description-index
guard o + 4 <= tfhd.payloadEnd else { break }
o += 4
}
if flags & 0x000008 != 0 { // default-sample-duration
guard o + 4 <= tfhd.payloadEnd else { break }
o += 4
}
if flags & 0x000010 != 0 { // default-sample-size
guard o + 4 <= tfhd.payloadEnd else { break }
defaultSampleSize = readU32(data, o); o += 4
}
tfhdFlagsSeen = flags
_ = versionFlags
}
// trun
for trun in findBoxesInRange(data, type: "trun", from: traf.payloadStart, to: traf.payloadEnd) {
guard trun.payloadLength >= 8 else { continue }
let versionFlags = readU32(data, trun.payloadStart)
let version = Int(versionFlags >> 24)
let flags = versionFlags & 0x00FF_FFFF
trunFlagsSeen = flags
var o = trun.payloadStart + 4
let sampleCount = Int(readU32(data, o)); o += 4
guard sampleCount >= 0, sampleCount < 100_000 else { continue }
var dataOffset: Int32 = 0
if flags & 0x000001 != 0 {
guard o + 4 <= trun.payloadEnd else { break }
dataOffset = Int32(bitPattern: readU32(data, o)); o += 4
dataOffsetPresent = true
}
if flags & 0x000004 != 0 {
guard o + 4 <= trun.payloadEnd else { break }
o += 4
}
sampleSizes.removeAll(keepingCapacity: true)
for _ in 0..<sampleCount {
if flags & 0x000100 != 0 {
guard o + 4 <= trun.payloadEnd else { break }
o += 4
}
var size = Int(defaultSampleSize)
if flags & 0x000200 != 0 {
guard o + 4 <= trun.payloadEnd else { break }
size = Int(readU32(data, o)); o += 4
}
sampleSizes.append(max(0, size))
if flags & 0x000400 != 0 {
guard o + 4 <= trun.payloadEnd else { break }
o += 4
}
if flags & 0x000800 != 0 {
guard o + 4 <= trun.payloadEnd else { break }
o += 4
}
}
if dataOffsetPresent {
// data_offset is relative to moof start when base-data-offset-present is NOT set;
// when base-data-offset is set, absolute = base + dataOffset.
// Common CMAF: no base-data-offset, data_offset relative to moof.
let tfhd = findBoxesInRange(data, type: "tfhd", from: traf.payloadStart, to: traf.payloadEnd).first
let tfhdFlags: UInt32 = {
guard let tfhd else { return 0 }
return readU32(data, tfhd.payloadStart) & 0x00FF_FFFF
}()
if tfhdFlags & 0x000001 != 0 {
mdatCursor = Int(baseDataOffset + Int64(dataOffset))
} else {
mdatCursor = moof.start + Int(dataOffset)
}
}
_ = version
}
// Prefer track-crypto uit DÉZE lane. Fallback alleen binnen dezelfde lane.
let crypto: TrackCrypto? = state.byTrackId[trackId]
?? (state.byTrackId.count == 1 ? state.byTrackId.values.first : nil)
?? state.fallback
if let crypto {
perSampleIvSize = crypto.perSampleIvSize
}
for senc in findBoxesInRange(data, type: "senc", from: traf.payloadStart, to: traf.payloadEnd) {
sencSamples = parseSenc(data, box: senc, perSampleIvSize: perSampleIvSize, constantIv: crypto?.constantIv)
}
// saiz/saio fallback when no senc
if sencSamples.isEmpty {
sencSamples = parseSaizSaio(
data,
traf: traf,
moof: moof,
perSampleIvSize: perSampleIvSize,
constantIv: crypto?.constantIv,
sampleCount: sampleSizes.count
)
}
guard !sampleSizes.isEmpty, sencSamples.count == sampleSizes.count || sencSamples.isEmpty == false else {
diag("frag track=\(trackId) skip: samples=\(sampleSizes.count) senc=\(sencSamples.count) tfhd=0x\(String(tfhdFlagsSeen, radix: 16)) trun=0x\(String(trunFlagsSeen, radix: 16))")
// Advance cursor even if we cannot decrypt.
for sz in sampleSizes { mdatCursor += sz }
continue
}
// Exacte KID-match, anders primaryKey als er maar één key is.
let key: Data? = {
guard let crypto else { return keys.primaryKey }
return keys.key(forKid: crypto.kid)
}()
guard let key else {
diag("frag track=\(trackId) skip: geen key (kid=\(crypto.map { hex($0.kid) } ?? "-") keys=\(keys.uniqueKeyCount))")
for sz in sampleSizes { mdatCursor += sz }
continue
}
let scheme = (crypto?.scheme ?? "cenc").lowercased()
let cryptBlocks = crypto?.cryptByteBlock ?? 0
let skipBlocks = crypto?.skipByteBlock ?? 0
var didDecrypt = false
for i in 0..<sampleSizes.count {
let size = sampleSizes[i]
guard size > 0, mdatCursor + size <= data.count else { break }
let sampleRange = mdatCursor..<(mdatCursor + size)
var sample = data.subdata(in: sampleRange)
let senc = i < sencSamples.count ? sencSamples[i] : nil
if let senc {
if let decrypted = decryptSample(
sample,
key: key,
iv: senc.iv,
subsamples: senc.subsamples,
scheme: scheme,
cryptByteBlock: cryptBlocks,
skipByteBlock: skipBlocks
) {
sample = decrypted
data.replaceSubrange(sampleRange, with: sample)
didDecrypt = true
}
}
mdatCursor += size
}
let decryptedCount = didDecrypt ? min(sencSamples.count, sampleSizes.count) : 0
diag("frag track=\(trackId) crypto=\(crypto != nil) scheme=\(scheme) pat=\(cryptBlocks):\(skipBlocks) samples=\(sampleSizes.count) senc=\(sencSamples.count) iv=\(sencSamples.first?.iv.count ?? 0) subs=\(sencSamples.first?.subsamples?.count ?? -1) size0=\(sampleSizes.first ?? 0) dec=\(decryptedCount) tfhd=0x\(String(tfhdFlagsSeen, radix: 16)) trun=0x\(String(trunFlagsSeen, radix: 16)) mdat=\(mdat.payloadLength)")
// AVPlayer/VLC mogen geen tweede decrypt-poging doen op clear samples.
if didDecrypt {
for senc in findBoxesInRange(data, type: "senc", from: traf.payloadStart, to: traf.payloadEnd) {
writeFourCC(&data, senc.start + 4, "free")
}
}
}
}
private struct SencSample {
var iv: Data
var subsamples: [(clear: Int, protected: Int)]?
}
private static func parseSenc(
_ data: Data,
box: Box,
perSampleIvSize: Int,
constantIv: Data?
) -> [SencSample] {
let versionFlags = readU32(data, box.payloadStart)
let flags = versionFlags & 0x00FF_FFFF
var o = box.payloadStart + 4
let sampleCount = Int(readU32(data, o)); o += 4
var out: [SencSample] = []
out.reserveCapacity(sampleCount)
let useSubsamples = (flags & 0x2) != 0
for _ in 0..<sampleCount {
var iv = Data(count: 16)
if perSampleIvSize > 0 {
let n = min(perSampleIvSize, 16)
guard o + n <= box.payloadEnd else { break }
iv.replaceSubrange(0..<n, with: data.subdata(in: o..<(o + n)))
o += n
} else if let constantIv {
let n = min(constantIv.count, 16)
iv.replaceSubrange(0..<n, with: constantIv.prefix(n))
}
var subs: [(clear: Int, protected: Int)]?
if useSubsamples {
guard o + 2 <= box.payloadEnd else { break }
let count = Int(readU16(data, o)); o += 2
var list: [(clear: Int, protected: Int)] = []
for _ in 0..<count {
guard o + 6 <= box.payloadEnd else { break }
let clear = Int(readU16(data, o)); o += 2
let protected = Int(readU32(data, o)); o += 4
list.append((clear, protected))
}
subs = list
}
out.append(SencSample(iv: iv, subsamples: subs))
}
return out
}
private static func parseSaizSaio(
_ data: Data,
traf: Box,
moof: Box,
perSampleIvSize: Int,
constantIv: Data?,
sampleCount: Int
) -> [SencSample] {
let saizBoxes = findBoxesInRange(data, type: "saiz", from: traf.payloadStart, to: traf.payloadEnd)
let saioBoxes = findBoxesInRange(data, type: "saio", from: traf.payloadStart, to: traf.payloadEnd)
guard let saiz = saizBoxes.first, let saio = saioBoxes.first else { return [] }
let saizFlags = readU32(data, saiz.payloadStart) & 0x00FF_FFFF
var o = saiz.payloadStart + 4
if saizFlags & 0x1 != 0 {
guard o + 8 <= saiz.payloadEnd else { return [] }
o += 8
}
guard o + 5 <= saiz.payloadEnd else { return [] }
let defaultSize = Int(data[o]); o += 1
let count = Int(readU32(data, o)); o += 4
guard count >= 0, count < 100_000 else { return [] }
var sizes: [Int] = []
if defaultSize == 0 {
guard o + count <= saiz.payloadEnd else { return [] }
for _ in 0..<count {
sizes.append(Int(data[o])); o += 1
}
} else {
sizes = Array(repeating: defaultSize, count: count)
}
let saioVersionFlags = readU32(data, saio.payloadStart)
let saioVersion = Int(saioVersionFlags >> 24)
let saioFlags = saioVersionFlags & 0x00FF_FFFF
var so = saio.payloadStart + 4
if saioFlags & 0x1 != 0 { so += 8 }
let entryCount = Int(readU32(data, so)); so += 4
guard entryCount >= 1 else { return [] }
let auxOffset: Int
if saioVersion == 0 {
auxOffset = Int(readU32(data, so))
} else {
auxOffset = Int(readU64(data, so))
}
// Offsets are usually relative to moof start (ISO) or absolute file offset.
var auxPos = auxOffset
if auxPos < moof.start || auxPos > data.count {
auxPos = moof.start + auxOffset
}
guard auxPos >= 0, auxPos < data.count else { return [] }
var out: [SencSample] = []
let n = min(sizes.count, sampleCount > 0 ? sampleCount : sizes.count)
for i in 0..<n {
let entrySize = sizes[i]
guard auxPos + entrySize <= data.count else { break }
var p = auxPos
var iv = Data(count: 16)
if perSampleIvSize > 0 {
let ivn = min(perSampleIvSize, 16)
iv.replaceSubrange(0..<ivn, with: data.subdata(in: p..<(p + ivn)))
p += ivn
} else if let constantIv {
let ivn = min(constantIv.count, 16)
iv.replaceSubrange(0..<ivn, with: constantIv.prefix(ivn))
}
var subs: [(clear: Int, protected: Int)]?
if entrySize > perSampleIvSize {
let sc = Int(readU16(data, p)); p += 2
var list: [(clear: Int, protected: Int)] = []
for _ in 0..<sc {
let clear = Int(readU16(data, p)); p += 2
let protected = Int(readU32(data, p)); p += 4
list.append((clear, protected))
}
subs = list
}
out.append(SencSample(iv: iv, subsamples: subs))
auxPos += entrySize
}
return out
}
private static func decryptSample(
_ sample: Data,
key: Data,
iv: Data,
subsamples: [(clear: Int, protected: Int)]?,
scheme: String,
cryptByteBlock: Int,
skipByteBlock: Int
) -> Data? {
if scheme == "cbcs" || scheme == "cbc1" {
return decryptCbcsSample(
sample,
key: key,
iv: iv,
subsamples: subsamples,
cryptByteBlock: cryptByteBlock,
skipByteBlock: skipByteBlock
)
}
// cenc / cens: AES-CTR. Concatenate protected ranges → one CTR stream → merge.
guard let ranges = protectedRanges(sampleCount: sample.count, subsamples: subsamples), !ranges.isEmpty else {
return AesCtr.crypt(key: key, iv: iv, data: sample)
}
var encrypted = Data()
for r in ranges {
encrypted.append(sample.subdata(in: r))
}
guard let plain = AesCtr.crypt(key: key, iv: iv, data: encrypted) else { return nil }
var out = sample
var read = 0
for r in ranges {
let len = r.count
out.replaceSubrange(r, with: plain.subdata(in: read..<(read + len)))
read += len
}
return out
}
private static func decryptCbcsSample(
_ sample: Data,
key: Data,
iv: Data,
subsamples: [(clear: Int, protected: Int)]?,
cryptByteBlock: Int,
skipByteBlock: Int
) -> Data? {
// Per subsample: IV reset (Chromium/ISO). Pattern chaint binnen de protected range.
var out = sample
if let subsamples {
var cursor = 0
for sub in subsamples {
cursor += sub.clear
let end = cursor + sub.protected
guard end <= out.count else { break }
let slice = out.subdata(in: cursor..<end)
guard let dec = AesCtr.cryptCbcs(
key: key,
iv: iv,
data: slice,
cryptByteBlock: cryptByteBlock,
skipByteBlock: skipByteBlock
) else { return nil }
out.replaceSubrange(cursor..<end, with: dec)
cursor = end
}
return out
}
return AesCtr.cryptCbcs(
key: key,
iv: iv,
data: sample,
cryptByteBlock: cryptByteBlock,
skipByteBlock: skipByteBlock
)
}
private static func protectedRanges(
sampleCount: Int,
subsamples: [(clear: Int, protected: Int)]?
) -> [Range<Int>]? {
guard sampleCount > 0 else { return nil }
guard let subsamples else {
return [0..<sampleCount]
}
var ranges: [Range<Int>] = []
var cursor = 0
for sub in subsamples {
cursor += max(0, sub.clear)
let prot = max(0, sub.protected)
if prot > 0 {
let end = min(cursor + prot, sampleCount)
if cursor < end {
ranges.append(cursor..<end)
}
}
cursor += prot
if cursor >= sampleCount { break }
}
return ranges
}
private static func findBoxesInRange(_ data: Data, type: String, from: Int, to: Int) -> [Box] {
var out: [Box] = []
walk(data, from: from, to: to, recursive: true) { box in
if box.type == type { out.append(box) }
return true
}
return out
}
}