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 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
- // Optinally, you can use this callback to handle encryption information (like key ID and PSSH boxes)
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.0";
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, decryptProtectedData, reportEncryptionInfo) {
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 = this.decryptSample ? await this.decryptSample({
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
- }) : await this.decryptWithProtectedData(encryptionInfo, encryptedSample, metadataPacket.microsecondTimestamp, keyId, psshBoxes);
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, decryptProtectedData, handleEncryptionInfo, ...options }) {
406
- if (!keys?.size && !decryptSample && !decryptProtectedData) throw new Error("Either keys, decryptSample, or decryptProtectedData must be provided.");
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 || decryptProtectedData ? new IsobmffDecryptSamplePatcher({ getTracks: (query) => this.getTracksWithoutPatching(query) }, decryptSample, decryptProtectedData, reportEncryptionInfo) : null;
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 IsobmffScheme = 'cenc' | 'cens' | 'cbcs';
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 IsobmffDecryptSampleOptions = {
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: IsobmffScheme; /** Per-sample IV resolved from container metadata. */
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
- * Options passed to {@link DecryptProtectedData}.
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: IsobmffDecryptSampleOptions) => Uint8Array | Promise<Uint8Array>;
73
- /**
74
- * Simplified protected-data decryption callback.
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 DecryptProtectedData, type DecryptSample, Conversion as Decryption, FilePathSource, FilePathTarget, Input, type MediabunnyInputOptions as InputOptions, type IsobmffDecryptProtectedDataOptions, type IsobmffDecryptSampleOptions, type Key, type KeyId, type KeyMap, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output, type OutputFormat, type OutputOptions, ReadableStreamSource, ShifroInputOptions, StreamSource, type StreamSourceOptions, StreamTarget, type StreamTargetOptions, UrlSource };
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, decryptProtectedData, reportEncryptionInfo) {
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 = this.decryptSample ? await this.decryptSample({
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
- }) : await this.decryptWithProtectedData(encryptionInfo, encryptedSample, metadataPacket.microsecondTimestamp, keyId, psshBoxes);
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, decryptProtectedData, handleEncryptionInfo, ...options }) {
399
- if (!keys?.size && !decryptSample && !decryptProtectedData) throw new Error("Either keys, decryptSample, or decryptProtectedData must be provided.");
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 || decryptProtectedData ? new IsobmffDecryptSamplePatcher({ getTracks: (query) => this.getTracksWithoutPatching(query) }, decryptSample, decryptProtectedData, reportEncryptionInfo) : null;
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shifro",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "A lightweight library and command-line tool for decrypting MP4 files",
5
5
  "files": [
6
6
  "dist"