shifro 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/dist/cli.mjs +7 -91
- package/dist/index.d.mts +12 -55
- package/dist/index.mjs +134 -91
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Run `npm install -g shifro` to install the command-line tool globally.
|
|
|
16
16
|
shifro --key eb676abbcb345e96bbcf616630f1a3da:100b6c20940f779a4589152b57d2dacb ./input.mp4 ./output.mp4
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
>You can also use `npx shifro ...` to run the CLI tool without installing it globally
|
|
19
|
+
> You can also use `npx shifro ...` to run the CLI tool without installing it globally
|
|
20
20
|
|
|
21
21
|
This CLI can be used as an alternative to the [mp4decrypt](https://www.bento4.com/documentation/mp4decrypt/) or [shaka-packager](https://shaka-project.github.io/shaka-packager/html/documentation.html#raw-key-encryption-options).
|
|
22
22
|
|
|
@@ -38,7 +38,7 @@ async function decrypt() {
|
|
|
38
38
|
['4d97930a3d7b55fa81d0028653f5e499', '429ec76475e7a952d224d8ef867f12b6'],
|
|
39
39
|
['d21373c0b8ab5ba9954742bcdfb5f48b', '150a6c7d7dee6a91b74dccfce5b31928'],
|
|
40
40
|
]),
|
|
41
|
-
//
|
|
41
|
+
// Optionally, you can use this callback to handle encryption information (like key ID and PSSH boxes)
|
|
42
42
|
handleEncryptionInfo: ({ keyId, psshBoxes }) => {
|
|
43
43
|
console.group(`Key ID: ${keyId}`);
|
|
44
44
|
for (const psshBox of psshBoxes) {
|
|
@@ -57,6 +57,38 @@ async function decrypt() {
|
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
#### Decrypting file with an external AES backend
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import {
|
|
64
|
+
Decryption,
|
|
65
|
+
FilePathSource,
|
|
66
|
+
FilePathTarget,
|
|
67
|
+
Input,
|
|
68
|
+
Output,
|
|
69
|
+
decryptBytes,
|
|
70
|
+
type EncryptedBytes,
|
|
71
|
+
} from 'shifro';
|
|
72
|
+
|
|
73
|
+
// Decrypt bytes with your DRM library
|
|
74
|
+
async function decryptProtectedData({ scheme, data, iv }: EncryptedBytes) {
|
|
75
|
+
// Your decryption logic here
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function runDecryption() {
|
|
79
|
+
const decryption = await Decryption.init({
|
|
80
|
+
input: new Input({
|
|
81
|
+
source: new FilePathSource('./input.mp4'),
|
|
82
|
+
// `decryptBytes` - helper function that allows to handle only the encrypted bytes. Clear sample bytes are not included.
|
|
83
|
+
decryptSample: decryptBytes(decryptProtectedData),
|
|
84
|
+
}),
|
|
85
|
+
output: new Output({ target: new FilePathTarget('./output.mp4') }),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await decryption.execute();
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
60
92
|
#### Decrypting file using browser's Web Streams API
|
|
61
93
|
|
|
62
94
|
```ts
|
package/dist/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { ALL_FORMATS, Conversion, EncodedPacket, FilePathSource, FilePathTarget, Input, Mp4OutputFormat, Output } from "mediabunny";
|
|
4
4
|
//#region package.json
|
|
5
5
|
var name = "shifro";
|
|
6
|
-
var version = "1.2.
|
|
6
|
+
var version = "1.2.1";
|
|
7
7
|
var author = "Vitaly Gashkov <vitalygashkov@vk.com>";
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/mediabunny-isobmff.ts
|
|
@@ -142,39 +142,6 @@ const psshBoxesAreEqual = (left, right) => {
|
|
|
142
142
|
for (let i = 0; i < left.data.byteLength; i++) if (left.data[i] !== right.data[i]) return false;
|
|
143
143
|
return true;
|
|
144
144
|
};
|
|
145
|
-
const collectCryptRanges = (subsamples, cryptByteBlock, skipByteBlock) => {
|
|
146
|
-
const ranges = [];
|
|
147
|
-
const hasPattern = cryptByteBlock !== 0 || skipByteBlock !== 0;
|
|
148
|
-
let cursor = 0;
|
|
149
|
-
for (const subsample of subsamples) {
|
|
150
|
-
cursor += subsample.clearLen;
|
|
151
|
-
if (!hasPattern) {
|
|
152
|
-
if (subsample.protectedLen > 0) ranges.push({
|
|
153
|
-
offset: cursor,
|
|
154
|
-
length: subsample.protectedLen
|
|
155
|
-
});
|
|
156
|
-
cursor += subsample.protectedLen;
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
let remaining = subsample.protectedLen;
|
|
160
|
-
let position = cursor;
|
|
161
|
-
while (remaining > 0) {
|
|
162
|
-
if (remaining < 16 * cryptByteBlock) break;
|
|
163
|
-
const cryptLength = 16 * cryptByteBlock;
|
|
164
|
-
ranges.push({
|
|
165
|
-
offset: position,
|
|
166
|
-
length: cryptLength
|
|
167
|
-
});
|
|
168
|
-
position += cryptLength;
|
|
169
|
-
remaining -= cryptLength;
|
|
170
|
-
const skipLength = Math.min(16 * skipByteBlock, remaining);
|
|
171
|
-
position += skipLength;
|
|
172
|
-
remaining -= skipLength;
|
|
173
|
-
}
|
|
174
|
-
cursor += subsample.protectedLen;
|
|
175
|
-
}
|
|
176
|
-
return ranges;
|
|
177
|
-
};
|
|
178
145
|
const cloneSubsamples = (subsamples) => {
|
|
179
146
|
if (!subsamples) return null;
|
|
180
147
|
return subsamples.map((subsample) => ({
|
|
@@ -191,34 +158,6 @@ const getPattern = (encryptionInfo) => {
|
|
|
191
158
|
skipByteBlock
|
|
192
159
|
};
|
|
193
160
|
};
|
|
194
|
-
const createProtectedDataLayout = (data, encryptionInfo, sampleEncryption) => {
|
|
195
|
-
if (!sampleEncryption.subsamples) return {
|
|
196
|
-
data,
|
|
197
|
-
merge: (decryptedData) => new Uint8Array(decryptedData)
|
|
198
|
-
};
|
|
199
|
-
if (encryptionInfo.scheme === "cbcs") return null;
|
|
200
|
-
const cryptRanges = collectCryptRanges(sampleEncryption.subsamples, encryptionInfo.defaultCryptByteBlock ?? 0, encryptionInfo.defaultSkipByteBlock ?? 0);
|
|
201
|
-
let totalCryptLength = 0;
|
|
202
|
-
for (const range of cryptRanges) totalCryptLength += range.length;
|
|
203
|
-
const protectedData = new Uint8Array(totalCryptLength);
|
|
204
|
-
let writeOffset = 0;
|
|
205
|
-
for (const range of cryptRanges) {
|
|
206
|
-
protectedData.set(data.subarray(range.offset, range.offset + range.length), writeOffset);
|
|
207
|
-
writeOffset += range.length;
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
data: protectedData,
|
|
211
|
-
merge: (decryptedData) => {
|
|
212
|
-
const merged = new Uint8Array(data);
|
|
213
|
-
let readOffset = 0;
|
|
214
|
-
for (const range of cryptRanges) {
|
|
215
|
-
merged.set(decryptedData.subarray(readOffset, readOffset + range.length), range.offset);
|
|
216
|
-
readOffset += range.length;
|
|
217
|
-
}
|
|
218
|
-
return merged;
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
};
|
|
222
161
|
const isUint8Array = (value) => value instanceof Uint8Array;
|
|
223
162
|
const isIsobmffTrackBacking = (backing) => {
|
|
224
163
|
if (!backing || typeof backing !== "object") return false;
|
|
@@ -275,14 +214,12 @@ const createMetadataOnlyOptions = (options) => ({
|
|
|
275
214
|
var IsobmffDecryptSamplePatcher = class {
|
|
276
215
|
input;
|
|
277
216
|
decryptSample;
|
|
278
|
-
decryptProtectedData;
|
|
279
217
|
reportEncryptionInfo;
|
|
280
218
|
patchableTrackBackings = /* @__PURE__ */ new WeakSet();
|
|
281
219
|
patchTrackBackingsPromise = null;
|
|
282
|
-
constructor(input, decryptSample,
|
|
220
|
+
constructor(input, decryptSample, reportEncryptionInfo) {
|
|
283
221
|
this.input = input;
|
|
284
222
|
this.decryptSample = decryptSample;
|
|
285
|
-
this.decryptProtectedData = decryptProtectedData;
|
|
286
223
|
this.reportEncryptionInfo = reportEncryptionInfo;
|
|
287
224
|
}
|
|
288
225
|
async ensurePatched() {
|
|
@@ -351,7 +288,7 @@ var IsobmffDecryptSamplePatcher = class {
|
|
|
351
288
|
if (!encryptedSample) return fallback();
|
|
352
289
|
const psshBoxes = getPsshBoxes(backing, keyId, encryptedSample.fragment);
|
|
353
290
|
this.reportEncryptionInfo(keyId, psshBoxes);
|
|
354
|
-
const decryptedData =
|
|
291
|
+
const decryptedData = await this.decryptSample({
|
|
355
292
|
data: encryptedSample.data,
|
|
356
293
|
keyId,
|
|
357
294
|
psshBoxes,
|
|
@@ -360,32 +297,11 @@ var IsobmffDecryptSamplePatcher = class {
|
|
|
360
297
|
timestamp: metadataPacket.microsecondTimestamp,
|
|
361
298
|
subsamples: cloneSubsamples(encryptedSample.sampleEncryption.subsamples),
|
|
362
299
|
pattern: getPattern(encryptionInfo)
|
|
363
|
-
})
|
|
300
|
+
});
|
|
364
301
|
if (!isUint8Array(decryptedData)) throw new TypeError("decryption callback must return a Uint8Array.");
|
|
365
302
|
if (decryptedData.byteLength !== metadataPacket.byteLength) throw new Error("decryption callback must return the same number of bytes as the encrypted sample.");
|
|
366
303
|
return new EncodedPacket(decryptedData, metadataPacket.type, metadataPacket.timestamp, metadataPacket.duration, metadataPacket.sequenceNumber, metadataPacket.byteLength, metadataPacket.sideData);
|
|
367
304
|
}
|
|
368
|
-
async decryptWithProtectedData(encryptionInfo, encryptedSample, timestamp, keyId, psshBoxes) {
|
|
369
|
-
if (!this.decryptProtectedData) throw new Error("Encrypted sample encountered without a decryption callback.");
|
|
370
|
-
const protectedDataLayout = createProtectedDataLayout(encryptedSample.data, encryptionInfo, encryptedSample.sampleEncryption);
|
|
371
|
-
if (!protectedDataLayout) throw new Error("decryptProtectedData does not support cbcs samples with subsample encryption. Use decryptSample for this content.");
|
|
372
|
-
const decryptedProtectedData = await this.decryptProtectedData({
|
|
373
|
-
data: protectedDataLayout.data,
|
|
374
|
-
keyId,
|
|
375
|
-
psshBoxes,
|
|
376
|
-
scheme: encryptionInfo.scheme,
|
|
377
|
-
iv: encryptedSample.sampleEncryption.iv.slice(),
|
|
378
|
-
timestamp,
|
|
379
|
-
subsamples: [{
|
|
380
|
-
clearLen: 0,
|
|
381
|
-
protectedLen: protectedDataLayout.data.byteLength
|
|
382
|
-
}],
|
|
383
|
-
pattern: null
|
|
384
|
-
});
|
|
385
|
-
if (!isUint8Array(decryptedProtectedData)) throw new TypeError("decryptProtectedData must return a Uint8Array.");
|
|
386
|
-
if (decryptedProtectedData.byteLength !== protectedDataLayout.data.byteLength) throw new Error("decryptProtectedData must return the same number of bytes as the protected data input.");
|
|
387
|
-
return protectedDataLayout.merge(decryptedProtectedData);
|
|
388
|
-
}
|
|
389
305
|
};
|
|
390
306
|
//#endregion
|
|
391
307
|
//#region src/index.ts
|
|
@@ -402,8 +318,8 @@ const createEncryptionReporter = (handleEncryptionInfo) => {
|
|
|
402
318
|
};
|
|
403
319
|
var Input$1 = class extends Input {
|
|
404
320
|
decryptSamplePatcher;
|
|
405
|
-
constructor({ keys, decryptSample,
|
|
406
|
-
if (!keys?.size && !decryptSample
|
|
321
|
+
constructor({ keys, decryptSample, handleEncryptionInfo, ...options }) {
|
|
322
|
+
if (!keys?.size && !decryptSample) throw new Error("Either keys or decryptSample must be provided.");
|
|
407
323
|
const reportEncryptionInfo = createEncryptionReporter(handleEncryptionInfo);
|
|
408
324
|
super({
|
|
409
325
|
...options,
|
|
@@ -415,7 +331,7 @@ var Input$1 = class extends Input {
|
|
|
415
331
|
return key;
|
|
416
332
|
} } }
|
|
417
333
|
});
|
|
418
|
-
this.decryptSamplePatcher = decryptSample
|
|
334
|
+
this.decryptSamplePatcher = decryptSample ? new IsobmffDecryptSamplePatcher({ getTracks: (query) => this.getTracksWithoutPatching(query) }, decryptSample, reportEncryptionInfo) : null;
|
|
419
335
|
}
|
|
420
336
|
async getTracks(query) {
|
|
421
337
|
await this.decryptSamplePatcher?.ensurePatched();
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Conversion, FilePathSource, FilePathTarget, Input as Input$1, InputOptions as MediabunnyInputOptions, InputTrack, InputTrackQuery, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, OutputFormat, OutputOptions, PsshBox, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource } from "mediabunny";
|
|
1
|
+
import { Conversion, FilePathSource, FilePathTarget, Input as Input$1, InputOptions as MediabunnyInputOptions, InputTrack, InputTrackQuery, MaybePromise, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, OutputFormat, OutputOptions, PsshBox, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource } from "mediabunny";
|
|
2
2
|
|
|
3
3
|
//#region src/decrypt-sample.d.ts
|
|
4
4
|
type KeyId = string;
|
|
5
5
|
type Key = string;
|
|
6
6
|
type KeyMap = Map<KeyId, Key>;
|
|
7
|
-
type
|
|
7
|
+
type EncryptionScheme = 'cenc' | 'cens' | 'cbcs';
|
|
8
8
|
type SubsampleEncryption = {
|
|
9
9
|
/** Number of clear bytes that appear before the protected bytes in this subsample. */clearLen: number; /** Number of encrypted bytes in this subsample. */
|
|
10
10
|
protectedLen: number;
|
|
@@ -23,44 +23,19 @@ type EncryptionPattern = {
|
|
|
23
23
|
* that cannot be safely represented as a single flattened protected-byte stream, such as patterned `cbcs`
|
|
24
24
|
* subsample encryption.
|
|
25
25
|
*/
|
|
26
|
-
type
|
|
26
|
+
type EncryptedSample = {
|
|
27
27
|
/** Full sample bytes as stored in the container. Clear and protected regions are both present in this buffer. */data: Uint8Array; /** Key ID from the track encryption metadata. */
|
|
28
28
|
keyId: string; /** PSSH boxes associated with this key/sample and useful for DRM-specific key resolution. */
|
|
29
29
|
psshBoxes: PsshBox[]; /** Sample encryption scheme. */
|
|
30
|
-
scheme:
|
|
30
|
+
scheme: EncryptionScheme; /** Per-sample IV resolved from container metadata. */
|
|
31
31
|
iv: Uint8Array; /** Presentation timestamp of the sample in microseconds. */
|
|
32
32
|
timestamp: number; /** Subsample layout describing which parts of `data` are clear vs protected, or `null` for whole-sample encryption. */
|
|
33
33
|
subsamples: SubsampleEncryption[] | null; /** Pattern encryption parameters, or `null` when no pattern is used. */
|
|
34
34
|
pattern: EncryptionPattern | null;
|
|
35
35
|
};
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* This is the compatibility/simple callback contract. `data` contains only the protected byte stream that needs
|
|
40
|
-
* to be decrypted. `shifro` is responsible for extracting those protected bytes from the sample and inserting the
|
|
41
|
-
* decrypted result back into the sample afterward.
|
|
42
|
-
*
|
|
43
|
-
* This matches the old `transformSample` style API used by `wapter`.
|
|
44
|
-
*/
|
|
45
|
-
type IsobmffDecryptProtectedDataOptions = {
|
|
46
|
-
/** Only the protected bytes that should be decrypted. Clear sample bytes are not included. */data: Uint8Array; /** Key ID from the track encryption metadata. */
|
|
47
|
-
keyId: string; /** PSSH boxes associated with this key/sample and useful for DRM-specific key resolution. */
|
|
48
|
-
psshBoxes: PsshBox[]; /** Sample encryption scheme. */
|
|
49
|
-
scheme: IsobmffScheme; /** Per-sample IV resolved from container metadata. */
|
|
50
|
-
iv: Uint8Array; /** Presentation timestamp of the sample in microseconds. */
|
|
51
|
-
timestamp: number;
|
|
52
|
-
/**
|
|
53
|
-
* Subsample layout for the protected-data buffer passed to this callback.
|
|
54
|
-
*
|
|
55
|
-
* Since `data` already contains only protected bytes, this is typically a single entry covering the whole buffer.
|
|
56
|
-
*/
|
|
57
|
-
subsamples: SubsampleEncryption[];
|
|
58
|
-
/**
|
|
59
|
-
* Pattern metadata for the protected-data buffer passed to this callback.
|
|
60
|
-
*
|
|
61
|
-
* This is `null` when the flattened protected-data representation no longer has any skipped blocks to describe.
|
|
62
|
-
*/
|
|
63
|
-
pattern: EncryptionPattern | null;
|
|
36
|
+
/** Options passed to {@link decryptBytes}. */
|
|
37
|
+
type EncryptedBytes = Omit<EncryptedSample, 'data'> & {
|
|
38
|
+
/** Only the encrypted bytes that should be decrypted. Clear sample bytes are not included. */data: Uint8Array;
|
|
64
39
|
};
|
|
65
40
|
/**
|
|
66
41
|
* Advanced sample decryption callback.
|
|
@@ -69,20 +44,10 @@ type IsobmffDecryptProtectedDataOptions = {
|
|
|
69
44
|
* exact same length. This is the most general integration point and should be used when a backend needs explicit
|
|
70
45
|
* subsample and pattern metadata.
|
|
71
46
|
*/
|
|
72
|
-
type DecryptSample = (options:
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
* The callback receives only the protected byte stream extracted from a sample and must return the decrypted bytes
|
|
77
|
-
* for that protected region with the exact same length. `shifro` handles extraction from and reinsertion into the
|
|
78
|
-
* original sample.
|
|
79
|
-
*
|
|
80
|
-
* This is the recommended callback for integrations that only need a raw "decrypt these bytes" operation.
|
|
81
|
-
*
|
|
82
|
-
* Note: this simplified callback is not sufficient for every encryption layout. For example, `cbcs` samples with
|
|
83
|
-
* subsample encryption still require {@link DecryptSample}.
|
|
84
|
-
*/
|
|
85
|
-
type DecryptProtectedData = (options: IsobmffDecryptProtectedDataOptions) => Uint8Array | Promise<Uint8Array>;
|
|
47
|
+
type DecryptSample = (options: EncryptedSample) => MaybePromise<Uint8Array>;
|
|
48
|
+
/** Raw byte decryption callback used by {@link decryptBytes}. */
|
|
49
|
+
type DecryptBytesCallback = (options: EncryptedBytes) => MaybePromise<Uint8Array>;
|
|
50
|
+
declare const decryptBytes: (decryptBytesCallback: DecryptBytesCallback) => DecryptSample;
|
|
86
51
|
//#endregion
|
|
87
52
|
//#region src/index.d.ts
|
|
88
53
|
/**
|
|
@@ -106,13 +71,6 @@ type ShifroInputOptions = Omit<MediabunnyInputOptions, 'formats' | 'formatOption
|
|
|
106
71
|
* metadata, or pattern encryption information.
|
|
107
72
|
*/
|
|
108
73
|
decryptSample?: DecryptSample;
|
|
109
|
-
/**
|
|
110
|
-
* Simplified decryption callback that receives only the flattened protected byte stream.
|
|
111
|
-
*
|
|
112
|
-
* `shifro` extracts the protected bytes from the sample before calling this callback and reinserts the decrypted
|
|
113
|
-
* bytes back into the sample afterward.
|
|
114
|
-
*/
|
|
115
|
-
decryptProtectedData?: DecryptProtectedData;
|
|
116
74
|
/**
|
|
117
75
|
* Optional callback invoked when encryption metadata is discovered for a sample.
|
|
118
76
|
*
|
|
@@ -129,7 +87,6 @@ declare class Input extends Input$1 {
|
|
|
129
87
|
constructor({
|
|
130
88
|
keys,
|
|
131
89
|
decryptSample,
|
|
132
|
-
decryptProtectedData,
|
|
133
90
|
handleEncryptionInfo,
|
|
134
91
|
...options
|
|
135
92
|
}: ShifroInputOptions);
|
|
@@ -144,4 +101,4 @@ declare class Output extends Output$1 {
|
|
|
144
101
|
});
|
|
145
102
|
}
|
|
146
103
|
//#endregion
|
|
147
|
-
export { type
|
|
104
|
+
export { type DecryptBytesCallback, type DecryptSample, Conversion as Decryption, type EncryptedBytes, type EncryptedSample, type EncryptionPattern, FilePathSource, FilePathTarget, Input, type MediabunnyInputOptions as InputOptions, type Key, type KeyId, type KeyMap, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output, type OutputFormat, type OutputOptions, ReadableStreamSource, ShifroInputOptions, StreamSource, type StreamSourceOptions, StreamTarget, type StreamTargetOptions, type SubsampleEncryption, UrlSource, decryptBytes };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,131 @@
|
|
|
1
1
|
import { ALL_FORMATS, Conversion, EncodedPacket, FilePathSource, FilePathTarget, Input as Input$1, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, ReadableStreamSource, StreamSource, StreamTarget, UrlSource } from "mediabunny";
|
|
2
|
+
//#region src/decrypt-sample.ts
|
|
3
|
+
const AES_BLOCK_SIZE = 16;
|
|
4
|
+
const isUint8Array$1 = (value) => value instanceof Uint8Array;
|
|
5
|
+
const cloneSubsamples$1 = (subsamples) => {
|
|
6
|
+
if (!subsamples) return null;
|
|
7
|
+
return subsamples.map((subsample) => ({
|
|
8
|
+
clearLen: subsample.clearLen,
|
|
9
|
+
protectedLen: subsample.protectedLen
|
|
10
|
+
}));
|
|
11
|
+
};
|
|
12
|
+
const clonePattern = (pattern) => {
|
|
13
|
+
if (!pattern) return null;
|
|
14
|
+
return {
|
|
15
|
+
cryptByteBlock: pattern.cryptByteBlock,
|
|
16
|
+
skipByteBlock: pattern.skipByteBlock
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const collectProtectedRanges = (offset, protectedLen, pattern, fullBlocksOnly) => {
|
|
20
|
+
if (!pattern) {
|
|
21
|
+
const length = fullBlocksOnly ? protectedLen - protectedLen % AES_BLOCK_SIZE : protectedLen;
|
|
22
|
+
return length > 0 ? [{
|
|
23
|
+
offset,
|
|
24
|
+
length
|
|
25
|
+
}] : [];
|
|
26
|
+
}
|
|
27
|
+
if (pattern.cryptByteBlock <= 0) return [];
|
|
28
|
+
const ranges = [];
|
|
29
|
+
const cryptLen = AES_BLOCK_SIZE * pattern.cryptByteBlock;
|
|
30
|
+
const skipLen = AES_BLOCK_SIZE * pattern.skipByteBlock;
|
|
31
|
+
let remaining = protectedLen;
|
|
32
|
+
let position = offset;
|
|
33
|
+
while (remaining > 0) {
|
|
34
|
+
if (remaining < cryptLen) break;
|
|
35
|
+
ranges.push({
|
|
36
|
+
offset: position,
|
|
37
|
+
length: cryptLen
|
|
38
|
+
});
|
|
39
|
+
position += cryptLen;
|
|
40
|
+
remaining -= cryptLen;
|
|
41
|
+
const currentSkipLen = Math.min(skipLen, remaining);
|
|
42
|
+
position += currentSkipLen;
|
|
43
|
+
remaining -= currentSkipLen;
|
|
44
|
+
}
|
|
45
|
+
return ranges;
|
|
46
|
+
};
|
|
47
|
+
const collectCtrRanges = (options) => {
|
|
48
|
+
if (!options.subsamples) return [{
|
|
49
|
+
offset: 0,
|
|
50
|
+
length: options.data.byteLength
|
|
51
|
+
}];
|
|
52
|
+
const ranges = [];
|
|
53
|
+
let cursor = 0;
|
|
54
|
+
for (const subsample of options.subsamples) {
|
|
55
|
+
cursor += subsample.clearLen;
|
|
56
|
+
ranges.push(...collectProtectedRanges(cursor, subsample.protectedLen, options.pattern, false));
|
|
57
|
+
cursor += subsample.protectedLen;
|
|
58
|
+
}
|
|
59
|
+
return ranges;
|
|
60
|
+
};
|
|
61
|
+
const collectCbcsGroups = (options) => {
|
|
62
|
+
if (!options.subsamples) return [{
|
|
63
|
+
iv: options.iv.slice(),
|
|
64
|
+
ranges: [{
|
|
65
|
+
offset: 0,
|
|
66
|
+
length: options.data.byteLength - options.data.byteLength % AES_BLOCK_SIZE
|
|
67
|
+
}]
|
|
68
|
+
}];
|
|
69
|
+
if (!options.pattern) throw new Error("decryptBytes does not support cbcs subsample encryption without pattern encryption.");
|
|
70
|
+
const groups = [];
|
|
71
|
+
let cursor = 0;
|
|
72
|
+
for (const subsample of options.subsamples) {
|
|
73
|
+
cursor += subsample.clearLen;
|
|
74
|
+
groups.push({
|
|
75
|
+
iv: options.iv.slice(),
|
|
76
|
+
ranges: collectProtectedRanges(cursor, subsample.protectedLen, options.pattern, true)
|
|
77
|
+
});
|
|
78
|
+
cursor += subsample.protectedLen;
|
|
79
|
+
}
|
|
80
|
+
return groups;
|
|
81
|
+
};
|
|
82
|
+
const getRangeData = (data, ranges) => {
|
|
83
|
+
let totalLength = 0;
|
|
84
|
+
for (const range of ranges) totalLength += range.length;
|
|
85
|
+
const protectedData = new Uint8Array(totalLength);
|
|
86
|
+
let writeOffset = 0;
|
|
87
|
+
for (const range of ranges) {
|
|
88
|
+
protectedData.set(data.subarray(range.offset, range.offset + range.length), writeOffset);
|
|
89
|
+
writeOffset += range.length;
|
|
90
|
+
}
|
|
91
|
+
return protectedData;
|
|
92
|
+
};
|
|
93
|
+
const mergeRangeData = (target, decryptedData, ranges) => {
|
|
94
|
+
let readOffset = 0;
|
|
95
|
+
for (const range of ranges) {
|
|
96
|
+
target.set(decryptedData.subarray(readOffset, readOffset + range.length), range.offset);
|
|
97
|
+
readOffset += range.length;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const decryptGroups = async (options, groups, decryptBytesCallback) => {
|
|
101
|
+
const decryptedSample = new Uint8Array(options.data);
|
|
102
|
+
for (const group of groups) {
|
|
103
|
+
const encryptedData = getRangeData(options.data, group.ranges);
|
|
104
|
+
if (encryptedData.byteLength === 0) continue;
|
|
105
|
+
const decryptedData = await decryptBytesCallback({
|
|
106
|
+
data: encryptedData,
|
|
107
|
+
keyId: options.keyId,
|
|
108
|
+
psshBoxes: options.psshBoxes,
|
|
109
|
+
scheme: options.scheme,
|
|
110
|
+
iv: group.iv.slice(),
|
|
111
|
+
timestamp: options.timestamp,
|
|
112
|
+
subsamples: cloneSubsamples$1(options.subsamples),
|
|
113
|
+
pattern: clonePattern(options.pattern)
|
|
114
|
+
});
|
|
115
|
+
if (!isUint8Array$1(decryptedData)) throw new TypeError("decryptBytes callback must return a Uint8Array.");
|
|
116
|
+
if (decryptedData.byteLength !== encryptedData.byteLength) throw new Error("decryptBytes callback must return the same number of bytes as the encrypted input.");
|
|
117
|
+
mergeRangeData(decryptedSample, decryptedData, group.ranges);
|
|
118
|
+
}
|
|
119
|
+
return decryptedSample;
|
|
120
|
+
};
|
|
121
|
+
const decryptBytes = (decryptBytesCallback) => async (options) => {
|
|
122
|
+
if (options.scheme === "cbcs") return decryptGroups(options, collectCbcsGroups(options), decryptBytesCallback);
|
|
123
|
+
return decryptGroups(options, [{
|
|
124
|
+
iv: options.iv.slice(),
|
|
125
|
+
ranges: collectCtrRanges(options)
|
|
126
|
+
}], decryptBytesCallback);
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
2
129
|
//#region src/mediabunny-isobmff.ts
|
|
3
130
|
const binarySearchLessOrEqual = (items, needle, getValue) => {
|
|
4
131
|
let low = 0;
|
|
@@ -135,39 +262,6 @@ const psshBoxesAreEqual = (left, right) => {
|
|
|
135
262
|
for (let i = 0; i < left.data.byteLength; i++) if (left.data[i] !== right.data[i]) return false;
|
|
136
263
|
return true;
|
|
137
264
|
};
|
|
138
|
-
const collectCryptRanges = (subsamples, cryptByteBlock, skipByteBlock) => {
|
|
139
|
-
const ranges = [];
|
|
140
|
-
const hasPattern = cryptByteBlock !== 0 || skipByteBlock !== 0;
|
|
141
|
-
let cursor = 0;
|
|
142
|
-
for (const subsample of subsamples) {
|
|
143
|
-
cursor += subsample.clearLen;
|
|
144
|
-
if (!hasPattern) {
|
|
145
|
-
if (subsample.protectedLen > 0) ranges.push({
|
|
146
|
-
offset: cursor,
|
|
147
|
-
length: subsample.protectedLen
|
|
148
|
-
});
|
|
149
|
-
cursor += subsample.protectedLen;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
let remaining = subsample.protectedLen;
|
|
153
|
-
let position = cursor;
|
|
154
|
-
while (remaining > 0) {
|
|
155
|
-
if (remaining < 16 * cryptByteBlock) break;
|
|
156
|
-
const cryptLength = 16 * cryptByteBlock;
|
|
157
|
-
ranges.push({
|
|
158
|
-
offset: position,
|
|
159
|
-
length: cryptLength
|
|
160
|
-
});
|
|
161
|
-
position += cryptLength;
|
|
162
|
-
remaining -= cryptLength;
|
|
163
|
-
const skipLength = Math.min(16 * skipByteBlock, remaining);
|
|
164
|
-
position += skipLength;
|
|
165
|
-
remaining -= skipLength;
|
|
166
|
-
}
|
|
167
|
-
cursor += subsample.protectedLen;
|
|
168
|
-
}
|
|
169
|
-
return ranges;
|
|
170
|
-
};
|
|
171
265
|
const cloneSubsamples = (subsamples) => {
|
|
172
266
|
if (!subsamples) return null;
|
|
173
267
|
return subsamples.map((subsample) => ({
|
|
@@ -184,34 +278,6 @@ const getPattern = (encryptionInfo) => {
|
|
|
184
278
|
skipByteBlock
|
|
185
279
|
};
|
|
186
280
|
};
|
|
187
|
-
const createProtectedDataLayout = (data, encryptionInfo, sampleEncryption) => {
|
|
188
|
-
if (!sampleEncryption.subsamples) return {
|
|
189
|
-
data,
|
|
190
|
-
merge: (decryptedData) => new Uint8Array(decryptedData)
|
|
191
|
-
};
|
|
192
|
-
if (encryptionInfo.scheme === "cbcs") return null;
|
|
193
|
-
const cryptRanges = collectCryptRanges(sampleEncryption.subsamples, encryptionInfo.defaultCryptByteBlock ?? 0, encryptionInfo.defaultSkipByteBlock ?? 0);
|
|
194
|
-
let totalCryptLength = 0;
|
|
195
|
-
for (const range of cryptRanges) totalCryptLength += range.length;
|
|
196
|
-
const protectedData = new Uint8Array(totalCryptLength);
|
|
197
|
-
let writeOffset = 0;
|
|
198
|
-
for (const range of cryptRanges) {
|
|
199
|
-
protectedData.set(data.subarray(range.offset, range.offset + range.length), writeOffset);
|
|
200
|
-
writeOffset += range.length;
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
data: protectedData,
|
|
204
|
-
merge: (decryptedData) => {
|
|
205
|
-
const merged = new Uint8Array(data);
|
|
206
|
-
let readOffset = 0;
|
|
207
|
-
for (const range of cryptRanges) {
|
|
208
|
-
merged.set(decryptedData.subarray(readOffset, readOffset + range.length), range.offset);
|
|
209
|
-
readOffset += range.length;
|
|
210
|
-
}
|
|
211
|
-
return merged;
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
};
|
|
215
281
|
const isUint8Array = (value) => value instanceof Uint8Array;
|
|
216
282
|
const isIsobmffTrackBacking = (backing) => {
|
|
217
283
|
if (!backing || typeof backing !== "object") return false;
|
|
@@ -268,14 +334,12 @@ const createMetadataOnlyOptions = (options) => ({
|
|
|
268
334
|
var IsobmffDecryptSamplePatcher = class {
|
|
269
335
|
input;
|
|
270
336
|
decryptSample;
|
|
271
|
-
decryptProtectedData;
|
|
272
337
|
reportEncryptionInfo;
|
|
273
338
|
patchableTrackBackings = /* @__PURE__ */ new WeakSet();
|
|
274
339
|
patchTrackBackingsPromise = null;
|
|
275
|
-
constructor(input, decryptSample,
|
|
340
|
+
constructor(input, decryptSample, reportEncryptionInfo) {
|
|
276
341
|
this.input = input;
|
|
277
342
|
this.decryptSample = decryptSample;
|
|
278
|
-
this.decryptProtectedData = decryptProtectedData;
|
|
279
343
|
this.reportEncryptionInfo = reportEncryptionInfo;
|
|
280
344
|
}
|
|
281
345
|
async ensurePatched() {
|
|
@@ -344,7 +408,7 @@ var IsobmffDecryptSamplePatcher = class {
|
|
|
344
408
|
if (!encryptedSample) return fallback();
|
|
345
409
|
const psshBoxes = getPsshBoxes(backing, keyId, encryptedSample.fragment);
|
|
346
410
|
this.reportEncryptionInfo(keyId, psshBoxes);
|
|
347
|
-
const decryptedData =
|
|
411
|
+
const decryptedData = await this.decryptSample({
|
|
348
412
|
data: encryptedSample.data,
|
|
349
413
|
keyId,
|
|
350
414
|
psshBoxes,
|
|
@@ -353,32 +417,11 @@ var IsobmffDecryptSamplePatcher = class {
|
|
|
353
417
|
timestamp: metadataPacket.microsecondTimestamp,
|
|
354
418
|
subsamples: cloneSubsamples(encryptedSample.sampleEncryption.subsamples),
|
|
355
419
|
pattern: getPattern(encryptionInfo)
|
|
356
|
-
})
|
|
420
|
+
});
|
|
357
421
|
if (!isUint8Array(decryptedData)) throw new TypeError("decryption callback must return a Uint8Array.");
|
|
358
422
|
if (decryptedData.byteLength !== metadataPacket.byteLength) throw new Error("decryption callback must return the same number of bytes as the encrypted sample.");
|
|
359
423
|
return new EncodedPacket(decryptedData, metadataPacket.type, metadataPacket.timestamp, metadataPacket.duration, metadataPacket.sequenceNumber, metadataPacket.byteLength, metadataPacket.sideData);
|
|
360
424
|
}
|
|
361
|
-
async decryptWithProtectedData(encryptionInfo, encryptedSample, timestamp, keyId, psshBoxes) {
|
|
362
|
-
if (!this.decryptProtectedData) throw new Error("Encrypted sample encountered without a decryption callback.");
|
|
363
|
-
const protectedDataLayout = createProtectedDataLayout(encryptedSample.data, encryptionInfo, encryptedSample.sampleEncryption);
|
|
364
|
-
if (!protectedDataLayout) throw new Error("decryptProtectedData does not support cbcs samples with subsample encryption. Use decryptSample for this content.");
|
|
365
|
-
const decryptedProtectedData = await this.decryptProtectedData({
|
|
366
|
-
data: protectedDataLayout.data,
|
|
367
|
-
keyId,
|
|
368
|
-
psshBoxes,
|
|
369
|
-
scheme: encryptionInfo.scheme,
|
|
370
|
-
iv: encryptedSample.sampleEncryption.iv.slice(),
|
|
371
|
-
timestamp,
|
|
372
|
-
subsamples: [{
|
|
373
|
-
clearLen: 0,
|
|
374
|
-
protectedLen: protectedDataLayout.data.byteLength
|
|
375
|
-
}],
|
|
376
|
-
pattern: null
|
|
377
|
-
});
|
|
378
|
-
if (!isUint8Array(decryptedProtectedData)) throw new TypeError("decryptProtectedData must return a Uint8Array.");
|
|
379
|
-
if (decryptedProtectedData.byteLength !== protectedDataLayout.data.byteLength) throw new Error("decryptProtectedData must return the same number of bytes as the protected data input.");
|
|
380
|
-
return protectedDataLayout.merge(decryptedProtectedData);
|
|
381
|
-
}
|
|
382
425
|
};
|
|
383
426
|
//#endregion
|
|
384
427
|
//#region src/index.ts
|
|
@@ -395,8 +438,8 @@ const createEncryptionReporter = (handleEncryptionInfo) => {
|
|
|
395
438
|
};
|
|
396
439
|
var Input = class extends Input$1 {
|
|
397
440
|
decryptSamplePatcher;
|
|
398
|
-
constructor({ keys, decryptSample,
|
|
399
|
-
if (!keys?.size && !decryptSample
|
|
441
|
+
constructor({ keys, decryptSample, handleEncryptionInfo, ...options }) {
|
|
442
|
+
if (!keys?.size && !decryptSample) throw new Error("Either keys or decryptSample must be provided.");
|
|
400
443
|
const reportEncryptionInfo = createEncryptionReporter(handleEncryptionInfo);
|
|
401
444
|
super({
|
|
402
445
|
...options,
|
|
@@ -408,7 +451,7 @@ var Input = class extends Input$1 {
|
|
|
408
451
|
return key;
|
|
409
452
|
} } }
|
|
410
453
|
});
|
|
411
|
-
this.decryptSamplePatcher = decryptSample
|
|
454
|
+
this.decryptSamplePatcher = decryptSample ? new IsobmffDecryptSamplePatcher({ getTracks: (query) => this.getTracksWithoutPatching(query) }, decryptSample, reportEncryptionInfo) : null;
|
|
412
455
|
}
|
|
413
456
|
async getTracks(query) {
|
|
414
457
|
await this.decryptSamplePatcher?.ensurePatched();
|
|
@@ -427,4 +470,4 @@ var Output = class extends Output$1 {
|
|
|
427
470
|
}
|
|
428
471
|
};
|
|
429
472
|
//#endregion
|
|
430
|
-
export { Conversion as Decryption, FilePathSource, FilePathTarget, Input, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output, ReadableStreamSource, StreamSource, StreamTarget, UrlSource };
|
|
473
|
+
export { Conversion as Decryption, FilePathSource, FilePathTarget, Input, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output, ReadableStreamSource, StreamSource, StreamTarget, UrlSource, decryptBytes };
|