-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add cli, library, upgrade extension to manifest v3 and new design
- Loading branch information
1 parent
b46b674
commit ca6c9fd
Showing
79 changed files
with
31,118 additions
and
340 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,31 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
pnpm-debug.log* | ||
lerna-debug.log* | ||
|
||
node_modules | ||
.output | ||
stats.html | ||
stats-*.json | ||
.wxt | ||
web-ext.config.ts | ||
dist | ||
|
||
# Editor directories and files | ||
.vscode/* | ||
!.vscode/extensions.json | ||
.idea | ||
.DS_Store | ||
streamyx-extension.crx | ||
streamyx-extension.pem | ||
*.suo | ||
*.ntvs* | ||
*.njsproj | ||
*.sln | ||
*.sw? | ||
|
||
device_client_id_blob | ||
device_private_key | ||
*.wvd |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,48 @@ | ||
# Streamyx Chrome Extension | ||
# azot | ||
|
||
Intercept [DRM](https://www.widevine.com/) requests, view logs from [EME](https://w3c.github.io/encrypted-media/) session and copy ready commands for [Streamyx CLI](https://github.com/vitalygashkov/streamyx) | ||
Azot (Russian word for "nitrogen", pronounced `/azо́t/`) is a set of tools (JavaScript library, command-line utility and browser extension) for diagnosing, researching, and pentesting [Google's](https://about.google/) [Widevine](https://www.widevine.com/about) [DRM](https://www.urbandictionary.com/define.php?term=DRM). | ||
|
||
## Features | ||
|
||
- **Network-independent interception**: everything works even with one-time tokens and custom request body/response format. | ||
- **Custom client support**: WVD v2, device_client_id_blob + device_private_key, client_id.bin + private_key.pem | ||
- **Logging** details from EME events in Developer Tools console of current page | ||
- **Manifest V3** compliant browser extension | ||
- **Converting clients** between formats via CLI | ||
- **Encrypted Media Extensions API** compatibility via `requestMediaKeySystemAccess()` method | ||
- **Runtime agnostic** core: works in Node.js, Bun, Deno, browsers and more | ||
- **Minimal** dependencies | ||
|
||
## Installation | ||
|
||
1. Enable **Developer Mode** in [Chrome Extensions](chrome://extensions/) | ||
> JavaScript library and command-line tool installation requires pre-installed JavaScript runtime (e.g. Node.js). | ||
### JavaScript library | ||
|
||
```bash | ||
npm install azot | ||
``` | ||
|
||
### Command-line tool | ||
|
||
```bash | ||
npm install -g azot | ||
``` | ||
|
||
### Browser extension | ||
|
||
1. Enable **Developer Mode** in [Chrome Extensions](chrome://extensions/) page | ||
2. Click on **Load Unpacked** | ||
3. Select folder where you extracted zip downloaded from [Releases](https://github.com/vitalygashkov/streamyx-extension/releases) page | ||
3. Select folder where you extracted zip downloaded from [Releases](https://github.com/vitalygashkov/azot/releases) page | ||
4. Done! | ||
|
||
## Features | ||
## Usage | ||
|
||
### Example with Encrypted Media Extensions API | ||
|
||
Source code for example available [here](https://github.com/vitalygashkov/orlan/blob/main/examples/demo/eme.js). It's a minimal example of using `azot` to get a license for Bitmovin's Art of Motion Demo. This example similar to [example from EME](https://www.w3.org/TR/encrypted-media-2/#example-8). | ||
|
||
There are some differences from the native EME implementation: | ||
|
||
- **Notifications** about intercepted request with button to **copy Streamyx command** for your shell (run it using [Streamyx](https://github.com/vitalygashkov/streamyx) >= v4.0.0-beta.46 to extract content decryption keys) | ||
- [Encrypted Media Extensions (EME)](https://w3c.github.io/encrypted-media/) logging in Developer Tools console (**session ID, key ID, PSSH**, etc.) | ||
- Request **blocking** to catch license requests with disposable / one time tokens (you can add your own list of servers to block in `./src/worker.js`) | ||
1. You must import your Widevine client to obtain a license. | ||
2. In the `keyStatuses` field, Map's key is not just the key ID (like in EME), but pair with ID and value. For example, if key ID in HEX is `35e7eff366b24121a261832f3368146f` and content key itself is `f01471043a064e039a602346845b690f` then Map's key will be `35e7eff366b24121a261832f3368146f:f01471043a064e039a602346845b690f` (but as binary with `BufferSource` type) instead of just `35e7eff366b24121a261832f3368146f`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { col } from '../../utils'; | ||
|
||
export const help = () => { | ||
console.log(`azot client: Widevine client utilities\n`); | ||
console.log(`Usage: azot client <subcommand> [...flags]\n`); | ||
console.log(`Commands:`); | ||
console.log( | ||
col(`info <input>`) + | ||
'print info about client that is on <input> path (*.wvd filepath or directory with id and private key)', | ||
); | ||
console.log( | ||
col(`pack <input> <output>`) + | ||
'pack client id and private key from <input> directory into single *.wvd file with <output> path', | ||
); | ||
console.log( | ||
col(`unpack <input> <output>`) + | ||
'unpack *.wvd from <input> path to separate client id and private key placed in <output> directory', | ||
); | ||
console.log(''); | ||
console.log(`Flags:`); | ||
console.log(col(`-h, --help`) + 'Display this menu and exit'); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { help } from './help'; | ||
import { info } from './info'; | ||
import { pack } from './pack'; | ||
import { unpack } from './unpack'; | ||
|
||
export const client = { info, pack, unpack, help }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { importClient } from '../../utils'; | ||
|
||
export const info = async (input: string) => { | ||
const client = await importClient(input); | ||
for (const [key, value] of client.info.entries()) { | ||
console.log(`${key}: ${value}`); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { writeFile } from 'node:fs/promises'; | ||
import { join } from 'node:path'; | ||
import { importClient } from '../../utils'; | ||
|
||
export const pack = async (input = process.cwd(), output?: string) => { | ||
const client = await importClient(input); | ||
const wvd = await client.toWvd(); | ||
const wvdName = `${client.info.get('company_name')}_${client.info.get('model_name')}`; | ||
const wvdOutput = output || join(process.cwd(), `${wvdName}.wvd`); | ||
await writeFile(wvdOutput, wvd); | ||
console.log(`Client packed: ${wvdOutput}`); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { writeFile } from 'node:fs/promises'; | ||
import { join } from 'node:path'; | ||
import { importClient } from '../../utils'; | ||
|
||
export const unpack = async (input = process.cwd(), output?: string) => { | ||
const client = await importClient(input); | ||
const [id, key] = await client.toPair(); | ||
const idOutput = join(output || process.cwd(), `device_client_id_blob`); | ||
const keyOutput = join(output || process.cwd(), `device_private_key`); | ||
await writeFile(idOutput, id); | ||
await writeFile(keyOutput, key); | ||
console.log(`Client unpacked: ${idOutput}, ${keyOutput}`); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { col } from '../../utils'; | ||
|
||
export const help = () => { | ||
console.log(`azot license: Make license request\n`); | ||
console.log(`Usage: azot license <url> [...flags]\n`); | ||
console.log(`Commands:`); | ||
console.log( | ||
col(`<url>`) + | ||
'URL of license server (e.g. https://cwip-shaka-proxy.appspot.com/no_auth)', | ||
); | ||
console.log(''); | ||
console.log(`Flags:`); | ||
console.log(col(`-H, --header`) + 'headers to send with license request'); | ||
console.log(col(`-p, --pssh`) + 'widevine PSSH data in Base64'); | ||
console.log( | ||
col(`-c, --client`) + | ||
'path to client (directory with id and private key or path to *.wvd file)', | ||
); | ||
console.log( | ||
col(`-e, --encrypt`) + | ||
'enable client encryption with service certificate, disabled by default', | ||
); | ||
console.log(col(`-h, --help`) + 'display this menu and exit'); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './license'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { fetchDecryptionKeys } from '@azot/lib'; | ||
import { importClient } from '../../utils'; | ||
import { help } from './help'; | ||
|
||
type LicenseCommandParams = { | ||
url: string; | ||
pssh: string; | ||
clientPath?: string; | ||
encrypt?: boolean; | ||
headers?: string[]; | ||
}; | ||
|
||
export const license = async (params: LicenseCommandParams) => { | ||
const keys = await fetchDecryptionKeys({ | ||
server: params.url, | ||
pssh: params.pssh, | ||
client: await importClient(params.clientPath || process.cwd()), | ||
}); | ||
for (const key of keys) { | ||
console.log(`${key.id}:${key.value}`); | ||
} | ||
return keys; | ||
}; | ||
|
||
license.help = help; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
#!/usr/bin/env node | ||
|
||
import { parseArgs } from 'node:util'; | ||
import { client } from './commands/client'; | ||
import { license } from './commands/license'; | ||
import pkg from '../package.json' with { type: 'json' }; | ||
import { col } from './utils'; | ||
|
||
const args = parseArgs({ | ||
args: process.argv.slice(2), | ||
options: { | ||
pssh: { type: 'string', short: 'p' }, | ||
client: { type: 'string', short: 'c' }, | ||
encrypt: { type: 'boolean', short: 'e', default: false }, | ||
header: { type: 'string', short: 'H', multiple: true }, | ||
help: { type: 'boolean', short: 'h' }, | ||
version: { type: 'boolean', short: 'v' }, | ||
debug: { type: 'boolean', short: 'd' }, | ||
}, | ||
strict: false, | ||
allowPositionals: true, | ||
}); | ||
|
||
const help = () => { | ||
console.log( | ||
`Azot is a research & pentesting toolkit for Google's Widevine DRM. (${pkg.version})\n`, | ||
); | ||
console.log(`Usage: azot <command> [...flags]\n`); | ||
console.log(`Commands:`); | ||
console.log(col(`license <url>`) + 'Make a license request'); | ||
console.log( | ||
col(`client <subcommand>`) + 'Additional Widevine client utilities', | ||
); | ||
console.log(''); | ||
console.log(`Flags:`); | ||
console.log(col(`-d, --debug`) + 'Enable debug level logging'); | ||
console.log(col(`-v, --version`) + 'Print version and exit'); | ||
console.log(col(`-h, --help`) + 'Display this menu and exit'); | ||
console.log(''); | ||
console.log(`(more flags in azot license --help and azot client --help)`); | ||
}; | ||
|
||
(async () => { | ||
if (args.values.version) { | ||
console.log(pkg.version); | ||
process.exit(0); | ||
} | ||
|
||
const [command, ...positionals] = args.positionals; | ||
|
||
switch (command) { | ||
case 'client': { | ||
const subcommand = positionals.shift(); | ||
const [input, output] = positionals; | ||
switch (subcommand) { | ||
case 'pack': | ||
client.pack(input, output); | ||
break; | ||
case 'unpack': | ||
client.unpack(input, output); | ||
break; | ||
case 'info': | ||
client.info(input); | ||
break; | ||
default: { | ||
if (args.values.help) { | ||
client.help(); | ||
process.exit(0); | ||
} else { | ||
console.log('Client subcommand required: pack, unpack, info'); | ||
process.exit(1); | ||
} | ||
} | ||
} | ||
break; | ||
} | ||
case 'license': { | ||
if (args.values.help) { | ||
license.help(); | ||
process.exit(0); | ||
} | ||
const url = positionals.shift(); | ||
if (!url) throw new Error('License URL required'); | ||
const pssh = args.values.pssh as string; | ||
if (!pssh) throw new Error('PSSH required'); | ||
const clientPath = args.values.client as string; | ||
const encrypt = args.values.encrypt as boolean; | ||
const headers = args.values.header as string[]; | ||
await license({ url, pssh, clientPath, encrypt, headers }); | ||
break; | ||
} | ||
case 'pssh': | ||
break; | ||
case 'serve': | ||
break; | ||
case 'test': | ||
break; | ||
default: { | ||
if (args.values.help) { | ||
help(); | ||
process.exit(0); | ||
} else { | ||
console.log('Command not found'); | ||
process.exit(1); | ||
} | ||
} | ||
} | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { readdir, readFile, stat } from 'node:fs/promises'; | ||
import { join } from 'node:path'; | ||
import { Client } from '../lib'; | ||
|
||
export const importClient = async (input: string) => { | ||
const inputStat = await stat(input); | ||
const isDir = inputStat.isDirectory(); | ||
if (isDir) { | ||
const entries = isDir ? await readdir(input) : []; | ||
const idFilename = entries.find((entry) => entry.includes('client_id')); | ||
const keyFilename = entries.find((entry) => entry.includes('private_key')); | ||
const wvdFilename = entries.find((entry) => entry.endsWith('wvd')); | ||
const isUnpacked = !!(idFilename && keyFilename); | ||
const isPacked = !!wvdFilename; | ||
if (isUnpacked) { | ||
const idPath = join(input, idFilename); | ||
const keyPath = join(input, keyFilename); | ||
const id = await readFile(idPath); | ||
const key = await readFile(keyPath); | ||
return Client.fromPair(id, key); | ||
} else if (isPacked) { | ||
const wvdPath = join(input, wvdFilename); | ||
const wvd = await readFile(wvdPath); | ||
return await Client.fromWvd(wvd); | ||
} else { | ||
console.log(`Unable to find client files in ${input}`); | ||
process.exit(1); | ||
} | ||
} else if (input.endsWith('.wvd')) { | ||
const wvd = await readFile(input); | ||
return Client.fromWvd(wvd); | ||
} else { | ||
console.log(`Unable to find client files in ${input}`); | ||
process.exit(1); | ||
} | ||
}; | ||
|
||
export const col = (str: string, offset = 2, width = 30) => | ||
`${' '.repeat(offset)}${str.padEnd(width)}`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import globals from 'globals'; | ||
import pluginJs from '@eslint/js'; | ||
import tseslint from 'typescript-eslint'; | ||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; | ||
|
||
export default [ | ||
{ ignores: ['lib/proto/*', 'dist/*'] }, | ||
{ files: ['**/*.{js,mjs,cjs,ts}'] }, | ||
{ files: ['**/*.js'], languageOptions: { sourceType: 'commonjs' } }, | ||
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } }, | ||
pluginJs.configs.recommended, | ||
...tseslint.configs.recommended, | ||
eslintPluginPrettierRecommended, | ||
{ | ||
rules: { | ||
'no-empty': 'off', | ||
'@typescript-eslint/no-unused-vars': ['error', { caughtErrors: 'none' }], | ||
}, | ||
}, | ||
]; |
Oops, something went wrong.