Correctif FluxMaster pour mediabunny 1.61.3 (MPL-2.0) : retrait de tout le code de déchiffrement. Fichier d'origine : dist/bundles/mediabunny.mjs, SHA-256 ef4af783346b35fb85a32fc17b7cbeba7549129ef5105ae644a9041d414a99a5 Fichier obtenu : SHA-256 8146e8ec7cf0a4a1b442640be14dee0f43e09681d852ec9a224f28bf70f29b93 Application, depuis la racine du paquet npm mediabunny@1.61.3 : patch -p1 < mediabunny-1.61.3-fluxmaster.patch Généré par tools/vendor-mediabunny.mjs ; détail des étapes dans extension/vendor/mediabunny/PATCH.md. --- a/dist/bundles/mediabunny.mjs +++ b/dist/bundles/mediabunny.mjs @@ -4,6 +4,14 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +/*! + * [FluxMaster] Fichier modifié pour l'extension FluxMaster (Flavien Hue, 2026), sous la même licence + * MPL-2.0. Tout le code de déchiffrement a été retiré : module AES-128 logiciel, appels WebCrypto, + * rappel de résolution des clés (CENC, cbcs), lecture des clés HLS, lecture des paramètres de + * chiffrement MP4 (identifiants de clé, IV, sous-échantillons) et outils qui ne servaient qu'aux clés. + * Un contenu chiffré (HLS, MP4, Matroska) lève l'erreur « FluxMaster : contenu chiffré non pris en charge ». + * Détail : PATCH.md ; fichier d'origine et empreintes : SOURCE.md (même dossier). */ var __create = Object.create; var __defProp = Object.defineProperty; @@ -465,17 +473,9 @@ return resolver; } }; -var HEX_STRING_REGEX = /^[0-9a-fA-F]+$/; +// [FluxMaster] Retirée ici : la conversion en octets des clés fournies en hexadécimal (sans usage). var bytesToHexString = (bytes2) => { return [...bytes2].map((x) => x.toString(16).padStart(2, "0")).join(""); -}; -var hexStringToBytes = (hexString) => { - assert(hexString.length % 2 === 0); - const bytes2 = new Uint8Array(hexString.length / 2); - for (let i = 0; i < hexString.length; i += 2) { - bytes2[i / 2] = parseInt(hexString.slice(i, i + 2), 16); - } - return bytes2; }; var reverseBitsU32 = (x) => { x = x >> 1 & 1431655765 | (x & 1431655765) << 1; @@ -6078,35 +6078,8 @@ } return string; }; -var parsePsshBoxContents = (contents) => { - const view2 = toDataView(contents); - let pos = 0; - const version = view2.getUint8(pos); - pos += 1; - pos += 3; - const systemId = bytesToHexString(contents.subarray(pos, pos + 16)); - pos += 16; - let keyIds = null; - if (version > 0) { - const kidCount = view2.getUint32(pos); - pos += 4; - if (kidCount > 0) { - keyIds = []; - for (let i = 0; i < kidCount; i++) { - keyIds.push(bytesToHexString(contents.subarray(pos, pos + 16))); - pos += 16; - } - } - } - const dataSize = view2.getUint32(pos); - pos += 4; - return { - systemId, - keyIds, - data: contents.slice(pos, pos + dataSize) - }; -}; -var psshBoxesAreEqual = (a, b) => a.systemId === b.systemId && uint8ArraysAreEqual(a.data, b.data); +// [FluxMaster] Retirée ici : la lecture du contenu des boîtes pssh (sans usage depuis P25). +// [FluxMaster] Retirée ici : la comparaison de deux boîtes pssh (sans usage depuis P10). // src/isobmff/isobmff-reader.ts var MIN_BOX_HEADER_SIZE = 8; @@ -6180,184 +6153,10 @@ }; // src/aes.ts -var AES_128_BLOCK_SIZE = 16; -var Te4 = new Uint32Array(256); -var Td0 = new Uint32Array(256); -var Td1 = new Uint32Array(256); -var Td2 = new Uint32Array(256); -var Td3 = new Uint32Array(256); -var Td4 = new Uint32Array(256); -var rcon = new Uint32Array(10); -var tablesGenerated = false; -var generateAesTables = () => { - const sbox = new Uint8Array(256); - const log = new Uint8Array(256); - const pow = new Uint8Array(256); - for (let i = 0, p = 1; i < 256; i++) { - pow[i] = p; - log[p] = i; - p = p ^ p << 1 ^ (p & 128 ? 283 : 0); - } - const mul = (a, b) => a && b ? pow[(log[a] + log[b]) % 255] : 0; - sbox[0] = 99; - for (let i = 1; i < 256; i++) { - const x = pow[255 - log[i]]; - let s = x ^ x << 1 ^ x << 2 ^ x << 3 ^ x << 4; - s = s >>> 8 ^ s & 255 ^ 99; - sbox[i] = s; - } - for (let i = 0; i < 256; i++) { - const s = sbox[i]; - const is = sbox.indexOf(i); - Te4[i] = s << 24 | s << 16 | s << 8 | s; - Td4[i] = is << 24 | is << 16 | is << 8 | is; - const b0 = mul(is, 14); - const b1 = mul(is, 9); - const b2 = mul(is, 13); - const b3 = mul(is, 11); - const w = b0 << 24 | b1 << 16 | b2 << 8 | b3; - Td0[i] = w; - Td1[i] = w >>> 8 | w << 24; - Td2[i] = w >>> 16 | w << 16; - Td3[i] = w >>> 24 | w << 8; - } - let r = 1; - for (let i = 0; i < 10; i++) { - rcon[i] = r << 24; - r = r << 1 ^ (r & 128 ? 283 : 0); - } - tablesGenerated = true; -}; -var Aes128CbcContext = class { - constructor() { - this.roundkey = new Uint32Array(44); - this.iv = new Uint32Array(AES_128_BLOCK_SIZE / Uint32Array.BYTES_PER_ELEMENT); - this.in = new Uint8Array(AES_128_BLOCK_SIZE); - this.out = new Uint8Array(AES_128_BLOCK_SIZE); - this.inView = new DataView(this.in.buffer); - this.outView = new DataView(this.out.buffer); - } - init({ key, iv }) { - assert(key.byteLength === 16); - assert(iv.byteLength === 16); - if (!tablesGenerated) { - generateAesTables(); - } - const keyView = new DataView(key.buffer, key.byteOffset, key.byteLength); - const ivView = new DataView(iv.buffer, iv.byteOffset, iv.byteLength); - this.roundkey[0] = keyView.getUint32(0, false); - this.roundkey[1] = keyView.getUint32(4, false); - this.roundkey[2] = keyView.getUint32(8, false); - this.roundkey[3] = keyView.getUint32(12, false); - this.iv[0] = ivView.getUint32(0, false); - this.iv[1] = ivView.getUint32(4, false); - this.iv[2] = ivView.getUint32(8, false); - this.iv[3] = ivView.getUint32(12, false); - for (let index = 4; index < 44; index += 4) { - const temp = this.roundkey[index - 1]; - this.roundkey[index] = this.roundkey[index - 4] ^ Te4[temp >>> 16 & 255] & 4278190080 ^ Te4[temp >>> 8 & 255] & 16711680 ^ Te4[temp >>> 0 & 255] & 65280 ^ Te4[temp >>> 24 & 255] & 255 ^ rcon[index / 4 - 1]; - this.roundkey[index + 1] = this.roundkey[index - 3] ^ this.roundkey[index]; - this.roundkey[index + 2] = this.roundkey[index - 2] ^ this.roundkey[index + 1]; - this.roundkey[index + 3] = this.roundkey[index - 1] ^ this.roundkey[index + 2]; - } - for (let i = 0, j = 40; i < j; i += 4, j -= 4) { - for (let k = 0; k < 4; k++) { - const temp = this.roundkey[i + k]; - this.roundkey[i + k] = this.roundkey[j + k]; - this.roundkey[j + k] = temp; - } - } - for (let index = 4; index < 40; index += 4) { - for (let k = 0; k < 4; k++) { - const rk = this.roundkey[index + k]; - this.roundkey[index + k] = Td0[Te4[rk >>> 24 & 255] & 255] ^ Td1[Te4[rk >>> 16 & 255] & 255] ^ Td2[Te4[rk >>> 8 & 255] & 255] ^ Td3[Te4[rk >>> 0 & 255] & 255]; - } - } - } - decrypt() { - let s0 = this.inView.getUint32(0, false) ^ this.roundkey[0]; - let s1 = this.inView.getUint32(4, false) ^ this.roundkey[1]; - let s2 = this.inView.getUint32(8, false) ^ this.roundkey[2]; - let s3 = this.inView.getUint32(12, false) ^ this.roundkey[3]; - const temp0 = this.inView.getUint32(0, false); - const temp1 = this.inView.getUint32(4, false); - const temp2 = this.inView.getUint32(8, false); - const temp3 = this.inView.getUint32(12, false); - let t0, t1, t2, t3; - for (let round = 1; round < 10; round++) { - const offset = round * 4; - t0 = Td0[s0 >>> 24] ^ Td1[s3 >>> 16 & 255] ^ Td2[s2 >>> 8 & 255] ^ Td3[s1 & 255] ^ this.roundkey[offset]; - t1 = Td0[s1 >>> 24] ^ Td1[s0 >>> 16 & 255] ^ Td2[s3 >>> 8 & 255] ^ Td3[s2 & 255] ^ this.roundkey[offset + 1]; - t2 = Td0[s2 >>> 24] ^ Td1[s1 >>> 16 & 255] ^ Td2[s0 >>> 8 & 255] ^ Td3[s3 & 255] ^ this.roundkey[offset + 2]; - t3 = Td0[s3 >>> 24] ^ Td1[s2 >>> 16 & 255] ^ Td2[s1 >>> 8 & 255] ^ Td3[s0 & 255] ^ this.roundkey[offset + 3]; - s0 = t0; - s1 = t1; - s2 = t2; - s3 = t3; - } - const f0 = Td4[s0 >>> 24 & 255] & 4278190080 ^ Td4[s3 >>> 16 & 255] & 16711680 ^ Td4[s2 >>> 8 & 255] & 65280 ^ Td4[s1 >>> 0 & 255] & 255 ^ this.roundkey[40]; - const f1 = Td4[s1 >>> 24 & 255] & 4278190080 ^ Td4[s0 >>> 16 & 255] & 16711680 ^ Td4[s3 >>> 8 & 255] & 65280 ^ Td4[s2 >>> 0 & 255] & 255 ^ this.roundkey[41]; - const f2 = Td4[s2 >>> 24 & 255] & 4278190080 ^ Td4[s1 >>> 16 & 255] & 16711680 ^ Td4[s0 >>> 8 & 255] & 65280 ^ Td4[s3 >>> 0 & 255] & 255 ^ this.roundkey[42]; - const f3 = Td4[s3 >>> 24 & 255] & 4278190080 ^ Td4[s2 >>> 16 & 255] & 16711680 ^ Td4[s1 >>> 8 & 255] & 65280 ^ Td4[s0 >>> 0 & 255] & 255 ^ this.roundkey[43]; - this.outView.setUint32(0, f0 ^ this.iv[0], false); - this.outView.setUint32(4, f1 ^ this.iv[1], false); - this.outView.setUint32(8, f2 ^ this.iv[2], false); - this.outView.setUint32(12, f3 ^ this.iv[3], false); - this.iv[0] = temp0; - this.iv[1] = temp1; - this.iv[2] = temp2; - this.iv[3] = temp3; - } -}; -var createAes128CbcDecryptStream = (reader, getInit, close) => { - let initted = false; - let pos = 0; - const CHUNK_SIZE = 2 ** 16; - const BLOCK_SIZE = 16; - const aesContext = new Aes128CbcContext(); - return new ReadableStream({ - pull: async (controller) => { - if (!initted) { - aesContext.init(await getInit()); - initted = true; - } - const requestedLength = CHUNK_SIZE + BLOCK_SIZE; - let nextSlice = reader.requestSliceRange(pos, 0, requestedLength); - if (isThenable(nextSlice)) nextSlice = await nextSlice; - if (!nextSlice || nextSlice.length === 0) { - throw new Error("Invalid ciphertext."); - } - const sliceLength = nextSlice.length; - if (sliceLength % 16 !== 0) { - throw new Error("Invalid ciphertext."); - } - const bytesToRead = sliceLength === requestedLength ? sliceLength - BLOCK_SIZE : sliceLength; - const input = readBytes(nextSlice, bytesToRead); - const output = new Uint8Array(bytesToRead); - for (let i = 0; i < bytesToRead; i += 16) { - aesContext.in.set(input.subarray(i, i + 16)); - aesContext.decrypt(); - output.set(aesContext.out, i); - } - if (bytesToRead < sliceLength) { - controller.enqueue(output); - pos += bytesToRead; - } else { - const paddingLength = output[bytesToRead - 1]; - if (paddingLength === 0 || paddingLength > 16) { - throw new Error("Invalid PKCS#7 padding. Incorrect key or corrupted data."); - } - const trimmedOutput = output.subarray(0, bytesToRead - paddingLength); - controller.enqueue(trimmedOutput); - controller.close(); - close(); - } - }, - cancel: () => { - close(); - } - }); -}; +// [FluxMaster] Module retiré : implémentation logicielle du déchiffrement AES-128 (mode CBC), qui servait +// aux segments HLS « METHOD=AES-128 » et aux échantillons MP4 « cbcs ». Toute lecture d'un contenu +// chiffré lève désormais cette erreur : +var FLUXMASTER_ENCRYPTED_ERROR = "FluxMaster : contenu chiffré non pris en charge"; // src/isobmff/isobmff-demuxer.ts var IsobmffDemuxer = class _IsobmffDemuxer extends Demuxer { @@ -6375,14 +6174,12 @@ this.currentMetadataKeys = null; this.isFragmented = false; this.fragmentTrackDefaults = []; - this.psshBoxes = []; this.currentFragment = null; /** * Caches the last fragment that was read. Based on the assumption that there will be multiple reads to the * same fragment in quick succession. */ this.lastReadFragment = null; - this.decryptionKeyCache = /* @__PURE__ */ new Map(); this.reader = input._reader; } async getTrackBackings() { @@ -6492,7 +6289,6 @@ this.metadataTags = initDemuxer.metadataTags; this.isFragmented = true; this.fragmentTrackDefaults = initDemuxer.fragmentTrackDefaults; - this.psshBoxes = initDemuxer.psshBoxes; for (const foreignTrack of initDemuxer.tracks) { const track = { id: foreignTrack.id, @@ -6514,7 +6310,6 @@ editListPreviousSegmentDurations: foreignTrack.editListPreviousSegmentDurations, editListOffset: foreignTrack.editListOffset, encryptionInfo: foreignTrack.encryptionInfo, - encryptionAuxInfo: null, frmaCodecString: null, maxBitrate: foreignTrack.maxBitrate, avgBitrate: foreignTrack.avgBitrate, @@ -6683,17 +6478,7 @@ endTimestamp: trackData.endTimestamp }); } - if (trackData.encryptionAuxInfo && track.encryptionInfo) { - const entries = await resolveEncryptionAuxInfo( - this.reader, - track.encryptionInfo, - trackData.encryptionAuxInfo - ); - for (let i = 0; i < Math.min(trackData.samples.length, entries.length); i++) { - const entry = entries[i]; - trackData.samples[i].encryption = entry; - } - } + // [FluxMaster] Retiré ici : le rattachement de l'IV et des sous-échantillons à chaque échantillon (sans usage). } return fragment; } @@ -6784,7 +6569,6 @@ editListPreviousSegmentDurations: 0, editListOffset: 0, encryptionInfo: null, - encryptionAuxInfo: null, frmaCodecString: null, maxBitrate: null, avgBitrate: null @@ -7192,57 +6976,16 @@ break; case "schm": { - const track = this.currentTrack; - if (!track) { - break; - } - slice.skip(4); - const schemeType = readAscii(slice, 4); - if (schemeType === "cenc" || schemeType === "cens" || schemeType === "cbcs") { - track.encryptionInfo = { - scheme: schemeType, - defaultKid: null, - defaultIsProtected: null, - defaultPerSampleIvSize: null, - defaultConstantIv: null, - defaultCryptByteBlock: null, - defaultSkipByteBlock: null - }; - } else { - Logging._warn(`Unsupported encryption scheme '${schemeType}'.`); - } + // [FluxMaster] Schéma de protection d'une piste (cenc, cens, cbc1, cbcs ou inconnu), lu dans sa boîte + // sinf : refus explicite dès la lecture des pistes, quel que soit le schéma. + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); } - ; - break; case "tenc": { - const track = this.currentTrack; - if (!track || !track.encryptionInfo) { - break; - } - const version = readU8(slice); - slice.skip(3); - slice.skip(1); - const patternByte = readU8(slice); - if (version > 0) { - track.encryptionInfo.defaultCryptByteBlock = patternByte >> 4; - track.encryptionInfo.defaultSkipByteBlock = patternByte & 15; - } else { - track.encryptionInfo.defaultCryptByteBlock = 0; - track.encryptionInfo.defaultSkipByteBlock = 0; - } - track.encryptionInfo.defaultIsProtected = readU8(slice) !== 0; - track.encryptionInfo.defaultPerSampleIvSize = readU8(slice); - track.encryptionInfo.defaultKid = bytesToHexString(readBytes(slice, 16)); - if (track.encryptionInfo.defaultIsProtected && track.encryptionInfo.defaultPerSampleIvSize === 0) { - const constantIvSize = readU8(slice); - const constantIv = new Uint8Array(16); - constantIv.set(readBytes(slice, constantIvSize), 0); - track.encryptionInfo.defaultConstantIv = constantIv; - } + // [FluxMaster] Paramètres de chiffrement par défaut d'une piste (boîte tenc) : refus explicite, rien + // n'est lu (ni identifiant de clé, ni IV). + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); } - ; - break; case "avcC": { const track = this.currentTrack; @@ -7918,8 +7661,7 @@ moofOffset: startPos, moofSize: boxInfo.totalSize, implicitBaseDataOffset: startPos, - trackData: /* @__PURE__ */ new Map(), - psshBoxes: [] + trackData: /* @__PURE__ */ new Map() }; this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize)); this.lastReadFragment = this.currentFragment; @@ -7962,9 +7704,6 @@ offsetFragmentTrackDataByTimestamp(trackData, currentFragmentState.startTimestamp); trackData.startTimestampIsFinal = true; } - if (currentFragmentState.encryptionAuxInfo && !trackData.samples[0].encryption) { - trackData.encryptionAuxInfo = currentFragmentState.encryptionAuxInfo; - } } this.currentTrack.currentFragmentState = null; this.currentTrack = null; @@ -7974,18 +7713,9 @@ break; case "pssh": { - if (this.input._formatOptions.isobmff?._suppressPsshParsing) { - break; - } - const psshBox = parsePsshBoxContents(readBytes(slice, boxInfo.contentSize)); - if (this.currentFragment) { - this.currentFragment.psshBoxes.push(psshBox); - } else if (!this.currentTrack) { - this.psshBoxes.push(psshBox); - } + // [FluxMaster] Données d'un système de protection (boîte pssh) : refus explicite, rien n'est lu. + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); } - ; - break; case "tfhd": { assert(this.currentFragment); @@ -8011,8 +7741,7 @@ defaultSampleDuration: defaults?.defaultSampleDuration ?? null, defaultSampleSize: defaults?.defaultSampleSize ?? null, defaultSampleFlags: defaults?.defaultSampleFlags ?? null, - startTimestamp: null, - encryptionAuxInfo: null + startTimestamp: null }; if (baseDataOffsetPresent) { track.currentFragmentState.baseDataOffset = readU64Be(slice); @@ -8093,8 +7822,7 @@ firstKeyFrameTimestamp: null, samples: [], presentationTimestamps: [], - startTimestampIsFinal: false, - encryptionAuxInfo: null + startTimestampIsFinal: false }; this.currentFragment.trackData.set(track.id, trackData); } @@ -8133,8 +7861,7 @@ duration: sampleDuration, byteOffset: trackData.currentOffset, byteSize: sampleSize, - isKeyFrame, - encryption: null + isKeyFrame }); trackData.currentOffset += sampleSize; trackData.currentTimestamp += sampleDuration; @@ -8144,104 +7871,20 @@ ; break; case "saiz": - { - const track = this.currentTrack; - if (!track || !track.encryptionInfo) { - break; - } - slice.skip(1); - const flags = readU24Be(slice); - if (flags & 1) { - const auxInfoType = readAscii(slice, 4); - const auxInfoTypeParam = readU32Be(slice); - if (auxInfoType !== track.encryptionInfo.scheme || auxInfoTypeParam !== 0) { - break; - } - } - const defaultSampleInfoSize = readU8(slice); - const sampleCount = readU32Be(slice); - let sampleSizes = null; - if (defaultSampleInfoSize === 0 && sampleCount > 0) { - sampleSizes = readBytes(slice, sampleCount); - } - const aux = getOrCreateEncryptionAuxInfo(track); - aux.defaultSampleInfoSize = defaultSampleInfoSize; - aux.sampleSizes = sampleSizes; - aux.sampleCount = sampleCount; - } - ; - break; case "saio": { - const track = this.currentTrack; - if (!track || !track.encryptionInfo) { - break; - } - const version = readU8(slice); - const flags = readU24Be(slice); - if (flags & 1) { - const auxInfoType = readAscii(slice, 4); - const auxInfoTypeParam = readU32Be(slice); - if (auxInfoType !== track.encryptionInfo.scheme || auxInfoTypeParam !== 0) { - break; - } - } - const entryCount = readU32Be(slice); - if (entryCount === 0) { - break; - } - if (entryCount > 1) { - Logging._warn("Multiple saio entries are not supported; using the first offset only."); - } - let offset = version === 0 ? readU32Be(slice) : Number(readU64Be(slice)); - if (this.currentFragment) { - offset += this.currentFragment.moofOffset; - } - const aux = getOrCreateEncryptionAuxInfo(track); - aux.offset = offset; + // [FluxMaster] Informations auxiliaires des échantillons (boîtes saiz et saio) : jamais lues. Dans une + // piste chiffrée, elles situent l'IV et les sous-échantillons de chaque échantillon ; cette piste est + // déjà refusée (boîtes schm, tenc, senc). Ailleurs, ces boîtes génériques sont ignorées. } ; break; case "senc": { - const track = this.currentTrack; - if (!track || !track.encryptionInfo) { - break; - } - assert(this.currentFragment); - const trackData = this.currentFragment.trackData.get(track.id); - if (!trackData) { - break; - } - slice.skip(1); - const flags = readU24Be(slice); - const useSubsamples = Boolean(flags & 2); - const sampleCount = readU32Be(slice); - const ivSize = track.encryptionInfo.defaultPerSampleIvSize; - assert(ivSize !== null); - for (let i = 0; i < Math.min(sampleCount, trackData.samples.length); i++) { - const iv = new Uint8Array(16); - if (ivSize > 0) { - iv.set(readBytes(slice, ivSize), 0); - } else { - iv.set(track.encryptionInfo.defaultConstantIv, 0); - } - let subsamples = null; - if (useSubsamples) { - const subsampleCount = readU16Be(slice); - subsamples = []; - for (let j = 0; j < subsampleCount; j++) { - const clearLen = readU16Be(slice); - const protectedLen = readU32Be(slice); - subsamples.push({ clearLen, protectedLen }); - } - } - const sample = trackData.samples[i]; - sample.encryption = { iv, subsamples }; - } + // [FluxMaster] Informations de chiffrement des échantillons (boîte senc) : refus explicite, dans un + // MP4 non fragmenté comme dans un fragment. Rien n'est lu (ni IV, ni sous-échantillons). + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); } - ; - break; // Metadata section // https://exiftool.org/TagNames/QuickTime.html // https://mp4workshop.com/about @@ -8851,21 +8494,8 @@ } data = readBytes(slice, sampleInfo.sampleSize); if (this.internalTrack.encryptionInfo) { - let sampleEncryption = null; - if (this.internalTrack.encryptionAuxInfo) { - const entries = await resolveEncryptionAuxInfo( - this.internalTrack.demuxer.reader, - this.internalTrack.encryptionInfo, - this.internalTrack.encryptionAuxInfo - ); - if (sampleIndex < entries.length) { - sampleEncryption = entries[sampleIndex]; - } - } - sampleEncryption ??= getDefaultSampleEncryption(this.internalTrack.encryptionInfo); - if (sampleEncryption) { - data = await decryptSample(this.internalTrack, sampleEncryption, data, null); - } + // [FluxMaster] Échantillon chiffré (CENC, cbcs) : refus, aucun déchiffrement n'existe dans cette copie. + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); } } const timestamp = (sampleInfo.presentationTimestamp - this.internalTrack.editListOffset) / this.internalTrack.timescale; @@ -8902,10 +8532,8 @@ } data = readBytes(slice, fragmentSample.byteSize); if (this.internalTrack.encryptionInfo) { - const sampleEncryption = fragmentSample.encryption ?? getDefaultSampleEncryption(this.internalTrack.encryptionInfo); - if (sampleEncryption) { - data = await decryptSample(this.internalTrack, sampleEncryption, data, fragment); - } + // [FluxMaster] Échantillon chiffré (CENC, cbcs) : refus, aucun déchiffrement n'existe dans cette copie. + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); } } const timestamp = (fragmentSample.presentationTimestamp - this.internalTrack.editListOffset) / this.internalTrack.timescale; @@ -9305,254 +8933,10 @@ var sampleTableIsEmpty = (sampleTable) => { return sampleTable.sampleSizes.length === 0; }; -var getOrCreateEncryptionAuxInfo = (track) => { - if (track.currentFragmentState) { - return track.currentFragmentState.encryptionAuxInfo ??= { - defaultSampleInfoSize: 0, - sampleSizes: null, - sampleCount: 0, - offset: null, - resolved: null - }; - } else { - return track.encryptionAuxInfo ??= { - defaultSampleInfoSize: 0, - sampleSizes: null, - sampleCount: 0, - offset: null, - resolved: null - }; - } -}; -var resolveEncryptionAuxInfo = async (reader, encryptionInfo, aux) => { - if (aux.resolved) { - return aux.resolved; - } - if (aux.offset === null || aux.sampleCount === 0) { - throw new Error("Incomplete saiz/saio info; cannot resolve encryption data."); - } - let totalSize = 0; - if (aux.defaultSampleInfoSize > 0) { - totalSize = aux.defaultSampleInfoSize * aux.sampleCount; - } else { - assert(aux.sampleSizes); - for (let i = 0; i < aux.sampleCount; i++) { - totalSize += aux.sampleSizes[i]; - } - } - let slice = reader.requestSlice(aux.offset, totalSize); - if (isThenable(slice)) slice = await slice; - if (!slice) { - throw new Error("Failed to read auxiliary encryption info."); - } - const ivSize = encryptionInfo.defaultPerSampleIvSize; - assert(ivSize !== null); - const entries = []; - for (let i = 0; i < aux.sampleCount; i++) { - const entrySize = aux.defaultSampleInfoSize > 0 ? aux.defaultSampleInfoSize : aux.sampleSizes[i]; - const iv = new Uint8Array(16); - if (ivSize > 0) { - iv.set(readBytes(slice, ivSize), 0); - } else { - iv.set(encryptionInfo.defaultConstantIv, 0); - } - let subsamples = null; - if (entrySize > ivSize) { - const subsampleCount = readU16Be(slice); - subsamples = []; - for (let j = 0; j < subsampleCount; j++) { - const clearLen = readU16Be(slice); - const protectedLen = readU32Be(slice); - subsamples.push({ clearLen, protectedLen }); - } - } - entries.push({ iv, subsamples }); - } - aux.resolved = entries; - return entries; -}; -var getDefaultSampleEncryption = (encryptionInfo) => { - if (!encryptionInfo.defaultConstantIv) { - return null; - } - return { - iv: encryptionInfo.defaultConstantIv, - subsamples: null - }; -}; -var decryptSample = async (track, sampleEncryption, data, fragment) => { - assert(track.encryptionInfo); - const encryptionInfo = track.encryptionInfo; - assert(encryptionInfo.defaultKid !== null); - const keyId = encryptionInfo.defaultKid; - let keyBytes; - const cacheEntry = track.demuxer.decryptionKeyCache.get(keyId); - if (cacheEntry) { - keyBytes = await cacheEntry; - } else { - if (!track.demuxer.input._formatOptions.isobmff?.resolveKeyId) { - throw new Error( - "Encrypted media samples encountered. To decrypt them, please provide a callback for InputOptions.formatOptions.isobmff.resolveKeyId." - ); - } - const promise = (async () => { - let psshBoxes = track.demuxer.psshBoxes; - if (fragment) { - psshBoxes = [ - ...psshBoxes, - ...fragment.psshBoxes - ].filter((x) => x.keyIds === null || x.keyIds.includes(keyId)); - for (let i = 0; i < psshBoxes.length - 1; i++) { - for (let j = i + 1; j < psshBoxes.length; j++) { - if (psshBoxesAreEqual(psshBoxes[i], psshBoxes[j])) { - psshBoxes.splice(j, 1); - j--; - } - } - } - } - const keyResult = await track.demuxer.input._formatOptions.isobmff.resolveKeyId({ keyId, psshBoxes }); - if (!(typeof keyResult === "string" && keyResult.length === 32 && HEX_STRING_REGEX.test(keyResult) || keyResult instanceof Uint8Array && keyResult.byteLength === 16)) { - throw new TypeError( - "resolveKeyId must return a 32-character hex string or a 16-byte Uint8Array containing the decryption key." - ); - } - return keyResult instanceof Uint8Array ? keyResult : hexStringToBytes(keyResult); - })(); - track.demuxer.decryptionKeyCache.set(keyId, promise); - keyBytes = await promise; - } - if (encryptionInfo.scheme === "cenc" || encryptionInfo.scheme === "cens") { - return decryptCtr(keyBytes, encryptionInfo, sampleEncryption, data); - } else { - return decryptCbcs(keyBytes, encryptionInfo, sampleEncryption, data); - } -}; -var decryptCtr = async (key, encryptionInfo, sampleEncryption, data) => { - const counter = new Uint8Array(16); - counter.set(sampleEncryption.iv, 0); - const cryptoKey = await crypto.subtle.importKey( - "raw", - key, - { name: "AES-CTR" }, - false, - ["decrypt"] - ); - const cryptApply = async (input) => { - const plaintext = await crypto.subtle.decrypt( - { name: "AES-CTR", counter, length: 64 }, - cryptoKey, - input - ); - return new Uint8Array(plaintext); - }; - if (!sampleEncryption.subsamples) { - return cryptApply(data); - } - assert(encryptionInfo.defaultCryptByteBlock !== null && encryptionInfo.defaultSkipByteBlock !== null); - const cryptRanges = collectCryptRanges( - sampleEncryption.subsamples, - encryptionInfo.defaultCryptByteBlock, - encryptionInfo.defaultSkipByteBlock - ); - let totalCryptLen = 0; - for (const range of cryptRanges) { - for (const seg of range.perSubsample) { - totalCryptLen += seg.length; - } - } - const cryptBuffer = new Uint8Array(totalCryptLen); - let writePos = 0; - for (const range of cryptRanges) { - for (const seg of range.perSubsample) { - cryptBuffer.set(data.subarray(seg.offset, seg.offset + seg.length), writePos); - writePos += seg.length; - } - } - const plain = await cryptApply(cryptBuffer); - const output = new Uint8Array(data); - let readPos = 0; - for (const range of cryptRanges) { - for (const seg of range.perSubsample) { - output.set(plain.subarray(readPos, readPos + seg.length), seg.offset); - readPos += seg.length; - } - } - return output; -}; -var decryptCbcs = (key, encryptionInfo, sampleEncryption, data) => { - const ctx = new Aes128CbcContext(); - ctx.init({ key, iv: sampleEncryption.iv }); - const cryptByteBlock = encryptionInfo.defaultCryptByteBlock; - const skipByteBlock = encryptionInfo.defaultSkipByteBlock; - assert(cryptByteBlock !== null && skipByteBlock !== null); - if (!sampleEncryption.subsamples) { - const output2 = new Uint8Array(data); - const numBlocks = Math.floor(data.length / 16); - for (let b = 0; b < numBlocks; b++) { - const off = b * 16; - ctx.in.set(data.subarray(off, off + 16)); - ctx.decrypt(); - output2.set(ctx.out, off); - } - return output2; - } - if (cryptByteBlock === 0 && skipByteBlock === 0) { - throw new Error("cbcs with subsamples requires pattern encryption."); - } - const output = new Uint8Array(data); - const cryptRanges = collectCryptRanges(sampleEncryption.subsamples, cryptByteBlock, skipByteBlock); - const ivView = new DataView(sampleEncryption.iv.buffer, sampleEncryption.iv.byteOffset, 16); - for (const range of cryptRanges) { - ctx.iv[0] = ivView.getUint32(0, false); - ctx.iv[1] = ivView.getUint32(4, false); - ctx.iv[2] = ivView.getUint32(8, false); - ctx.iv[3] = ivView.getUint32(12, false); - for (const seg of range.perSubsample) { - const numBlocks = seg.length / 16; - for (let b = 0; b < numBlocks; b++) { - const offset = seg.offset + b * 16; - ctx.in.set(data.subarray(offset, offset + 16)); - ctx.decrypt(); - output.set(ctx.out, offset); - } - } - } - return output; -}; -var collectCryptRanges = (subsamples, cryptByteBlock, skipByteBlock) => { - const ranges = []; - const hasPattern = cryptByteBlock !== 0 || skipByteBlock !== 0; - let cursor = 0; - for (const subsample of subsamples) { - cursor += subsample.clearLen; - const perSubsample = []; - if (!hasPattern) { - if (subsample.protectedLen > 0) { - perSubsample.push({ offset: cursor, length: subsample.protectedLen }); - } - cursor += subsample.protectedLen; - } else { - let remaining = subsample.protectedLen; - let pos = cursor; - while (remaining > 0) { - if (remaining < 16 * cryptByteBlock) { - break; - } - const cryptBytes = 16 * cryptByteBlock; - perSubsample.push({ offset: pos, length: cryptBytes }); - pos += cryptBytes; - remaining -= cryptBytes; - const skipBytes = Math.min(16 * skipByteBlock, remaining); - pos += skipBytes; - remaining -= skipBytes; - } - cursor += subsample.protectedLen; - } - ranges.push({ perSubsample }); - } - return ranges; -}; +// [FluxMaster] Retirées ici : la lecture de l'IV et des sous-échantillons de chaque échantillon chiffré (sans +// usage depuis P24). +// [FluxMaster] Retirés ici : la résolution des clés par un rappel fourni par l'appelant, le déchiffrement +// CENC (AES-CTR, par WebCrypto) et le déchiffrement cbcs (par le module AES logiciel, lui aussi retiré). // src/matroska/ebml.ts var EBMLFloat32 = class { @@ -11477,13 +10861,9 @@ break; case 20533 /* ContentEncryption */: { - if (!this.currentDecodingInstruction) break; - this.currentDecodingInstruction.data = { - type: "decrypt" - }; + // [FluxMaster] Piste Matroska ou WebM chiffrée : refus explicite, au lieu d'un marquage puis d'un retrait silencieux. + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); } - ; - break; } slice.filePos = dataStartPos + size; return true; @@ -16143,7 +15523,14 @@ } } } - referencePesPackets.splice(index + 1, 0, pesPacketHeader); + // [FluxMaster] Mémoire : seuls la position et l'horodatage servent à la recherche ; le paquet PES + // entier (avec ses octets) n'est plus gardé comme point de référence. + referencePesPackets.splice(index + 1, 0, { + sectionStartPos: pesPacketHeader.sectionStartPos, + sectionEndPos: pesPacketHeader.sectionEndPos, + pts: pesPacketHeader.pts, + randomAccessIndicator: pesPacketHeader.randomAccessIndicator + }); return true; }; var PacketReadingContext = class { @@ -16718,6 +16105,8 @@ var TAG_EXTINF = "#EXTINF:"; var TAG_MAP = "#EXT-X-MAP:"; var TAG_KEY = "#EXT-X-KEY:"; +// [FluxMaster] Ajouté : clé de session de la playlist principale, refusée si sa méthode n'est pas NONE. +var TAG_SESSION_KEY = "#EXT-X-SESSION-KEY:"; var TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE:"; var TAG_BYTERANGE = "#EXT-X-BYTERANGE:"; var TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME:"; @@ -19028,8 +18417,7 @@ }; // src/hls/hls-segmented-input.ts -var IV_STRING_REGEX = /^0[xX][0-9a-fA-F]+$/; -var BASE64_DATA_URI_REGEX = /^data:.*;base64,/i; +// [FluxMaster] Retirés ici : les formats de l'IV et des données d'une ligne #EXT-X-KEY (sans usage depuis P09). var HlsSegmentedInput = class extends SegmentedInput { // Reasonable default in case the playlist doesn't specify it constructor(demuxer, path, trackDeclarations, lines) { @@ -19152,14 +18540,8 @@ if (nextSegmentDuration === null) { throw new Error("Invalid M3U8 file; a segment must be preceded by an #EXTINF tag."); } - let key = currentKey; - if (key && key.method === "AES-128" && !key.iv) { - const iv = new Uint8Array(AES_128_BLOCK_SIZE); - const view2 = toDataView(iv); - view2.setUint32(8, Math.floor(nextSequenceNumber / 2 ** 32)); - view2.setUint32(12, nextSequenceNumber); - key = { ...key, iv }; - } + // [FluxMaster] Calcul de l'IV AES-128 retiré : #EXT-X-KEY est refusée à la lecture, la clé reste nulle. + const key = currentKey; const fullPath = joinPaths(this.rootPath, variables.substitute(line)); const location = { path: fullPath, @@ -19235,9 +18617,6 @@ offset: parsedByteRange?.offset ?? 0, length: parsedByteRange?.length ?? null }; - if (currentKey?.method === "AES-128" && !currentKey.iv) { - throw new Error("IV attribute must be set on #EXT-X-KEY tag preceding the #EXT-X-MAP tag."); - } const segment = { timestamp: accumulatedTime, unixEpochTimestamp: accumulatedUnixTime, @@ -19259,70 +18638,14 @@ nextByteRange = null; } } else if (line.startsWith(TAG_KEY)) { + // [FluxMaster] Seule la méthode NONE est acceptée. AES-128, SAMPLE-AES, SAMPLE-AES-CTR ou toute autre + // méthode lève l'erreur de refus : l'URI de la clé n'est jamais lue. const attributes = new AttributeList(line.slice(TAG_KEY.length), variables); const method = attributes.get("method"); if (method === "NONE") { currentKey = null; - } else if (method === "AES-128") { - const uri = attributes.get("uri"); - if (!uri) { - throw new Error("Invalid #EXT-X-KEY: AES-128 requires a URI attribute."); - } - let iv = null; - const ivString = attributes.get("iv"); - if (ivString) { - if (!IV_STRING_REGEX.test(ivString)) { - throw new Error(`Unsupported IV format '${ivString}'.`); - } - let hex = ivString.slice(2); - hex = hex.padStart(2 * AES_128_BLOCK_SIZE, "0"); - hex = hex.slice(-2 * AES_128_BLOCK_SIZE); - iv = new Uint8Array(AES_128_BLOCK_SIZE); - for (let i2 = 0; i2 < AES_128_BLOCK_SIZE; i2++) { - const startIndex = 2 * i2; - iv[i2] = parseInt(hex.slice(startIndex, startIndex + 2), 16); - } - } - const keyFormat = attributes.get("keyformat") ?? "identity"; - if (keyFormat !== "identity") { - throw new Error( - "For AES-128 encryption, only the 'identity' KEYFORMAT is currently supported. If you think other formats should be supported, please raise an issue." - ); - } - currentKey = { - method: "AES-128", - keyUri: joinPaths(this.rootPath, uri), - iv, - keyFormat - }; - } else if (method === "SAMPLE-AES" || method === "SAMPLE-AES-CTR") { - const uri = attributes.get("uri"); - if (!uri) { - throw new Error(`Invalid #EXT-X-KEY: ${method} requires a URI attribute.`); - } - const keyFormat = attributes.get("keyformat") ?? "identity"; - if (keyFormat === "identity") { - throw new Error( - "For SAMPLE-AES and SAMPLE-AES-CTR encryption, the 'identity' KEYFORMAT is not supported. If you think this format should be supported, please raise an issue." - ); - } - let psshBox = null; - if (BASE64_DATA_URI_REGEX.test(uri)) { - const commaIndex = uri.indexOf(","); - const bytes2 = base64ToBytes(uri.slice(commaIndex + 1)); - if (bytes2.length >= 8 && bytes2[4] === 112 && bytes2[5] === 115 && bytes2[6] === 115 && bytes2[7] === 104) { - const size = toDataView(bytes2).getUint32(0); - psshBox = parsePsshBoxContents(bytes2.subarray(8, Math.min(size, bytes2.length))); - } - } - currentKey = { - method, - psshBox - }; - } else { - throw new Error( - `Unsupported encryption method '${method}'. If you think this method should be supported, please raise an issue.` - ); + } else { + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); } } else if (line.startsWith(TAG_MEDIA_SEQUENCE)) { const value = line.slice(TAG_MEDIA_SEQUENCE.length); @@ -19459,24 +18782,8 @@ if (hlsSegment.initSegment || hlsSegment.firstSegment) { initInput = this.getInputForSegment(hlsSegment.initSegment ?? hlsSegment.firstSegment); } - const formatOptions = { - ...this.input._formatOptions, - isobmff: { - ...this.input._formatOptions.isobmff, - // Intercept calls to resolveKeyId to inject our psshBox knowledge into it - resolveKeyId: this.input._formatOptions.isobmff?.resolveKeyId && ((options) => { - if (!hlsSegment.encryption || !(hlsSegment.encryption.method === "SAMPLE-AES" || hlsSegment.encryption.method === "SAMPLE-AES-CTR") || !hlsSegment.encryption.psshBox) { - return this.input._formatOptions.isobmff.resolveKeyId(options); - } - let psshBoxes = options.psshBoxes; - const { psshBox } = hlsSegment.encryption; - if ((psshBox.keyIds === null || psshBox.keyIds.includes(options.keyId)) && !psshBoxes.some((x) => psshBoxesAreEqual(x, psshBox))) { - psshBoxes = [...psshBoxes, psshBox]; - } - return this.input._formatOptions.isobmff.resolveKeyId({ ...options, psshBoxes }); - }) - } - }; + // [FluxMaster] Plus d'interception des clés : les options de format sont transmises telles quelles. + const formatOptions = { ...this.input._formatOptions }; const input = new Input({ source: new CustomPathedSource( hlsSegment.location.path, @@ -19488,57 +18795,21 @@ }; let ref; const needsSlice = hlsSegment.location.offset > 0 || hlsSegment.location.length !== null; - if (!hlsSegment.encryption || hlsSegment.encryption.method === "SAMPLE-AES" || hlsSegment.encryption.method === "SAMPLE-AES-CTR") { - ref = await this.input._getSourceCached(proxiedRequest); - if (needsSlice) { - const slice = ref.source.slice( - hlsSegment.location.offset, - hlsSegment.location.length ?? void 0 - ); - const sliceRef = slice.ref(); - ref.free(); - ref = sliceRef; - } - } else if (hlsSegment.encryption.method === "AES-128") { - const encryption = hlsSegment.encryption; - assert(encryption.iv); - let ciphertextRef = await this.input._getSourceCached(proxiedRequest); - if (needsSlice) { - const slice = ciphertextRef.source.slice( - hlsSegment.location.offset, - hlsSegment.location.length ?? void 0 - ); - const sliceRef = slice.ref(); - ciphertextRef.free(); - ciphertextRef = sliceRef; - } - const ciphertextReader = new Reader12(ciphertextRef.source); - const stream = createAes128CbcDecryptStream(ciphertextReader, async () => { - var _stack = []; - try { - const keyRef = __using(_stack, await this.input._getSourceCached( - { path: encryption.keyUri, isRoot: false }, - ENCRYPTION_KEY_CACHE_GROUP - )); - const keyReader = new Reader12(keyRef.source); - const keySlice = await keyReader.requestSlice(0, AES_128_BLOCK_SIZE); - if (!keySlice) { - throw new Error("Invalid AES-128 key; expected at least 16 bytes of data."); - } - const key = readBytes(keySlice, AES_128_BLOCK_SIZE); - return { key, iv: encryption.iv }; - } catch (_) { - var _error = _, _hasError = true; - } finally { - __callDispose(_stack, _error, _hasError); - } - }, () => { - ciphertextRef.free(); - }); - ref = new ReadableStreamSource(stream).ref(); - } else { - assert(false); + if (hlsSegment.encryption) { + // [FluxMaster] Segment chiffré : refus. Ce cas ne peut plus arriver (#EXT-X-KEY est refusée dès la + // lecture de la playlist) ; la garde reste par prudence. + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); } + ref = await this.input._getSourceCached(proxiedRequest); + if (needsSlice) { + const slice = ref.source.slice( + hlsSegment.location.offset, + hlsSegment.location.length ?? void 0 + ); + const sliceRef = slice.ref(); + ref.free(); + ref = sliceRef; + } return ref; } ), @@ -19547,13 +18818,6 @@ initInput: initInput ?? void 0, formatOptions }); - input._onFormatDetermined = (format) => { - if ((hlsSegment.encryption?.method === "SAMPLE-AES" || hlsSegment.encryption?.method === "SAMPLE-AES-CTR") && !format._isIsobmff) { - throw new Error( - "The SAMPLE-AES and SAMPLE-AES-CTR encryption methods are currently only supported for ISOBMFF files." - ); - } - }; this.inputCache.push({ segment: hlsSegment, input, @@ -19662,6 +18926,12 @@ fullPath = joinPaths(rootPath, uri); } mediaTags.push({ fullPath, attributes, lineNumber: i }); + } else if (line.startsWith(TAG_SESSION_KEY)) { + // [FluxMaster] Clé de session annoncée par la playlist principale : refus si la méthode n'est pas NONE. + const attributes = new AttributeList(line.slice(TAG_SESSION_KEY.length), variables); + if (attributes.get("method") !== "NONE") { + throw new Error(FLUXMASTER_ENCRYPTED_ERROR); + } } else if (line === TAG_I_FRAMES_ONLY) { } else if (line.startsWith(TAG_EXTINF)) { const segmentedInput = new HlsSegmentedInput(this, rootPath, null, lines); @@ -20701,9 +19971,6 @@ if (options.isobmff !== void 0) { if (!options.isobmff || typeof options.isobmff !== "object") { throw new TypeError(`${prefix}.isobmff, when provided, must be an object.`); - } - if (options.isobmff.resolveKeyId !== void 0 && typeof options.isobmff.resolveKeyId !== "function") { - throw new TypeError(`${prefix}.isobmff.resolveKeyId, when provided, must be a function.`); } } if (options.hls !== void 0) { @@ -26909,7 +26176,7 @@ // src/input.ts polyfillSymbolDispose(); var DEFAULT_SOURCE_CACHE_GROUP = 1; -var ENCRYPTION_KEY_CACHE_GROUP = 2; +// [FluxMaster] Retiré ici : le groupe de cache des fichiers de clé (sans usage depuis P11). var Input = class _Input extends EventEmitter { /** * Creates a new input file from the specified options. No reading operations will be performed until methods are