Skip to main content
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

data
ArrayBuffer
required
Encrypted data to decrypt. Must be a valid AES-128-CBC ciphertext
keyData
ArrayBuffer
required
Raw AES-128 key (must be exactly 16 bytes / 128 bits)
iv
Uint8Array
required
Initialization vector (must be exactly 16 bytes). Used to XOR the first block of ciphertext

Returns

Promise<ArrayBuffer>
Promise resolving to the decrypted plaintext data

Behavior

  1. Key import: Imports the raw key data using crypto.subtle.importKey with algorithm "aes-cbc"
  2. Decryption: Decrypts the data using crypto.subtle.decrypt with the specified IV
  3. 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

Build docs developers (and LLMs) love