agalwood/Motrix · error

Key must be 16 bytes, got

Error message

Key must be 16 bytes, got ${key.length} from ${uri}

What it means

Plain Error thrown inside SegmentDecryptor's default key fetcher when the bytes downloaded from the key URI are not exactly 16 bytes long. AES-128 keys are definitionally 128 bits = 16 bytes; any other length means the URI did not return a key (HTML error page, wrong content, truncated response). The message includes both the actual length and the offending URI.

Solutions

  1. Fetch the key URI directly with curl and inspect the byte length and content type.
  2. Confirm the EXT-X-KEY URI in the manifest resolves to a raw 16-byte octet stream.
  3. If the key endpoint returns base64/hex, supply a custom fetchKey to SegmentDecryptor that decodes before returning.
  4. Check response.status before reading the body — a 404 returning HTML is the usual culprit.

Example fix

// before
const decryptor = new SegmentDecryptor()
// after — custom fetcher that validates status and decodes base64 keys
const decryptor = new SegmentDecryptor(async (uri) => {
  const res = await fetch(uri)
  if (!res.ok) throw new Error(`key fetch failed: HTTP ${res.status}`)
  const raw = await res.text()
  const buf = Buffer.from(raw.trim(), 'base64')
  if (buf.length !== 16) throw new Error(`bad key length ${buf.length}`)
  return new Uint8Array(buf)
})
Defensive patterns

Strategy: try-catch

Validate before calling

async function fetchAes128Key(uri: string): Promise<Uint8Array> {
  const res = await fetch(uri)
  if (!res.ok) throw new Error(`key HTTP ${res.status}`)
  const buf = new Uint8Array(await res.arrayBuffer())
  if (buf.length !== 16) throw new Error(`key at ${uri} is ${buf.length} bytes, not 16`)
  return buf
}
const decryptor = new SegmentDecryptor(fetchAes128Key)

Try / catch

try {
  const key = await decryptor.getKey(keyUri)
} catch (e) {
  if (/Key must be 16 bytes.*from/.test(String(e))) {
    // re-fetch the key URI manually to inspect; likely a 404 HTML page
  } else throw e
}

Prevention

When it happens

Trigger: getKey(uri) or defaultFetchKey(uri) where the fetched resource is not a raw 16-byte AES key — e.g. the URI returns a 404 HTML page (200–5000 bytes), a JSON error object, a binary key of the wrong size (24/32 bytes for AES-192/256), or an empty response.

Common situations: Expired key URL returning an error page; CORS/proxy injecting HTML; key endpoint migrated to return base64-encoded keys (which are ~24 bytes); wrong key URI resolved from a malformed EXT-X-KEY line; TLS/cert page interception.

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/5fbb340ee76ae45c. Report an issue: GitHub.

Appendix: source

Thrown at src/core/media/segment-decryptor.ts:19

import { createDecipheriv } from 'node:crypto'

export class SegmentDecryptor {
  private keyCache: Map<string, Promise<Uint8Array>>

  private defaultFetchKey: (uri: string) => Promise<Uint8Array>

  constructor(fetchKey?: (uri: string) => Promise<Uint8Array>) {
    this.keyCache = new Map()

    if (fetchKey) {
      this.defaultFetchKey = fetchKey
    } else {
      this.defaultFetchKey = async (uri: string) => {
        const response = await fetch(uri)
        const buffer = await response.arrayBuffer()
        const key = new Uint8Array(buffer)
        if (key.length !== 16) {
          throw new Error(`Key must be 16 bytes, got ${key.length} from ${uri}`)
        }
        return key
      }
    }
  }

  decrypt(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8Array): Uint8Array {
    // Validate key and IV lengths
    if (key.length !== 16) {
      throw new Error(`Key must be 16 bytes, got ${key.length}`)
    }
    if (iv.length !== 16) {
      throw new Error(`IV must be 16 bytes, got ${iv.length}`)
    }

    // Convert Uint8Array to Buffer for crypto operations
    const keyBuffer = Buffer.from(key)
    const ivBuffer = Buffer.from(iv)

View on GitHub (pinned to 1a708ee577)