FILE // SECURITY // DECLASSIFIED

SECURITY

The Enigma rotors on screen are decoration. The real work is done by modern, standard cryptography built into your browser. No system is perfectly secure; this page explains what ENIGMA does and where its limits are.

1. How it works

  1. Your browser generates a random 256-bit key, a 128-bit salt and a 96-bit IV with crypto.getRandomValues().
  2. The message and its metadata are packed into a small JSON envelope, padded to a multiple of 256 bytes (to hide the exact length) and encrypted with AES-256-GCM through the Web Crypto API (window.crypto.subtle).
  3. Only the ciphertext, IV, salt, KDF parameters and two SHA-256 token hashes are sent to the server. The server generates the Dead Drop ID with PHP's random_bytes() and stores the record as a JSON file.
  4. The browser builds the secure link: /m/ID#key=ENIGMA1-…. The key sits in the URL fragment, which browsers never send to the server.
  5. The recipient's browser reads the key from the fragment, removes it from the address bar, derives an access proof, fetches the ciphertext and decrypts it locally. AES-GCM verifies an authentication tag: if the key is wrong or a single byte was changed, decryption fails and no text is shown.
linkKey    = 32 random bytes                      (only in the link)
ikm        = linkKey                              (no passphrase)
           = linkKey ‖ PBKDF2-SHA256(pass, salt, 600 000)   (with passphrase)
encKey     = HKDF-SHA256(ikm, salt, "ENIGMA2026/v1/enc")   → AES-256-GCM
authToken  = HKDF-SHA256(ikm, salt, "ENIGMA2026/v1/auth")  → server stores SHA-256(authToken)
deleteTok  = 32 random bytes                      → server stores SHA-256(deleteTok)

2. Cryptography

ComponentChoice
CipherAES-256-GCM, 96-bit random IV, 128-bit tag, protocol parameters as additional authenticated data
Randomnesscrypto.getRandomValues() in the browser, random_bytes() in PHP
Key derivationHKDF-SHA256 with separate labels for the encryption key and the access token
PassphrasePBKDF2-SHA256, 600 000 iterations, random 128-bit salt — never used directly as a key, never sent
Dead Drop ID28 characters Crockford Base32 = 140 bits of entropy
ImplementationNative Web Crypto only. No home-made algorithms. The rotor animation has no cryptographic function.

3. URL fragment key handling

Everything after # stays in the browser. The page reads the key into a JavaScript variable, then calls history.replaceState() to remove it from the address bar. The key is never written to cookies, localStorage, sessionStorage or a server session. All pages send Referrer-Policy: no-referrer.

Split transmission lets you send the Dead Drop ID and the key through two different channels. Passphrase mode adds a second factor: the link alone is no longer enough, and because the 256-bit link key never reaches the server, a stolen server file cannot be brute-forced offline against the passphrase.

4. Access proof — why a typo never burns a message

To fetch the ciphertext, the browser must present authToken, which can only be derived from the correct key (and passphrase). The server compares its SHA-256 hash in constant time. Consequences: a person who only knows the Dead Drop ID cannot fetch or burn the transmission, and a wrong key or passphrase is rejected before a read is counted. The token is derived one-way and cannot be turned back into the encryption key.

5. Server-side storage

No database. Each transmission is one JSON file in enigma_private/data/messages/. The whole private folder — configuration, PHP core, encrypted messages, rate-limit counters and logs — lives outside the web root (public_html), so the web server has no URL that points to it. Extra .htaccess deny rules and empty index files remain as a second line of defence.

Stored per transmission: id, ciphertext, iv, salt, kdf, auth_hash, delete_hash, created, expires, max_views, views. Never stored: plaintext, key, passphrase, tokens.

6. File locking & burn after reading

New files are written to a temporary file and atomically renamed into place. Every read takes an exclusive flock(). After acquiring the lock, the server verifies the file was not deleted or replaced while it waited. For read-once transmissions, the file is deleted inside the same locked section that hands out the ciphertext, so two simultaneous requests cannot both receive it.

Limitation: decryption happens in the recipient's browser, so the server cannot know with certainty that the message was actually displayed. The access proof makes it very likely the requester holds the right key; if the connection drops at exactly that moment, a read-once message can be lost. We chose "destroy on delivery" because a "destroy on confirmation" step could be skipped by a malicious client.

Read-once links do not auto-open: the recipient presses DECRYPT & BURN. This prevents link-preview bots and e-mail scanners from burning a message nobody has seen.

7. Expiry & cleanup

Every transmission has a server-computed expiry (10 minutes to 7 days). Expired files are refused on access and deleted, by a cron job (scripts/cleanup.php) and/or automatically on 1 % of API requests, using the same locks as reads.

8. Web hardening

9. Known limits

  • JavaScript is delivered by the server. If the server itself were compromised, it could ship malicious code. This is a general limit of web-based encryption.
  • Anyone who obtains the full secure link can read the message (unless a passphrase is used).
  • The recipient can copy, photograph or forward the text. Nothing can undo that.
  • Traffic metadata (IP addresses, timing, approximate size) is visible to network and hosting providers.
  • Security depends on the devices, browsers and extensions of both parties.
  • The passphrase strength meter is an estimate, not a guarantee.

10. Source

ENIGMA is plain PHP, HTML, CSS and vanilla JavaScript — no frameworks, no database, no external libraries (the QR generator is a small local file). All browser code is readable in assets/js/: crypto.js contains the complete cryptography.