March 25, 2026·Engineering Deep Dive·5 min read

Building a Piracy-Resistant Video LMS: Architecture Behind AES-128 HLS Delivery

By Amber Bisht, Full-Stack & Systems Engineer

0:00 / 4:54

Most learning platforms eventually run into the same problem: paid video content is trivial to rip once it reaches a browser, unless the delivery pipeline is designed with that assumption from day one. When I built my own LMS, the goal wasn't "unbreakable" DRM — nothing running in a browser truly is — it was to raise the cost of piracy high enough that casual downloading and account sharing stop being worth the effort, while keeping playback fast and infrastructure costs sane. Here's how the system is put together, layer by layer.

The pipeline, end to end

An instructor uploads a raw MP4 to a private S3 bucket. That upload triggers an S3 event, which kicks off AWS Elemental MediaConvert to transcode the video and package it as HLS — a manifest file plus a sequence of short .ts segments rather than one large file. This is where AWS MediaConvert does the heavy lifting: producing multiple bitrate renditions for adaptive streaming, encrypting each segment with AES-128, and writing the output to a second, separate S3 bucket dedicated to processed content.

Splitting raw and processed storage into two buckets isn't cosmetic. The raw bucket holds original uploads and is never exposed to the public internet — only the transcoding pipeline can read from it. The processed bucket holds encrypted, streaming-ready segments and is the only one CloudFront is allowed to serve from. If credentials for the CDN-facing bucket ever leaked, there'd be nothing in it but already-encrypted, time-limited content — the source files stay isolated.

Why the CDN and signed URLs matter as much as encryption

Encryption without access control just means someone has to work slightly harder before they can download the plaintext key along with the ciphertext. So the delivery layer does two things at once: CloudFront sits in front of the S3 processed bucket as the CDN, caching segments at the edge for low-latency playback, and every request to it has to carry a signed URL.

Signed URLs (or signed cookies, depending on how playback is scoped) are generated server-side, per session, with a short expiry window — typically minutes, not hours. The signature is tied to the resource path and an expiry timestamp, so a URL copied out of the network tab and shared elsewhere stops working once it expires, and can't be replayed against a different segment. Pairing this with origin access control on the bucket means the S3 objects themselves are unreachable directly — CloudFront is the only path in, and CloudFront won't serve anything without a valid signature.

Where AES-128 actually fits

The HLS segments are encrypted with AES-128 in CBC mode, which is the standard, well-understood approach HLS itself was designed around — this isn't custom cryptography, it's the boring, correct primitive. Each .tschunk is decrypted client-side as it's fetched, using a key delivered separately from the manifest.

The part that actually matters for security isn't the AES-128 step itself — that's table stakes — it's how the key reaches the client. A key handed over as a static, unauthenticated fetch is only marginally better than no encryption at all, since anyone can just replay that same request. So the key exchange is bound to the session: a short-lived, per-session secret (in my case negotiated via an ECDH handshake) is used so the actual AES key is never transmitted as a bare value that could be captured once and reused indefinitely. Decryption itself happens off the main thread in a Web Worker, partly for playback performance and partly so the raw key material isn't sitting in globally accessible browser state.

Access control: RBAC and session accountability

Underneath all of this sits role-based access control. Students, instructors, and admins get distinctly scoped permissions at the API layer — an instructor can push content into the pipeline, but nothing in the student-facing service can mint upload credentials or reach the raw bucket. This is enforced server-side on every request, not inferred from what the frontend chooses to render.

Session accountability is the other half of deterrence. Every playback session logs IP and user-agent, so unusual patterns — the same account streaming from many locations, or fetching segments far faster than real playback would require — get flagged automatically rather than relying on someone noticing manually. None of this stops a determined, technically capable person from extracting a video once. It's designed to make casual ripping and credential sharing costly and traceable enough that most people don't bother, which — for a browser-delivered video product — is a realistic bar, not a fantasy of an unbreakable client.

What this deliberately leaves out

A write-up like this is useful to other engineers precisely because it stays at the architecture level: buckets, signing, key exchange patterns, RBAC boundaries. It's not useful to anyone trying to defeat a specific deployment, because it doesn't include actual key values, endpoint paths, internal function names, or the exact byte-level encoding of any handshake — those specifics are what turn an architecture explainer into an extraction script, and they're also the parts that should differ across environments and rotate over time anyway.

If you're documenting your own system for a portfolio or blog, that's the line worth holding: explain the shape of the defense, not the literal values that make a specific instance breakable.