| Type | Web application (steganography + symmetric encryption) |
|---|---|
| Container | MP4 (.mp4) |
| Backend | Python, Flask |
| Cryptography | AES-256-GCM; PBKDF2-HMAC-SHA256 (100,000 iterations) |
| Marker | MP4VAULT_MSG_v1 + NUL (16 bytes) |
| Live app | uelleu.pythonanywhere.com |
| Upload limit | 500 MB (server configuration) |
MP4Vault is a small web application that hides a secret text message inside an ordinary-looking MP4 video file. The message is encrypted before it is embedded, so discovering the extra bytes at the end of the file does not reveal the plaintext without the correct PIN. This page summarizes how the system works and links to the hosted application.
Overview
Steganography is concerned with concealing the existence of a message, often inside a benign carrier. MP4Vault’s carrier is a standard MP4 file: the video bytes are left unchanged, and a short binary trailer is appended after the video data. Typical players read only the portion of the file they need for playback and ignore the trailer, so the file still plays normally.
The trailer includes a fixed magic header (MP4VAULT_MSG_v1 plus a null byte), a length field,
and a payload produced by AES-256-GCM encryption keyed from a user-chosen six-character PIN.
Video overview
Watch on YouTube: youtube.com/watch?v=4a_dGQqOXrU
What is steganography?
Steganography is the practice of hiding secret information inside ordinary, innocent-looking data. Unlike encryption alone—which makes ciphertext obviously secret—steganography aims to avoid drawing attention to the fact that a hidden channel exists.
A classical analogy is writing on a wooden tablet, covering the message with wax so the surface looks blank, and handing the tablet to a courier. The courier sees an unremarkable object; only someone who knows to remove the wax can read the message. MP4Vault applies the same idea to bytes: the visible “surface” is a normal video; the hidden layer is past the end of the video stream, where general-purpose players do not look.
How the app works
An MP4 file on disk is a sequence of bytes. Decoders read boxes and sample data in order until the media is exhausted; trailing bytes are typically ignored. MP4Vault writes, in order:
- The original MP4 file bytes (unchanged).
- A 16-byte magic header:
MP4VAULT_MSG_v1followed by\x00. - A big-endian 32-bit unsigned integer: length of the encrypted payload.
- The encrypted payload bytes.
To recover a message, the implementation searches for the magic header from the end of the
file using rfind, then reads the length and payload and attempts decryption with the supplied PIN.
Encryption and security model
PIN and key derivation
The user supplies a six-character alphanumeric PIN (case-insensitive; the server normalizes
with .upper()). A 256-bit AES key is derived with
PBKDF2-HMAC-SHA256: 100,000 iterations, 32-byte output. The salt is the SHA-256 digest of
pin_bytes + b'mp4vault_salt_2024', so the same PIN always yields the same key (required for
decryption without storing per-file secrets on the server).
AES-256-GCM payload
The plaintext message is UTF-8 encoded and encrypted with AES-256-GCM. Each encryption uses a fresh 12-byte IV from a cryptographically secure random source. The GCM authentication tag is 16 bytes. The binary layout stored inside the trailer’s payload is:
[ IV 12 bytes ][ tag 16 bytes ][ ciphertext length BE uint32 4 bytes ][ ciphertext ]
Decryption verifies the tag; a wrong PIN or corrupted data leads to failure and an error response rather than garbled plaintext.
Operational security
The magic header string is fixed and documented. Anyone can scan an MP4 for that marker and learn that MP4Vault-style data may be present; they still cannot read the message without the PIN. Practical confidentiality depends on PIN strength and how files are shared.
Project structure
In the reference implementation, responsibilities are split between a Flask server module and a browser template:
- Server module (e.g.
app.py) — encryption, embedding, extraction, HTTP routes, temporary file handling, and validation. A snapshot of this logic appears in this repository aspythoncode.txt. - Application UI — an
index.htmltemplate served by Flask atGET /on the live host. That template is not checked into this repository snapshot; its behavior is summarized in Frontend and UI from the project tutorial. - Repository root
index.html— this static wiki page, intended for Netlify (or any static host), documents the app and links to the PythonAnywhere deployment.
Backend logic
The Flask application (see pythoncode.txt in this repo) exposes three routes:
GET /— renders the interactive UI template.POST /encode— multipart form with fieldsvideo(file),message(non-empty string), andpin(exactly six alphanumeric characters). Validates an.mp4extension, embeds the encrypted message, and returns the modified file as a download named<basename>_vaulted.mp4. Temporary input and output paths are deleted after the response is prepared (after_this_requestcleanup on success).POST /decode— multipart form withvideoandpin. Returns JSON{"message": "..."}on success or{"error": "..."}with an appropriate HTTP status on validation or decryption failure.
Uploaded files are written under the configured upload folder (the system temporary directory in the snapshot).
Filenames from clients are passed through secure_filename. Maximum request body size is set to
500 MB. The Flask secret_key is generated at process start for session-related features.
Core functions in the reference code: derive_key, encrypt_message,
decrypt_message, embed_message_in_mp4, and extract_message_from_mp4.
Frontend and UI
The production UI is a single-page template with Encode and Decode modes
(tab panels). JavaScript switches visible panels and uses the Fetch API with FormData
to POST the MP4 and fields to /encode or /decode. On encode, the
browser receives the modified MP4 as a blob and triggers a download; on decode, it parses JSON and displays the
recovered message or an error.
The tutorial describes a deliberately retro visual style (for example gray chrome and beveled borders reminiscent of classic Windows). That styling is cosmetic only and does not change the on-wire protocol.
How to use it
- Open the live application in a modern browser.
- Encode: choose an MP4, enter the message and a six-character alphanumeric PIN, then submit.
Download the
_vaulted.mp4file. Keep the PIN separate from the file if you want separation of duties. - Decode: upload a vaulted MP4 produced by MP4Vault, enter the same PIN, and read the decrypted message from the result.
The live app is hosted on PythonAnywhere at https://uelleu.pythonanywhere.com/.
Limitations and notes
- Known format: the marker
MP4VAULT_MSG_v1is public; detection of a vault is possible without the PIN. - PIN-derived salt: the PBKDF2 salt is derived from the PIN (not a random per-file salt stored alongside the ciphertext). That supports stateless decryption but is weaker than storing independent random salts for each file.
- PIN space: a six-character alphanumeric PIN is acceptable for casual use but is not comparable to a high-entropy passphrase or key file.
- Transcoding and chat apps: do not send vaulted MP4s through services that re-encode video (for example some social or messaging pipelines). Re-encoding typically strips appended bytes and destroys the trailer.
- Integrity: GCM detects tampering of the ciphertext; treat decode errors as wrong PIN, corrupt data, or a non-vault file.
Launch the app
The interactive MP4Vault encoder and decoder run on PythonAnywhere. Use the button below to open the application in a new tab.