CBCS encryption accepts an IV that readers do not receive

Open
#710 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@perfloop-agent is already working on this.

Since Nov 13, 2026.

  • #720 by @perfloop-agent — open

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
52/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
go
Domain
cryptography

Research direction

The constructor at mp4/crypto.go:1022–1024 copies the supplied IV without binding it to tenc; read from there into the CBCS write path (1121–1139) and the decrypt-side default (1481–1483). Run the reproduction in the issue body, which builds examples/stream-encrypt and cmd/mp4ff-encrypt, and note the per-fragment IV derivation in examples/stream-encrypt/encryptor.go:57–62. Done means the script reports 4/4 and 60/60 or an explicit encryption error, plus a regression test for a mismatched IV and CBCS init data without a constant IV.

Written by the indexing model from the issue text.

Description

CBCS encryption can succeed with an initialization vector (IV) different from the constant IV in the init segment's tenc box. Readers use the init's IV and recover different media bytes. This affects callers that protect an init once, then supply a different IV through mp4.EncryptFragment, mp4.EncryptFragments, or InitProtectData.NewFragmentEncryptor / FragmentEncryptor.EncryptFragment, including mp4ff-encrypt -init.

Reproduced at d98f0d6 with committed files, Go 1.27.4 and FFmpeg 7.1.5:

Reproduction Observed Expected
examples/stream-encrypt, -scheme cbcs, mp4/testdata/v300_multiple_segments.mp4 0/4 decrypted media payloads equal the originals 4/4, or an explicit encryption error
Same example, -scheme cenc 4/4 equal 4/4
CLI, differing IV, mp4/testdata/init.mp4 + mp4/testdata/1.m4s Both CLIs exit 0; FFmpeg frames equal: 0/60 60/60, or an explicit encryption error
Same CLI input, matching IV FFmpeg frames equal: 60/60 60/60

The likely cause is in mp4/crypto.go:1022–1024: the constructor copies the supplied IV without binding it to tenc. CBCS uses that IV but writes no per-sample IV (mp4/crypto.go:1121–1139); decryption starts from tenc.DefaultConstantIV (mp4/crypto.go:1481–1483). The streaming example derives a new IV before each fragment (examples/stream-encrypt/encryptor.go:57–62). The constructor also lacks a binding check for CBCS init data without a constant IV.

Production prevalence is unknown.

Reproduction from the repository root (Bash, Python 3 and FFmpeg)
set -euo pipefail
mkdir -p /tmp/cbcs-iv-repro
out=/tmp/cbcs-iv-repro
key=00112233445566778899aabbccddeeff
iv=a0a1a2a3a4a5a6a7a8a9aaabacadaeaf
bad=a0a1a2a3a4a5a6a7a8a9aaabacadaeb0
go build -o "$out/stream" ./examples/stream-encrypt
go build -o "$out/encrypt" ./cmd/mp4ff-encrypt
go build -o "$out/decrypt" ./cmd/mp4ff-decrypt
for scheme in cbcs cenc; do
  "$out/stream" -port 18080 -input mp4/testdata/v300_multiple_segments.mp4 \
    -scheme "$scheme" -key "$key" -keyid "$key" -iv "$iv" > "$out/server-$scheme.log" 2>&1 &
  server=$!
  trap 'kill "$server" 2>/dev/null || true' EXIT
  for attempt in {1..100}; do
    if curl -fsS http://localhost:18080/enc.mp4 -o "$out/$scheme.mp4" 2>/dev/null; then break; fi
    sleep .05
  done
  kill "$server"; wait "$server" || true
  trap - EXIT
  "$out/decrypt" -key "$key" "$out/$scheme.mp4" "$out/$scheme-clear.mp4"
done
python3 - <<'PY'
from pathlib import Path
import struct
def mdats(path):
    data, pos, result = Path(path).read_bytes(), 0, []
    while pos < len(data):
        size, kind = struct.unpack_from('>I4s', data, pos)
        assert size >= 8 and pos+size <= len(data)
        if kind == b'mdat': result.append(data[pos+8:pos+size])
        pos += size
    return result
original = mdats('mp4/testdata/v300_multiple_segments.mp4')
for scheme in ('cbcs', 'cenc'):
    got = mdats('/tmp/cbcs-iv-repro/'+scheme+'-clear.mp4')
    print(scheme, 'recovered payloads:', sum(a == b for a,b in zip(original,got)),
          '/', len(original), 'output payloads:', len(got))
PY
"$out/encrypt" -scheme cbcs -key "$key" -kid "$key" -iv "$iv" mp4/testdata/init.mp4 "$out/init.mp4"
cat mp4/testdata/init.mp4 mp4/testdata/1.m4s > "$out/clear.mp4"
ffmpeg -nostdin -v error -y -i "$out/clear.mp4" -f framemd5 "$out/clear.md5"
for mode in matching differing; do
  supplied=$iv
  if [ "$mode" = differing ]; then supplied=$bad; fi
  "$out/encrypt" -key "$key" -iv "$supplied" -init "$out/init.mp4" mp4/testdata/1.m4s "$out/$mode.m4s"
  echo "$mode encrypt exit: $?"
  "$out/decrypt" -key "$key" -init "$out/init.mp4" "$out/$mode.m4s" "$out/$mode-clear.m4s"
  echo "$mode decrypt exit: $?"
  cat "$out/init.mp4" "$out/$mode.m4s" > "$out/$mode.mp4"
  ffmpeg -nostdin -v error -y -decryption_key "$key" -i "$out/$mode.mp4" -f framemd5 "$out/$mode.md5"
done
python3 - <<'PY'
from pathlib import Path
def frames(name):
    return [line.split(',')[-1].strip() for line in
            Path('/tmp/cbcs-iv-repro/'+name+'.md5').read_text().splitlines()
            if line.strip() and not line.startswith('#')]
original = frames('clear')
for name in ('matching', 'differing'):
    got = frames(name)
    print(name, 'equal frames:', sum(a == b for a,b in zip(original,got)),
          '/', len(original), 'decoded frames:', len(got))
PY

How should CBCS IVs be handled in these APIs and the streaming example, including init segments without a constant IV? Would you like a pull request with a fix?

Case and evidence


Made by Perfloop. Human sponsor: Tomás Senart.

Dominant language
Go
Stars
667
Forks
132
Avg merge
20h 4m
Merged PRs (30d)
54

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Eyevinn/mp4ff

All issues in Eyevinn/mp4ff

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.