[.ENC FILE FORMAT SPECIFICATION]
The exact byte layout of the ENCF container produced by CryptFile.Online — version 2.0.
This page documents the on-disk format written by this tool, so that anyone can write an independent reader or verify what the container holds. The format is self-describing: every parameter needed to decrypt a file, apart from the key, is stored in its header.
The reference implementation lives in src/encryption.js and src/decryption.js. Where this page and the code disagree, the code is authoritative — please report the discrepancy.
[OVERALL STRUCTURE]
┌────────────────────────────┐ │ Header 256 bytes │ fixed size, unencrypted ├────────────────────────────┤ │ Ciphertext variable │ encrypted in 2 MiB chunks ├────────────────────────────┤ │ Auth tag 32 bytes │ HMAC-SHA256 over the ciphertext └────────────────────────────┘
The header is deliberately left unencrypted so that a reader can determine the algorithm and key-derivation method before it has a key. It contains no secret material: the salt and IV are not secrets, and the original filename is the only potentially sensitive field.
[HEADER LAYOUT (256 BYTES)]
| Offset | Length | Field | Value |
|---|---|---|---|
| 0 | 4 | Magic | 45 4E 43 46 — ASCII "ENCF" |
| 4 | 1 | Version major | 0x02 (0x01 = legacy, still readable) |
| 5 | 1 | Version minor | 0x00 |
| 6 | 1 | Algorithm | 0x01 = AES-256-GCM · 0x02 = ChaCha20-Poly1305 |
| 7 | 1 | Key derivation | 0x01 = PBKDF2 · 0x02 = Argon2 (reserved) · 0x03 = random · 0x04 = user-supplied |
| 8 | 32 | Salt | PBKDF2 salt; zero-filled for other methods |
| 40 | 12 | IV / nonce | 96-bit random value |
| 52 | 2 | Filename length | uint16, little-endian, ≤ 200 |
| 54 | 200 | Original filename | UTF-8, zero-padded, truncated at 200 bytes |
| 254 | 1 | Compression flag | 0x01 = payload was DEFLATE-compressed before encryption |
| 255 | 1 | Reserved | 0x00 |
[PAYLOAD AND AUTHENTICATION]
The plaintext is read in 2 MiB (2,097,152-byte) chunks and each chunk is encrypted independently as an AEAD ciphertext, which keeps memory use flat regardless of file size. Every chunk therefore carries its own 16-byte authentication tag, so a chunk on disk occupies 2 MiB + 16 bytes. If the compression flag is set, the whole file is DEFLATE-compressed before chunking; compression is applied only when it actually reduces the size.
Three mechanisms protect a v2 file, and a reader must honour all of them:
- Key separation. The master key is never used directly. HKDF-SHA256 derives two independent subkeys, with the file's IV as the HKDF salt and the ASCII labels
ENCF/v2/cipherandENCF/v2/macas info. - Per-chunk AEAD. The nonce is the 12-byte IV with the big-endian chunk index XORed into its last four bytes. The associated data is the entire 256-byte header followed by the big-endian chunk index — so the filename, IV, algorithm byte, compression flag and chunk ordering are all authenticated.
- Trailer. The final 32 bytes are HMAC-SHA256 over header ‖ ciphertext, keyed with the MAC subkey. This catches truncation.
VERSION HISTORY
v2.0 (current) — algorithm 0x01 is genuine AES-256-GCM per chunk. HKDF subkeys, header bound as associated data, trailer over header ‖ ciphertext.
v1.0 (legacy, read-only) — algorithm 0x01 was AES-256 in CTR mode with a separate HMAC over the ciphertext only, and one key served as both cipher and MAC key. v1 files remain fully readable by this tool and are decrypted through a dedicated legacy path; they are no longer written. If you hold v1 files, re-encrypting them upgrades them to v2.
[READING A HEADER]
Enough to identify a file and route it correctly:
const head = new Uint8Array(await file.slice(0, 256).arrayBuffer());
const magic = String.fromCharCode(...head.subarray(0, 4));
if (magic !== 'ENCF') throw new Error('Not an ENCF file');
const version = head[4] + '.' + head[5];
const algorithm = head[6]; // 1 = AES-256, 2 = ChaCha20-Poly1305
const kdf = head[7]; // 1 = PBKDF2, 3 = random, 4 = user key
const salt = head.subarray(8, 40);
const iv = head.subarray(40, 52);
const nameLen = head[52] | (head[53] << 8);
const name = new TextDecoder().decode(head.subarray(54, 54 + nameLen));
const compressed = head[254] === 0x01;
[FREQUENTLY ASKED QUESTIONS]
What is the ENCF file format?
ENCF is the container format written by CryptFile.Online, using the .enc extension. It consists of a fixed 256-byte plaintext header describing the algorithm, key-derivation method, salt, IV and original filename, followed by the ciphertext and a 32-byte HMAC-SHA256 trailer.
Why is the header not encrypted?
A reader has to know which algorithm and key-derivation method to apply before it can derive a key, so those parameters must be readable up front. The header holds no secret material: salts and IVs are not secrets and are routinely stored in the clear. It is not encrypted, but in format v2 it is authenticated — every chunk includes the header in its associated data, so any edit to it is detected. The one field worth noting is the original filename, which is visible to anyone holding the file.
Can I write my own decrypter for this format?
Yes — that is why this specification exists. Read the 256-byte header, derive the cipher and MAC subkeys with HKDF-SHA256 using the IV as salt, decrypt each 2 MiB + 16 byte chunk as an AEAD ciphertext with the header and chunk index as associated data, then verify the HMAC-SHA256 trailer over header and ciphertext. Reject the file if any tag fails.
Is the encrypted file size predictable?
Yes: 256 bytes of header, plus the payload, plus 16 bytes for every 2 MiB chunk, plus a 32-byte trailer. Both algorithms are authenticated modes in format v2, so both add one 16-byte tag per chunk. A 5 MiB file therefore produces 5 MiB + 256 + 48 + 32 bytes.
What is the maximum filename length?
200 bytes of UTF-8, which is where the field ends. Longer names are truncated at that boundary. Because the limit is in bytes rather than characters, names using non-Latin scripts fit fewer characters — roughly 66 for scripts encoded in three bytes per character.