shifro 0.2.0 → 1.0.0
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/LICENSE +21 -661
- package/README.md +51 -121
- package/dist/cli.mjs +81 -0
- package/dist/index.cjs +43 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +25 -0
- package/package.json +25 -21
- package/dist/cli.cjs +0 -15
- package/dist/shifro.cjs +0 -1066
- package/dist/shifro.d.cts +0 -101
- package/dist/shifro.d.ts +0 -101
- package/dist/shifro.js +0 -1033
- /package/dist/{cli.d.cts → cli.d.mts} +0 -0
package/README.md
CHANGED
|
@@ -1,155 +1,85 @@
|
|
|
1
1
|
# shifro
|
|
2
2
|
|
|
3
|
-
A lightweight library for decrypting MP4 files, directly in the browser
|
|
3
|
+
A lightweight library for decrypting MP4 files, directly in the browser or via CLI on your machine.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This tool is a small wrapper around the [Mediabunny](https://github.com/Vanilagy/mediabunny) library with slight API changes and command-line support.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Small** size (under 15kB without types, minified)
|
|
9
|
-
- **Command-line** interface
|
|
10
|
-
- **Segment-by-segment processing** with JavaScript library
|
|
11
|
-
- **Custom handler** for samples processing
|
|
12
|
-
|
|
13
|
-
## Prerequisites
|
|
14
|
-
|
|
15
|
-
- [Node.js](https://nodejs.org/en)
|
|
16
|
-
|
|
17
|
-
## Installation
|
|
18
|
-
|
|
19
|
-
Install library as dependency for your project:
|
|
7
|
+
## Usage
|
|
20
8
|
|
|
21
|
-
|
|
22
|
-
npm install shifro
|
|
23
|
-
```
|
|
9
|
+
### CLI
|
|
24
10
|
|
|
25
|
-
|
|
11
|
+
Run `npm install -g shifro` to install the command-line tool globally ([Node.js](https://nodejs.org/en/download) required).
|
|
26
12
|
|
|
27
13
|
```bash
|
|
28
|
-
|
|
14
|
+
shifro --key eb676abbcb345e96bbcf616630f1a3da:100b6c20940f779a4589152b57d2dacb ./input.mp4 ./output.mp4
|
|
29
15
|
```
|
|
30
16
|
|
|
31
|
-
|
|
17
|
+
This 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).
|
|
32
18
|
|
|
33
19
|
### Library
|
|
34
20
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
import { Input, StreamSource, Output, StreamTarget, Decryption } from 'shifro';
|
|
21
|
+
Run `npm install shifro` to install the library as dependency for your project.
|
|
39
22
|
|
|
40
|
-
|
|
41
|
-
new Promise<File>((resolve) => {
|
|
42
|
-
const input = document.querySelector<HTMLInputElement>('#input')!;
|
|
43
|
-
input.addEventListener('change', () => resolve(input.files![0]));
|
|
44
|
-
});
|
|
23
|
+
#### Decrypting file using Node.js streams
|
|
45
24
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const inputStream = inputFile.stream();
|
|
25
|
+
```ts
|
|
26
|
+
import { Decryption, Input, FilePathSource, KeyId, Key, Output, FilePathTarget } from 'shifro';
|
|
49
27
|
|
|
50
|
-
|
|
51
|
-
const outputStream = await outputFileHandle.createWritable();
|
|
52
|
-
|
|
53
|
-
const input = new Input({ source: new StreamSource(inputStream) });
|
|
54
|
-
const output = new Output({ target: new StreamTarget(outputStream) });
|
|
28
|
+
async function decrypt() {
|
|
55
29
|
const decryption = await Decryption.init({
|
|
56
|
-
input
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
onProgress: (progress) => console.log(`Decrypting... ${progress}`),
|
|
30
|
+
input: new Input({
|
|
31
|
+
source: new FilePathSource('./input.mp4'),
|
|
32
|
+
keys: new Map<KeyId, Key>([
|
|
33
|
+
['4d97930a3d7b55fa81d0028653f5e499', '429ec76475e7a952d224d8ef867f12b6'],
|
|
34
|
+
['d21373c0b8ab5ba9954742bcdfb5f48b', '150a6c7d7dee6a91b74dccfce5b31928'],
|
|
35
|
+
])
|
|
36
|
+
}),
|
|
37
|
+
output: new Output({ target: new FilePathTarget('./output.mp4') }),
|
|
65
38
|
});
|
|
66
|
-
await decryption.execute();
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
39
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
import { Input, FilePathSource, Output, FilePathTarget, Decryption } from 'shifro';
|
|
40
|
+
decryption.onProgress = (progress) => process.stdout.write(`\rDecrypting... [${Math.round(progress * 100)}%]`),
|
|
74
41
|
|
|
75
|
-
async function main() {
|
|
76
|
-
const input = new Input({ source: new FilePathSource('./input.mp4') });
|
|
77
|
-
const output = new Output({ target: new FilePathTarget('./output.mp4') });
|
|
78
|
-
const decryption = await Decryption.init({
|
|
79
|
-
input,
|
|
80
|
-
output,
|
|
81
|
-
keys: [
|
|
82
|
-
{
|
|
83
|
-
kid: 'eb676abbcb345e96bbcf616630f1a3da',
|
|
84
|
-
key: '100b6c20940f779a4589152b57d2dacb',
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
onProgress: (progress) => process.stdout.write(`\rDecrypting... [${progress}]`),
|
|
88
|
-
});
|
|
89
42
|
await decryption.execute();
|
|
90
43
|
}
|
|
91
44
|
```
|
|
92
45
|
|
|
93
|
-
####
|
|
94
|
-
|
|
95
|
-
```js
|
|
96
|
-
import { decryptSegment } from 'shifro';
|
|
46
|
+
#### Decrypting file using browser's Web Streams API
|
|
97
47
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const encryptionScheme = 'cenc';
|
|
48
|
+
```ts
|
|
49
|
+
import { Decryption, Input, ReadableStreamSource, KeyId, Key, Output, StreamTarget } from 'shifro';
|
|
101
50
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
51
|
+
async function decryptFromBrowser() {
|
|
52
|
+
const handleFileSelect = async () => {
|
|
53
|
+
return new Promise<File>((resolve) => {
|
|
54
|
+
const input = document.querySelector<HTMLInputElement>('#input')!;
|
|
55
|
+
input.addEventListener('change', () => resolve(input.files![0]));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
107
58
|
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
decryptedSegments.push(decrypted);
|
|
111
|
-
}
|
|
59
|
+
const inputFile = await handleFileSelect();
|
|
60
|
+
const inputReadableStream = inputFile.stream();
|
|
112
61
|
|
|
113
|
-
|
|
114
|
-
|
|
62
|
+
const outputFileHandle = window.showSaveFilePicker({ suggestedName: 'output.mp4', startIn: 'downloads' });
|
|
63
|
+
const outputWritableStream = await outputFileHandle.createWritable();
|
|
115
64
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const encryptedSegments = [
|
|
127
|
-
// List of binary MP4 segments (Uint8Array)
|
|
128
|
-
// ...
|
|
129
|
-
];
|
|
130
|
-
const decryptedSegments = [];
|
|
131
|
-
|
|
132
|
-
for (const segment of encryptedSegments) {
|
|
133
|
-
const decrypted = await decryptSegment(segment, {
|
|
134
|
-
keyId,
|
|
135
|
-
transformSample: (params) => {
|
|
136
|
-
// Implement custom handler for subsample processing
|
|
137
|
-
// ...
|
|
138
|
-
const sampleData = params.data;
|
|
139
|
-
const iv = params.iv;
|
|
140
|
-
const decipher = createDecipheriv('aes-128-ctr', key, iv);
|
|
141
|
-
const decrypted = Buffer.concat([decipher.update(sampleData), decipher.final()]);
|
|
142
|
-
return decrypted;
|
|
143
|
-
},
|
|
65
|
+
const decryption = await Decryption.init({
|
|
66
|
+
input: new Input({
|
|
67
|
+
source: new ReadableStreamSource(inputReadableStream),
|
|
68
|
+
keys: new Map<KeyId, Key>([
|
|
69
|
+
['4d97930a3d7b55fa81d0028653f5e499', '429ec76475e7a952d224d8ef867f12b6'],
|
|
70
|
+
['d21373c0b8ab5ba9954742bcdfb5f48b', '150a6c7d7dee6a91b74dccfce5b31928'],
|
|
71
|
+
])
|
|
72
|
+
}),
|
|
73
|
+
output: new Output({ target: new StreamTarget(outputWritableStream) }),
|
|
144
74
|
});
|
|
145
|
-
decryptedSegments.push(decrypted);
|
|
146
|
-
}
|
|
147
75
|
|
|
148
|
-
|
|
76
|
+
decryption.onProgress = (progress) => console.log(`Decrypting... [${Math.round(progress * 100)}%]`),
|
|
77
|
+
|
|
78
|
+
await decryption.execute();
|
|
79
|
+
}
|
|
149
80
|
```
|
|
150
81
|
|
|
151
|
-
### CLI
|
|
152
82
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
83
|
+
## Credits
|
|
84
|
+
|
|
85
|
+
- [mediabunny](https://github.com/Vanilagy/mediabunny)
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { ALL_FORMATS, Conversion, FilePathSource, FilePathTarget, Input, Mp4OutputFormat, Output } from "mediabunny";
|
|
4
|
+
//#region package.json
|
|
5
|
+
var name = "shifro";
|
|
6
|
+
var version = "1.0.0";
|
|
7
|
+
var author = "Vitaly Gashkov <vitalygashkov@vk.com>";
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/main.ts
|
|
10
|
+
var Input$1 = class extends Input {
|
|
11
|
+
constructor({ keys, ...options }) {
|
|
12
|
+
super({
|
|
13
|
+
...options,
|
|
14
|
+
formats: ALL_FORMATS,
|
|
15
|
+
formatOptions: { isobmff: { resolveKeyId: ({ keyId }) => {
|
|
16
|
+
const key = keys.get(keyId);
|
|
17
|
+
if (!key) throw new Error("Unknown key ID.");
|
|
18
|
+
return key;
|
|
19
|
+
} } }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var Output$1 = class extends Output {
|
|
24
|
+
constructor({ ...options }) {
|
|
25
|
+
super({
|
|
26
|
+
...options,
|
|
27
|
+
format: options.format ?? new Mp4OutputFormat()
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/cli.ts
|
|
33
|
+
const args = parseArgs({
|
|
34
|
+
allowPositionals: true,
|
|
35
|
+
strict: false,
|
|
36
|
+
options: {
|
|
37
|
+
key: {
|
|
38
|
+
short: "k",
|
|
39
|
+
type: "string",
|
|
40
|
+
multiple: true
|
|
41
|
+
},
|
|
42
|
+
help: {
|
|
43
|
+
short: "h",
|
|
44
|
+
type: "boolean"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const keyStrings = Array.isArray(args.values.key) ? args.values.key : [];
|
|
49
|
+
const [inputPath, outputPath] = args.positionals;
|
|
50
|
+
const decryptFile = async () => {
|
|
51
|
+
console.time("\nDone!");
|
|
52
|
+
const decryption = await Conversion.init({
|
|
53
|
+
input: new Input$1({
|
|
54
|
+
source: new FilePathSource(inputPath),
|
|
55
|
+
keys: new Map(keyStrings.map((keyString) => keyString.split(":")))
|
|
56
|
+
}),
|
|
57
|
+
output: new Output$1({ target: new FilePathTarget(outputPath) })
|
|
58
|
+
});
|
|
59
|
+
decryption.onProgress = (progress) => process.stdout.write(`\rDecrypting... [${Math.round(progress * 100)}%]`), await decryption.execute();
|
|
60
|
+
console.timeEnd("\nDone!");
|
|
61
|
+
};
|
|
62
|
+
const showHelp = () => {
|
|
63
|
+
console.log(`
|
|
64
|
+
${name} version ${version}
|
|
65
|
+
(c) 2024-2026 ${author}
|
|
66
|
+
|
|
67
|
+
Usage: ${name} [options] <input> <output>
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--key <id>:<k>
|
|
71
|
+
<id> is either a track ID in decimal or a 128-bit KID in hex,
|
|
72
|
+
<k> is a 128-bit key in hex
|
|
73
|
+
(several --key options can be used, one for each track or KID)
|
|
74
|
+
`.trim());
|
|
75
|
+
};
|
|
76
|
+
(async () => {
|
|
77
|
+
if (inputPath && outputPath && keyStrings.length > 0) await decryptFile();
|
|
78
|
+
else showHelp();
|
|
79
|
+
})();
|
|
80
|
+
//#endregion
|
|
81
|
+
export {};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let mediabunny = require("mediabunny");
|
|
3
|
+
//#region src/main.ts
|
|
4
|
+
var Input = class extends mediabunny.Input {
|
|
5
|
+
constructor({ keys, ...options }) {
|
|
6
|
+
super({
|
|
7
|
+
...options,
|
|
8
|
+
formats: mediabunny.ALL_FORMATS,
|
|
9
|
+
formatOptions: { isobmff: { resolveKeyId: ({ keyId }) => {
|
|
10
|
+
const key = keys.get(keyId);
|
|
11
|
+
if (!key) throw new Error("Unknown key ID.");
|
|
12
|
+
return key;
|
|
13
|
+
} } }
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var Output = class extends mediabunny.Output {
|
|
18
|
+
constructor({ ...options }) {
|
|
19
|
+
super({
|
|
20
|
+
...options,
|
|
21
|
+
format: options.format ?? new mediabunny.Mp4OutputFormat()
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
exports.Decryption = mediabunny.Conversion;
|
|
27
|
+
exports.FilePathSource = mediabunny.FilePathSource;
|
|
28
|
+
exports.FilePathTarget = mediabunny.FilePathTarget;
|
|
29
|
+
exports.Input = Input;
|
|
30
|
+
exports.InputOptions = mediabunny.InputOptions;
|
|
31
|
+
exports.MkvOutputFormat = mediabunny.MkvOutputFormat;
|
|
32
|
+
exports.MovOutputFormat = mediabunny.MovOutputFormat;
|
|
33
|
+
exports.Mp3OutputFormat = mediabunny.Mp3OutputFormat;
|
|
34
|
+
exports.Mp4OutputFormat = mediabunny.Mp4OutputFormat;
|
|
35
|
+
exports.Output = Output;
|
|
36
|
+
exports.OutputFormat = mediabunny.OutputFormat;
|
|
37
|
+
exports.OutputOptions = mediabunny.OutputOptions;
|
|
38
|
+
exports.ReadableStreamSource = mediabunny.ReadableStreamSource;
|
|
39
|
+
exports.StreamSource = mediabunny.StreamSource;
|
|
40
|
+
exports.StreamSourceOptions = mediabunny.StreamSourceOptions;
|
|
41
|
+
exports.StreamTarget = mediabunny.StreamTarget;
|
|
42
|
+
exports.StreamTargetOptions = mediabunny.StreamTargetOptions;
|
|
43
|
+
exports.UrlSource = mediabunny.UrlSource;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Conversion, FilePathSource, FilePathTarget, Input as Input$1, InputOptions, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, OutputFormat, OutputOptions, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource } from "mediabunny";
|
|
2
|
+
|
|
3
|
+
//#region src/main.d.ts
|
|
4
|
+
type KeyId = string;
|
|
5
|
+
type Key = string;
|
|
6
|
+
type KeyMap = Map<KeyId, Key>;
|
|
7
|
+
declare class Input extends Input$1 {
|
|
8
|
+
constructor({
|
|
9
|
+
keys,
|
|
10
|
+
...options
|
|
11
|
+
}: Omit<InputOptions, 'formats' | 'formatOptions'> & {
|
|
12
|
+
keys: KeyMap;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
declare class Output extends Output$1 {
|
|
16
|
+
constructor({
|
|
17
|
+
...options
|
|
18
|
+
}: Omit<OutputOptions, 'format'> & {
|
|
19
|
+
format?: OutputFormat;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { Conversion as Decryption, FilePathSource, FilePathTarget, Input, InputOptions, Key, KeyId, KeyMap, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output, OutputFormat, OutputOptions, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Conversion, FilePathSource, FilePathTarget, Input as Input$1, InputOptions, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, OutputFormat, OutputOptions, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource } from "mediabunny";
|
|
2
|
+
|
|
3
|
+
//#region src/main.d.ts
|
|
4
|
+
type KeyId = string;
|
|
5
|
+
type Key = string;
|
|
6
|
+
type KeyMap = Map<KeyId, Key>;
|
|
7
|
+
declare class Input extends Input$1 {
|
|
8
|
+
constructor({
|
|
9
|
+
keys,
|
|
10
|
+
...options
|
|
11
|
+
}: Omit<InputOptions, 'formats' | 'formatOptions'> & {
|
|
12
|
+
keys: KeyMap;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
declare class Output extends Output$1 {
|
|
16
|
+
constructor({
|
|
17
|
+
...options
|
|
18
|
+
}: Omit<OutputOptions, 'format'> & {
|
|
19
|
+
format?: OutputFormat;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { Conversion as Decryption, FilePathSource, FilePathTarget, Input, InputOptions, Key, KeyId, KeyMap, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output, OutputFormat, OutputOptions, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ALL_FORMATS, Conversion, FilePathSource, FilePathTarget, Input as Input$1, InputOptions, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output as Output$1, OutputFormat, OutputOptions, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource } from "mediabunny";
|
|
2
|
+
//#region src/main.ts
|
|
3
|
+
var Input = class extends Input$1 {
|
|
4
|
+
constructor({ keys, ...options }) {
|
|
5
|
+
super({
|
|
6
|
+
...options,
|
|
7
|
+
formats: ALL_FORMATS,
|
|
8
|
+
formatOptions: { isobmff: { resolveKeyId: ({ keyId }) => {
|
|
9
|
+
const key = keys.get(keyId);
|
|
10
|
+
if (!key) throw new Error("Unknown key ID.");
|
|
11
|
+
return key;
|
|
12
|
+
} } }
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var Output = class extends Output$1 {
|
|
17
|
+
constructor({ ...options }) {
|
|
18
|
+
super({
|
|
19
|
+
...options,
|
|
20
|
+
format: options.format ?? new Mp4OutputFormat()
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { Conversion as Decryption, FilePathSource, FilePathTarget, Input, InputOptions, MkvOutputFormat, MovOutputFormat, Mp3OutputFormat, Mp4OutputFormat, Output, OutputFormat, OutputOptions, ReadableStreamSource, StreamSource, StreamSourceOptions, StreamTarget, StreamTargetOptions, UrlSource };
|
package/package.json
CHANGED
|
@@ -1,41 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shifro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A lightweight library for decrypting MP4 files, directly in the browser",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"start": "node ./dist/cli.
|
|
9
|
+
"start": "node ./dist/cli.mjs",
|
|
10
10
|
"web": "vite",
|
|
11
|
-
"build": "npm run
|
|
12
|
-
"
|
|
13
|
-
"
|
|
11
|
+
"build": "npm run bundle:lib && npm run bundle:cli",
|
|
12
|
+
"bundle:lib": "tsdown ./src/index.ts --format cjs --format esm --dts --platform neutral",
|
|
13
|
+
"bundle:cli": "tsdown ./src/cli.ts --format esm --no-clean",
|
|
14
|
+
"build:cli": "npm run build:cli:mac && npm run build:cli:linux && npm run build:cli:win",
|
|
15
|
+
"build:cli:mac": "bun build --compile --minify --bytecode --target=bun-darwin-arm64 ./src/cli.ts --outfile shifro-windows.exe",
|
|
16
|
+
"build:cli:linux": "bun build --compile --minify --bytecode --target=bun-linux-x64 ./src/cli.ts --outfile shifro-linux",
|
|
17
|
+
"build:cli:win": "bun build --compile --minify --bytecode --target=bun-windows-x64 ./src/cli.ts --outfile shifro-macos",
|
|
14
18
|
"typecheck": "tsc --noEmit",
|
|
15
19
|
"test": "vitest",
|
|
16
20
|
"prepublishOnly": "npm run build"
|
|
17
21
|
},
|
|
18
22
|
"bin": {
|
|
19
|
-
"shifro": "./dist/cli.
|
|
23
|
+
"shifro": "./dist/cli.mjs"
|
|
20
24
|
},
|
|
21
25
|
"type": "module",
|
|
22
|
-
"main": "./dist/
|
|
23
|
-
"types": "./dist/
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
24
28
|
"exports": {
|
|
25
29
|
".": {
|
|
26
30
|
"require": {
|
|
27
|
-
"types": "./dist/
|
|
28
|
-
"default": "./dist/
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
29
33
|
},
|
|
30
34
|
"import": {
|
|
31
|
-
"types": "./dist/
|
|
32
|
-
"default": "./dist/
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
40
|
"repository": {
|
|
37
41
|
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/
|
|
42
|
+
"url": "git+https://github.com/azot-labs/shifro.git"
|
|
39
43
|
},
|
|
40
44
|
"keywords": [
|
|
41
45
|
"mp4",
|
|
@@ -54,11 +58,11 @@
|
|
|
54
58
|
"shaka-packager"
|
|
55
59
|
],
|
|
56
60
|
"bugs": {
|
|
57
|
-
"url": "https://github.com/
|
|
61
|
+
"url": "https://github.com/azot-labs/shifro/issues",
|
|
58
62
|
"email": "vitalygashkov@vk.com"
|
|
59
63
|
},
|
|
60
64
|
"author": "Vitaly Gashkov <vitalygashkov@vk.com>",
|
|
61
|
-
"license": "
|
|
65
|
+
"license": "MIT",
|
|
62
66
|
"readmeFilename": "README.md",
|
|
63
67
|
"funding": [
|
|
64
68
|
{
|
|
@@ -67,15 +71,15 @@
|
|
|
67
71
|
}
|
|
68
72
|
],
|
|
69
73
|
"engines": {
|
|
70
|
-
"node": ">=
|
|
74
|
+
"node": ">=22"
|
|
75
|
+
},
|
|
76
|
+
"dependencies": {
|
|
77
|
+
"mediabunny": "^1.42.0-beta.5"
|
|
71
78
|
},
|
|
72
79
|
"devDependencies": {
|
|
73
80
|
"@types/node": "^24.5.2",
|
|
74
|
-
"tsdown": "^0.
|
|
81
|
+
"tsdown": "^0.21.10",
|
|
75
82
|
"typescript": "^5.9.2",
|
|
76
|
-
"vitest": "^
|
|
77
|
-
},
|
|
78
|
-
"dependencies": {
|
|
79
|
-
"mp4box": "^2.1.1"
|
|
83
|
+
"vitest": "^4.1.5"
|
|
80
84
|
}
|
|
81
85
|
}
|
package/dist/cli.cjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=e=>{let n={};for(var r in e)t(n,r,{get:e[r],enumerable:!0});return n},s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let l=require(`node:util`);l=c(l);let u=require(`node:fs`);u=c(u);let d=require(`mp4box`);d=c(d);let f=require(`node:stream`);f=c(f);const p=async({key:e,iv:t,data:n,algorithm:r=`AES-CTR`})=>{let i=r.toUpperCase(),a=await crypto.subtle.importKey(`raw`,e,{name:i},!1,[`decrypt`]),o=await crypto.subtle.decrypt({name:i,counter:t,length:64},a,n);return new Uint8Array(o)},m=e=>{let t=e.buffer;return new DataView(t,e.byteOffset,e.byteLength)},ee=(e,t=0,n)=>new Uint8Array(e.buffer,e.byteOffset+t,n);var h=class extends Error{constructor(e,t,n,r){super(r),this.severity=e,this.category=t,this.code=n,this.name=`DataViewReaderError`}},g=class{dataView;littleEndian;position=0;constructor(e,t=`BE`){this.dataView=m(e),this.littleEndian=t===`LE`}getDataView(){return this.dataView}hasMoreData(){return this.position<this.dataView.byteLength}getPosition(){return this.position}getLength(){return this.dataView.byteLength}readUint8(){this.checkBounds(1);let e=this.dataView.getUint8(this.position);return this.position+=1,e}readUint16(){this.checkBounds(2);let e=this.dataView.getUint16(this.position,this.littleEndian);return this.position+=2,e}readUint32(){this.checkBounds(4);let e=this.dataView.getUint32(this.position,this.littleEndian);return this.position+=4,e}readInt32(){this.checkBounds(4);let e=this.dataView.getInt32(this.position,this.littleEndian);return this.position+=4,e}readUint64(){let e,t;try{this.littleEndian?(e=this.dataView.getUint32(this.position,!0),t=this.dataView.getUint32(this.position+4,!0)):(t=this.dataView.getUint32(this.position,!1),e=this.dataView.getUint32(this.position+4,!1))}catch{throw this.outOfBounds()}if(t>2097151)throw new h(`CRITICAL`,`MEDIA`,`JS_INTEGER_OVERFLOW`,`64-bit integer overflow`);return this.position+=8,t*2**32+e}readBytes(e){this.checkBounds(e);let t=ee(this.dataView,this.position,e);return this.position+=e,t}skip(e){this.checkBounds(e),this.position+=e}rewind(e){if(this.position<e)throw this.outOfBounds();this.position-=e}seek(e){if(e<0||e>this.dataView.byteLength)throw this.outOfBounds();this.position=e}readTerminatedString(){let e=this.position;for(;this.hasMoreData()&&this.dataView.getUint8(this.position)!==0;)this.position++;if(!this.hasMoreData())throw this.outOfBounds();let t=new Uint8Array(this.dataView.buffer,e,this.position-e);return this.position++,new TextDecoder().decode(t)}checkBounds(e){if(this.position+e>this.dataView.byteLength)throw this.outOfBounds()}outOfBounds(){return new h(`CRITICAL`,`MEDIA`,`BUFFER_READ_OUT_OF_BOUNDS`,`Read operation out of bounds`)}};const _=(e,t)=>{if(e.length===0)return new Uint8Array;let n=t??e.reduce((e,t)=>e+t.length,0);n=Math.min(n,e.reduce((e,t)=>e+t.length,0));let r=new Uint8Array(n),i=0;for(let t of e){if(i>=n)break;let e=t.subarray(0,Math.min(t.length,n-i));r.set(e,i),i+=e.length}return r},v=e=>e.match(/.{1,2}/g)?.map(e=>parseInt(e,16))??[];function y(e,t){let n=e.getBox(`tenc`);n.default_isProtected,n.default_KID;let r=n.default_Per_Sample_IV_Size,i=e.moofs[t],a=e.mdats[t],o=i.trafs[0],s=o.truns[0],c=o.senc,l=new g(c.data),u=l.readUint32();c.samples=[];for(let e=0;e<u;e++){s.sample_size[e],s.sample_duration[e];let t={},n=new Uint8Array(16);if(n.set(l.readBytes(r)),t.InitializationVector=n,c.flags&2){t.subsamples=[];let e=l.readUint16();for(let n=0;n<e;n++){let e={};e.BytesOfClearData=l.readUint16(),e.BytesOfProtectedData=l.readUint32(),t.subsamples.push(e)}}c.samples.push(t)}return{moof:i,mdat:a,traf:o,trun:s,senc:c}}var te=class{input;inputReady;output;buffer=new Uint8Array;bytesRead=0;processedBytes=0;samplesProcessingQueue=[];constructor(e={}){this.options=e,this.input=(0,d.createFile)(!0),this.output=(0,d.createFile)(!1),this.inputReady=new Promise(e=>{this.input.onReady=e}),this.inputReady.then(e=>{let t=e.tracks[0],n=t.nb_samples;this.input.setExtractionOptions(t.id,void 0,{nbSamples:n});let r=this.processInit();this.buffer=_([this.buffer,r]),this.input.onSamples=(e,t,n)=>{this.samplesProcessingQueue.push(this.processSamples(n))},this.input.start()})}processInit(){let e=new d.DataStream,t=this.output.ftyp,n=this.output.moov,r=n.traks[0].mdia.minf.stbl.stsd,i=r.entries?.find(e=>!e.box_name),a=i.sinf.frma,o=new d.BoxParser.sampleEntry[a.data_format];for(let e of Object.keys(i))o.set(e,i[e]);return o.boxes=o.boxes?.filter(e=>e.box_name!==`ProtectionSchemeInfoBox`),r.addEntry(o),r.entries=r.entries?.filter(e=>!!e.box_name),n.psshs=[],n.boxes=this.output.moov.boxes?.filter(e=>e.box_name!==`ProtectionSystemSpecificHeaderBox`),t.write(e),n.write(e),new Uint8Array(e.buffer)}async processSamples(e){let t=e[0].track_id,n=this.input.moofs[e[0].moof_number-1],r=n.trafs[0];r.truns[0];for(let t of e){let{senc:e}=y(this.input,t.moof_number-1),n=e.samples[t.number_in_traf],r=[],i=0;for(let e of n.subsamples)r.push(t.data.subarray(i+e.BytesOfClearData,i+e.BytesOfClearData+e.BytesOfProtectedData)),i+=e.BytesOfClearData+e.BytesOfProtectedData;let a=_(r),o=await this.options.transformSample?.({data:a,iv:n.InitializationVector,timestamp:t.cts,encryptionScheme:`cenc`}),s=[],c=0,l=0;for(let e of n.subsamples)s.push(t.data.subarray(c,c+e.BytesOfClearData)),c+=e.BytesOfClearData+e.BytesOfProtectedData,s.push(o.subarray(l,l+e.BytesOfProtectedData)),l+=e.BytesOfProtectedData;let u=_(s);t.data=u,t.size=u.byteLength}r.boxes=r.boxes?.filter(e=>e.box_name!==`PiffSampleEncryptionBox`&&e.box_name!==`SampleEncryptionBox`&&e.box_name!==`SampleAuxiliaryInformationOffsetsBox`&&e.box_name!==`SampleAuxiliaryInformationSizesBox`);let i=new d.DataStream;n.write(i),n.trafs[0].truns[0].data_offset=n.size+8,i.adjustUint32(n.trafs[0].truns[0].data_offset_position,n.trafs[0].truns[0].data_offset);let a=new d.BoxParser.box.mdat,o=_(e.map(e=>e.data)),s=d.MP4BoxBuffer.fromArrayBuffer(o.buffer,0);a.stream=new d.MultiBufferStream(s),a.write(i);let c=new Uint8Array(i.buffer),l=Math.max(...e.map(e=>e.moof_number))+1;return this.input.releaseUsedSamples(t,l),c}async transform(e,t){try{let n=d.MP4BoxBuffer.fromArrayBuffer(e.buffer,this.bytesRead);if(this.input.appendBuffer(n),this.output.appendBuffer(n),this.bytesRead+=e.length,this.samplesProcessingQueue.length>0){let e=await Promise.all(this.samplesProcessingQueue);this.samplesProcessingQueue=[];let t=_(e);this.buffer=_([this.buffer,t])}this.buffer.length>0&&(t.enqueue(this.buffer),this.updateProgress(this.buffer.byteLength),this.buffer=new Uint8Array)}catch(e){t.error(e)}}async flush(e){this.buffer.length>0&&(e.enqueue(this.buffer),this.updateProgress(this.buffer.byteLength))}updateProgress(e){this.processedBytes+=e,this.options.onProgress&&this.options.onProgress(this.processedBytes)}},ne=class extends TransformStream{constructor(e={}){let t=new te(e);super({transform:(e,n)=>t.transform(e,n),flush:e=>t.flush(e)})}},b=o({fs:()=>u,stream:()=>f.default});const x=b===void 0?void 0:b;var S=class{readable;constructor(e){this.filePath=e;let t=x.fs.createReadStream(e);this.readable=x.stream.Readable.toWeb(t)}},C=class{writable;constructor(e){this.filePath=e;let t=x.fs.createWriteStream(e);this.writable=x.stream.Writable.toWeb(t)}},w=class{source;constructor(e){this.source=e.source}},T=class{target;constructor(e){this.target=e.target}},E=class e{decrypt;input;output;static async init(t){let n=new e(t);return await n.#init(),n}constructor(e){this.input=e.input,this.output=e.output;let t=new Uint8Array(v(e.keys[0].key));this.decrypt=new ne({transformSample:async e=>D(t,e),onProgress:e.onProgress})}async#init(){}async execute(){let e=this.input.source.readable,t=this.output.target.writable,n=this.decrypt;await e.pipeThrough(n).pipeTo(t)}};const D=async(e,t)=>{let n=t.encryptionScheme,r=t.iv,i=t.data;return p(n===`cbcs`?{key:e,iv:r,data:i,algorithm:`AES-CBC`}:{key:e,iv:r,data:i,algorithm:`AES-CTR`})},O=async(e,t,n)=>{console.time(`
|
|
3
|
-
Done!`);let r=new w({source:new S(e)}),i=new T({target:new C(t)});await(await E.init({input:r,output:i,keys:[{kid:n.keyId,key:`key`in n?n.key:``}],onProgress:e=>process.stdout.write(`\rDecrypting... [${e}]`)})).execute(),console.timeEnd(`
|
|
4
|
-
Done!`)};var k=`shifro`,A=`0.2.0`,j=`A lightweight library for decrypting MP4 files, directly in the browser`,M=[`dist`],N={start:`node ./dist/cli.cjs`,web:`vite`,build:`npm run build:lib && npm run build:cli`,"build:cli":`tsdown ./lib/node/cli.ts --format cjs --dts --minify --no-clean`,"build:lib":`tsdown shifro.ts --format cjs --format esm --dts --platform neutral`,typecheck:`tsc --noEmit`,test:`vitest`,prepublishOnly:`npm run build`},P={shifro:`./dist/cli.cjs`},F=`module`,I=`./dist/shifro.js`,re=`./dist/shifro.d.ts`,L={".":{require:{types:`./dist/shifro.d.cts`,default:`./dist/shifro.cjs`},import:{types:`./dist/shifro.d.ts`,default:`./dist/shifro.js`}}},R={type:`git`,url:`git+https://github.com/streamyx-labs/shifro.git`},z=[`mp4`,`mpeg`,`cenc`,`cbcs`,`decrypt`,`decryption`,`decrypter`,`decryptor`,`dash`,`drm`,`mp4decrypt`,`mp4box`,`shaka`,`shaka-packager`],B={url:`https://github.com/streamyx-labs/shifro/issues`,email:`vitalygashkov@vk.com`},V=`Vitaly Gashkov <vitalygashkov@vk.com>`,H=`AGPL-3.0`,U=`README.md`,W=[{type:`individual`,url:`https://t.me/tribute/app?startapp=dqW2`}],G={node:`>=20`},K={"@types/node":`^24.5.2`,tsdown:`^0.15.5`,typescript:`^5.9.2`,vitest:`^3.2.4`},q={mp4box:`^2.1.1`},J={name:k,version:A,description:j,files:M,scripts:N,bin:P,type:F,main:I,types:re,exports:L,repository:R,keywords:z,bugs:B,author:V,license:H,readmeFilename:U,funding:W,engines:G,devDependencies:K,dependencies:q};const Y=(0,l.parseArgs)({allowPositionals:!0,strict:!1,options:{key:{short:`k`,type:`string`,multiple:!0},help:{short:`h`,type:`boolean`}}}),[X,Z]=(Array.isArray(Y.values.key)?Y.values.key:[])[0]?.split(`:`)??[],[Q,$]=Y.positionals;(async()=>{Q&&$&&X&&Z?await O(Q,$,{key:Z,keyId:X}):console.log(`
|
|
5
|
-
${J.name} version ${J.version}
|
|
6
|
-
(c) 2024-2025 ${J.author}
|
|
7
|
-
|
|
8
|
-
Usage: ${J.name} [options] <input> <output>
|
|
9
|
-
|
|
10
|
-
Options:
|
|
11
|
-
--key <id>:<k>
|
|
12
|
-
<id> is either a track ID in decimal or a 128-bit KID in hex,
|
|
13
|
-
<k> is a 128-bit key in hex
|
|
14
|
-
(several --key options can be used, one for each track or KID)
|
|
15
|
-
`.trim())})();
|