The Crypto decryptor service provides AES-128 decryption for encrypted HLS fragments using the browser’s native Web Crypto API.
Overview
This service handles:
- Importing raw AES-128 encryption keys
- Decrypting HLS fragments encrypted with AES-128-CBC
- Using proper initialization vectors (IVs) for CBC mode
- Leveraging hardware-accelerated cryptography via Web Crypto API
API
The CryptoDecryptor object exports a single method:
export const CryptoDecryptor = {
decrypt,
};
Methods
decrypt
Decrypts AES-128 encrypted data using CBC mode.
async function decrypt(
data: ArrayBuffer,
keyData: ArrayBuffer,
iv: Uint8Array
): Promise<ArrayBuffer>
Parameters
Encrypted data to decrypt. Must be a valid AES-128-CBC ciphertext
Raw AES-128 key (must be exactly 16 bytes / 128 bits)
Initialization vector (must be exactly 16 bytes). Used to XOR the first block of ciphertext
Returns
Promise resolving to the decrypted plaintext data
Behavior
- Key import: Imports the raw key data using
crypto.subtle.importKey with algorithm "aes-cbc"
- Decryption: Decrypts the data using
crypto.subtle.decrypt with the specified IV
- Returns plaintext: Returns the decrypted data as an ArrayBuffer
Example
import { CryptoDecryptor } from "./crypto-decryptor";
// Fetch the encryption key
const keyResponse = await fetch("https://example.com/key.bin");
const keyData = await keyResponse.arrayBuffer();
// Parse the IV from the playlist (e.g., "0x12345678901234567890123456789012")
const ivHex = "12345678901234567890123456789012";
const iv = new Uint8Array(
ivHex.match(/.{2}/g).map((byte) => parseInt(byte, 16))
);
// Fetch and decrypt the fragment
const fragmentResponse = await fetch("https://example.com/segment0.ts");
const encryptedData = await fragmentResponse.arrayBuffer();
const decryptedData = await CryptoDecryptor.decrypt(
encryptedData,
keyData,
iv
);
console.log("Decrypted:", decryptedData.byteLength, "bytes");
AES-128-CBC decryption
Algorithm overview
AES-128-CBC (Cipher Block Chaining) is the standard encryption method for HLS:
- Key size: 128 bits (16 bytes)
- Block size: 128 bits (16 bytes)
- Mode: CBC (Cipher Block Chaining)
- Padding: PKCS#7 padding is automatically handled by Web Crypto API
Initialization vector (IV)
The IV is used to randomize the first block of encryption:
- Size: Must be exactly 16 bytes (128 bits)
- Source: Specified in the
#EXT-X-KEY tag’s IV attribute, or derived from the media sequence number
- Format: Hex string starting with
0x (e.g., 0x12345678901234567890123456789012)
If the playlist does not specify an IV, the HLS specification requires using the media sequence number as the IV (padded to 16 bytes).
Key retrieval
The encryption key is specified in the #EXT-X-KEY tag:
#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key.bin",IV=0x...
- Key format: Raw binary (16 bytes)
- Key location: Fetched from the URI specified in the
#EXT-X-KEY tag
- Key reuse: A single key may be used for multiple segments
Usage patterns
Basic decryption workflow
import { FetchLoader } from "./fetch-loader";
import { CryptoDecryptor } from "./crypto-decryptor";
import { M3u8Parser } from "./m3u8-parser";
// 1. Parse the playlist
const playlist = await FetchLoader.fetchText(
"https://example.com/stream.m3u8"
);
const fragments = M3u8Parser.parseLevelPlaylist(
playlist,
"https://example.com/stream.m3u8"
);
// 2. Download the encryption key
const keyUri = fragments[0].key.uri;
if (!keyUri) {
throw new Error("Fragment is not encrypted");
}
const keyData = await FetchLoader.fetchArrayBuffer(keyUri);
// 3. Parse the IV
const ivHex = fragments[0].key.iv?.replace("0x", "");
if (!ivHex) {
throw new Error("IV not specified");
}
const iv = new Uint8Array(
ivHex.match(/.{2}/g)!.map((byte) => parseInt(byte, 16))
);
// 4. Download and decrypt the fragment
const encryptedData = await FetchLoader.fetchArrayBuffer(fragments[0].uri);
const decryptedData = await CryptoDecryptor.decrypt(
encryptedData,
keyData,
iv
);
console.log("Decrypted fragment:", decryptedData.byteLength, "bytes");
Batch decryption with key caching
import { FetchLoader } from "./fetch-loader";
import { CryptoDecryptor } from "./crypto-decryptor";
// Cache keys to avoid redundant downloads
const keyCache = new Map<string, ArrayBuffer>();
async function decryptFragment(
fragmentData: ArrayBuffer,
keyUri: string,
ivHex: string
): Promise<ArrayBuffer> {
// Fetch key if not cached
if (!keyCache.has(keyUri)) {
const keyData = await FetchLoader.fetchArrayBuffer(keyUri, 3);
keyCache.set(keyUri, keyData);
}
const keyData = keyCache.get(keyUri)!;
// Parse IV
const iv = new Uint8Array(
ivHex.replace("0x", "").match(/.{2}/g)!.map((byte) => parseInt(byte, 16))
);
// Decrypt
return CryptoDecryptor.decrypt(fragmentData, keyData, iv);
}
// Usage
const decryptedFragment = await decryptFragment(
encryptedData,
"https://example.com/key.bin",
"0x12345678901234567890123456789012"
);
Handling unencrypted fragments
import { CryptoDecryptor } from "./crypto-decryptor";
async function processFragment(
fragmentData: ArrayBuffer,
keyUri: string | null,
ivHex: string | null
): Promise<ArrayBuffer> {
// Skip decryption if fragment is not encrypted
if (!keyUri || !ivHex) {
return fragmentData;
}
// Decrypt if encrypted
const keyData = await fetch(keyUri).then((r) => r.arrayBuffer());
const iv = new Uint8Array(
ivHex.replace("0x", "").match(/.{2}/g)!.map((byte) => parseInt(byte, 16))
);
return CryptoDecryptor.decrypt(fragmentData, keyData, iv);
}
Error handling
The Web Crypto API may throw errors in several scenarios:
Invalid key size
try {
await CryptoDecryptor.decrypt(data, invalidKey, iv);
} catch (error) {
console.error("Invalid key size (must be 16 bytes)");
}
Invalid IV size
try {
await CryptoDecryptor.decrypt(data, key, invalidIV);
} catch (error) {
console.error("Invalid IV size (must be 16 bytes)");
}
Corrupted ciphertext
try {
await CryptoDecryptor.decrypt(corruptedData, key, iv);
} catch (error) {
console.error("Decryption failed (corrupted ciphertext)");
}
The key must be exactly 16 bytes (128 bits) for AES-128. Keys of other sizes (e.g., AES-192 or AES-256) will cause the decryption to fail.
The IV must be exactly 16 bytes (128 bits). Shorter or longer IVs will cause the decryption to fail.
Web Crypto API integration
The service uses the following Web Crypto API methods:
crypto.subtle.importKey
const rawKey = await crypto.subtle.importKey(
"raw", // Key format (raw binary)
keyData, // ArrayBuffer containing the key
"aes-cbc", // Algorithm
false, // Not extractable
["decrypt"] // Key usage
);
crypto.subtle.decrypt
const decryptedData = await crypto.subtle.decrypt(
{
name: "aes-cbc", // Algorithm
iv: iv, // Initialization vector
},
rawKey, // CryptoKey object
data // ArrayBuffer to decrypt
);
The Web Crypto API is hardware-accelerated on most modern browsers, providing excellent performance for decryption operations.
HLS encryption specification
For more information on HLS encryption, refer to:
- RFC 8216: HTTP Live Streaming specification
- Section 5.2: Encryption and decryption of media segments
- AES-128-CBC: Standard encryption method for HLS
Source location
~/workspace/source/src/background/src/services/crypto-decryptor.ts:1