Skip to content

Commit f79f78b

Browse files
committed
feat: add new packed format .azot for DRM client files
1 parent b4e0238 commit f79f78b

File tree

5 files changed

+93
-49
lines changed

5 files changed

+93
-49
lines changed

src/cli/commands/client/help.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ export const help = () => {
1010
);
1111
console.log(
1212
col(`pack <input> <output>`) +
13-
'pack client id and private key from <input> directory into single *.wvd file with <output> path',
13+
'pack client id and private key from <input> directory into single file with <output> path',
1414
);
1515
console.log(
1616
col(`unpack <input> <output>`) +
17-
'unpack *.wvd from <input> path to separate client id and private key placed in <output> directory',
17+
'unpack from <input> path to separate client id and private key placed in <output> directory',
1818
);
1919
console.log('');
2020
console.log(`Flags:`);
21+
console.log(
22+
col(`-f, --format`) + 'Specify format for pack command (wvd/azot)',
23+
);
2124
console.log(col(`-h, --help`) + 'Display this menu and exit');
2225
};

src/cli/commands/client/pack.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { writeFile } from 'node:fs/promises';
2-
import { join } from 'node:path';
2+
import { extname, join } from 'node:path';
33
import { importClient } from '../../utils';
44

5-
export const pack = async (input = process.cwd(), output?: string) => {
5+
export const pack = async (
6+
input = process.cwd(),
7+
format?: string,
8+
output?: string,
9+
) => {
610
const client = await importClient(input);
7-
const wvd = await client.pack();
8-
const wvdName = `${client.info.get('company_name')}_${client.info.get('model_name')}`;
9-
const wvdOutput = output || join(process.cwd(), `${wvdName}.wvd`);
10-
await writeFile(wvdOutput, wvd);
11-
console.log(`Client packed: ${wvdOutput}`);
11+
const ext = format || (output ? extname(output) : 'azot');
12+
const data = await client.pack(ext as 'wvd' | 'azot' | undefined);
13+
const filename = `${client.info.get('company_name')}-${client.info.get('model_name')?.toLowerCase()}`;
14+
const outputPath = output || join(process.cwd(), `${filename}.${ext}`);
15+
await writeFile(outputPath, data);
16+
console.log(`Client packed: ${outputPath}`);
1217
};

src/cli/main.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const args = parseArgs({
1717
help: { type: 'boolean', short: 'h' },
1818
version: { type: 'boolean', short: 'v' },
1919
debug: { type: 'boolean', short: 'd' },
20+
format: { type: 'boolean', short: 'f' },
2021

2122
host: { type: 'string' },
2223
port: { type: 'string' },
@@ -77,7 +78,7 @@ const help = () => {
7778
const [input, output] = positionals;
7879
switch (subcommand) {
7980
case 'pack':
80-
client.pack(input, output);
81+
client.pack(input, args.values.format as string | undefined, output);
8182
break;
8283
case 'unpack':
8384
client.unpack(input, output);

src/cli/utils.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readdir, readFile, stat } from 'node:fs/promises';
2-
import { join } from 'node:path';
2+
import { extname, join } from 'node:path';
33
import { Client } from '../lib';
44

55
export const importClient = async (input: string) => {
@@ -9,19 +9,22 @@ export const importClient = async (input: string) => {
99
const entries = isDir ? await readdir(input) : [];
1010
const idFilename = entries.find((entry) => entry.includes('client_id'));
1111
const keyFilename = entries.find((entry) => entry.includes('private_key'));
12-
const wvdFilename = entries.find((entry) => entry.endsWith('wvd'));
12+
const packedFilename = entries.find(
13+
(entry) => entry.endsWith('wvd') || entry.endsWith('azot'),
14+
);
1315
const isUnpacked = !!(idFilename && keyFilename);
14-
const isPacked = !!wvdFilename;
16+
const isPacked = !!packedFilename;
1517
if (isUnpacked) {
1618
const idPath = join(input, idFilename);
1719
const keyPath = join(input, keyFilename);
1820
const id = await readFile(idPath);
1921
const key = await readFile(keyPath);
2022
return Client.fromUnpacked(id, key);
2123
} else if (isPacked) {
22-
const wvdPath = join(input, wvdFilename);
23-
const wvd = await readFile(wvdPath);
24-
return await Client.fromPacked(wvd);
24+
const packedPath = join(input, packedFilename);
25+
const packed = await readFile(packedPath);
26+
const ext = extname(packedPath) as 'wvd' | 'azot';
27+
return await Client.fromPacked(packed, ext);
2528
} else {
2629
console.log(`Unable to find client files in ${input}`);
2730
process.exit(1);

src/lib/client.ts

+65-33
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,31 @@ export class Client {
4141

4242
#key?: { forDecrypt: CryptoKey; forSign: CryptoKey };
4343

44-
static async fromPacked(data: Uint8Array, format: 'WVDv2' = 'WVDv2') {
45-
if (format !== 'WVDv2') throw new Error('Only WVDv2 is supported');
46-
const parsed = parseWvd(data);
47-
const pcks1 = `-----BEGIN RSA PRIVATE KEY-----\n${fromBuffer(parsed.privateKey).toBase64()}\n-----END RSA PRIVATE KEY-----`;
48-
const key = fromText(pcks1).toBuffer();
49-
const type = types.get(parsed.deviceType);
50-
const securityLevel = parsed.securityLevel as SecurityLevel;
51-
const client = new Client(parsed.clientId, type, securityLevel);
52-
await client.importKey(key);
53-
return client;
44+
static async fromPacked(data: Uint8Array, format: 'wvd' | 'azot' = 'azot') {
45+
if (format === 'wvd') {
46+
const parsed = parseWvd(data);
47+
const pcks1 = `-----BEGIN RSA PRIVATE KEY-----\n${fromBuffer(parsed.privateKey).toBase64()}\n-----END RSA PRIVATE KEY-----`;
48+
const key = fromText(pcks1).toBuffer();
49+
const type = types.get(parsed.deviceType);
50+
const securityLevel = parsed.securityLevel as SecurityLevel;
51+
const client = new Client(parsed.clientId, type, securityLevel);
52+
await client.importKey(key);
53+
return client;
54+
} else if (format === 'azot') {
55+
const parsed = JSON.parse(fromBuffer(data).toText());
56+
const clientId = ClientIdentification.fromObject(
57+
parsed.data.clientIdentification,
58+
);
59+
const client = new Client(
60+
clientId,
61+
parsed.data.type,
62+
parsed.data.securityLevel,
63+
);
64+
await client.importKey(parsed.data.privateKey);
65+
return client;
66+
} else {
67+
throw new Error('Unsupported format');
68+
}
5469
}
5570

5671
static async fromUnpacked(id: Uint8Array, key: Uint8Array, vmp?: Uint8Array) {
@@ -69,11 +84,11 @@ export class Client {
6984
}
7085

7186
constructor(
72-
id: Uint8Array,
87+
id: Uint8Array | ClientIdentification,
7388
type: ClientType = CLIENT_TYPE.android,
7489
securityLevel: SecurityLevel = 3,
7590
) {
76-
this.id = ClientIdentification.decode(id);
91+
this.id = ArrayBuffer.isView(id) ? ClientIdentification.decode(id) : id;
7792
this.signedDrmCertificate = SignedDrmCertificate.decode(this.id.token);
7893
this.drmCertificate = DrmCertificate.decode(
7994
this.signedDrmCertificate.drmCertificate,
@@ -92,29 +107,46 @@ export class Client {
92107
return [id, key];
93108
}
94109

95-
async pack(format: 'WVDv2' = 'WVDv2') {
96-
if (format !== 'WVDv2') throw new Error('Only WVDv2 is supported');
97-
const id = ClientIdentification.encode(this.id).finish();
98-
const key = await this.exportKey();
99-
const keyDer = fromBuffer(key)
100-
.toText()
101-
.split('\n')
102-
.map((s) => s.trim())
103-
.slice(1, -1)
104-
.join('\n');
105-
const keyDerBinary = fromBase64(keyDer).toBuffer();
106-
const [type] = types.entries().find(([, type]) => type === this.type)!;
107-
const wvd = buildWvd({
108-
clientId: id,
109-
deviceType: type,
110-
securityLevel: this.securityLevel,
111-
privateKey: keyDerBinary,
112-
});
113-
return wvd;
110+
async pack(format: 'wvd' | 'azot' = 'azot') {
111+
if (format === 'wvd') {
112+
const id = ClientIdentification.encode(this.id).finish();
113+
const key = await this.exportKey();
114+
const keyDer = fromBuffer(key)
115+
.toText()
116+
.split('\n')
117+
.map((s) => s.trim())
118+
.slice(1, -1)
119+
.join('\n');
120+
const keyDerBinary = fromBase64(keyDer).toBuffer();
121+
const [type] = types.entries().find(([, type]) => type === this.type)!;
122+
const wvd = buildWvd({
123+
clientId: id,
124+
deviceType: type,
125+
securityLevel: this.securityLevel,
126+
privateKey: keyDerBinary,
127+
});
128+
return wvd;
129+
} else if (format === 'azot') {
130+
const clientIdentification = this.id.toJSON();
131+
const privateKey = await this.exportKey();
132+
const payload = {
133+
version: 1,
134+
type: 'widevine-client',
135+
data: {
136+
clientIdentification,
137+
privateKey: fromBuffer(privateKey).toText(),
138+
},
139+
};
140+
const data = JSON.stringify(payload, null, 2);
141+
return fromText(data).toBuffer();
142+
} else {
143+
throw new Error('Unsupported format');
144+
}
114145
}
115146

116-
async importKey(pkcs1: Uint8Array) {
117-
const pkcs1pem = fromBuffer(pkcs1).toText();
147+
async importKey(pkcs1: Uint8Array | string) {
148+
const pkcs1pem =
149+
typeof pkcs1 === 'string' ? pkcs1 : fromBuffer(pkcs1).toText();
118150
const pkcs8pem = toPKCS8(pkcs1pem);
119151
const pemContents = pkcs8pem.split('\n').slice(1, -2).join('\n');
120152
const data = fromBase64(pemContents).toBuffer();

0 commit comments

Comments
 (0)