.crin File Format
Version 2 · Status: stable — version 1 is read-only
A .crin file is a self-contained encrypted container. Everything a reader
needs is in the file except the key, which lives on the dongle that wrote it.
1. .crin — the format new implementations write
Section titled “1. .crin — the format new implementations write”Header is 66 bytes.
offset size field 0 4 magic, ASCII "CRIN" 4 1 version, = 1 or 2 5 1 flags (bit 0 = payload is a ZIP of a folder; other bits reserved, 0) 6 32 K_identity public key of the encrypting dongle 38 16 HKDF salt (random, per file) 54 12 AES-GCM nonce (random, per file) 66 n ciphertext 66+n 16 AES-GCM tag- Key derivation:
K_file = HKDF-SHA256(ikm = file_secret, salt = header[38:54], info = "cryptin-file-key-v1", length = 32). The derivation runs on the dongle. - AAD is empty. Files written by the dongle authenticate the ciphertext only: no header byte is covered by the GCM tag.
- File length is always
66 + n + 16. Anything shorter than 82 bytes cannot be valid. - Flags bit 0 means the plaintext is a ZIP archive of a directory rather than a single file. On decrypt, unzip it back into a folder.
- Reject unknown flag bits rather than ignoring them, so a future flag cannot be silently misread as “plain file”.
- The ciphertext and the tag are adjacent on disk: one run of
n + 16bytes, identical to what a combined-tag API produces.
Version 2
Section titled “Version 2”Written by every dongle from beta-2026-10-13 on, and the only version the chip
still writes. The same 66-byte header as above, with version = 2.
K_file = HKDF-SHA256(ikm = file_secret, salt = header[38:54], info = "cryptin-file-key-v2", length = 32). Cipher: standard AES-256-GCM (NIST SP
800-38D), 12-byte nonce, 16-byte tag. AAD: the whole 66-byte header, bytes
[0, 66).
Any AES-GCM implementation opens a version 2 file given file_secret. Editing
any header byte fails the tag. Version 1 and version 2 keys derived from the same
secret and salt are unrelated. There is no fallback: a version 2 file whose
tag fails is damaged.
Version 1 files written by the chip are not standard AES-GCM
Section titled “Version 1 files written by the chip are not standard AES-GCM”⚠️ This is true of the files already on disk, independently of version 2. A reader built from the description above — standard GCM, empty AAD — does not open any version 1 file written by the chip that is longer than 27 bytes. Up to 27 bytes it agrees with standard GCM, which is exactly why the gap went unnoticed.
- The chip restarts from a 16-byte boundary at every chunk. In CTR terms the
keystream advanced by a chunk that is not a multiple of 16 is discarded, so the
counter for chunk i is
inc32(J0) + Σ_{j<i} ceil(len_j/16). No keystream is ever reused, so confidentiality holds — what breaks is interoperability. - The tag runs GHASH over each ciphertext chunk padded to 16 bytes separately, then the length block with the true total length.
- The cuts are at 27, then 55 bytes, and the file does not record them: a reader has to know them.
Known limitation — the header is not authenticated
Section titled “Known limitation — the header is not authenticated”This section describes version 1 only. Measured on 2026-09-30: the header of
a version 1 .crin written by the dongle is not authenticated. Changing the version byte, the flags byte or the K_identity
field is not detected by the tag. It does not expose the content, and it does not
let another dongle decrypt the file, but a reader must not treat the header as
tamper-evident: validate version and flags explicitly (steps 3–4) and treat the
K_identity comparison (step 7) as a routing check, not an integrity check.
Binding the header as AAD needed a new format version: closed by version 2 on
9 October 2026 for every file written from beta-2026-10-13 on. Version 1
files keep this limitation for as long as they exist.
The file name is not in the format
Section titled “The file name is not in the format”There is no name field. The only thing carrying the original name is the name on
disk, which is why the convention is to append .crin when encrypting and strip
it when decrypting. Anything cleverer loses the name.
2. Where the encryption happens
Section titled “2. Where the encryption happens”On the dongle, not on the host. Since firmware 2026-08-02 the file key never
reaches the computer: the host streams plaintext to the dongle in chunks over
HID and receives ciphertext back (CMD_ENCRYPT_BLOCK / CMD_DECRYPT_BLOCK, see
the protocol). The dongle derives the per-file key with
HKDF over the salt in the header and runs AES-256-GCM itself.
Two consequences worth knowing before you design around it:
- Encryption is gated by the licence, decryption never is. A file already encrypted opens for as long as that dongle exists — expired licence, revoked licence, no network. The licence governs the product; the dongle governs the data.
- The per-transfer ceiling of 5 000 000 bytes belongs to the Windows client
(Personal v1,
MaxEncryptBytes), not to the firmware or to the format, which carry up to ~900 MB; a developer driving the SDK directly is not subject to it. The reason it exists is time: at the measured ~27.8 KB/s of the HID transport, 5 MB is already about three minutes in each direction. Where the ceiling applies to a folder it applies to the compressed archive, not to the folder: 20 MB of text may pass where 5 MB of photos does not.
3. .cryptin — legacy, read-only for new implementations
Section titled “3. .cryptin — legacy, read-only for new implementations”Header is 69 bytes.
offset size field 0 8 magic, ASCII "CRYPTIN1" 8 1 version, = 1 9 32 K_identity public key of the encrypting dongle 41 16 HKDF salt 57 12 AES-GCM nonce 69 m ciphertext with the 16-byte GCM tag appended- AAD is bytes
[0, 69). m = n + 16: the writer emitted ciphertext‖tag as one buffer.- No flags byte — the folder concept does not exist in this container, so there is nothing to preserve when reading one.
New implementations read this and never write it.
Legacy .cryptin files, and any .crin written by a host from file_secret
before August 2026, use info = "cryptin-file-v1" with the header as AAD. The
dongle’s on-chip decryption derives with "cryptin-file-key-v1" and does not open
them.
4. Telling them apart
Section titled “4. Telling them apart”The two magics diverge at byte 2 (CRIN vs CRYP), so the first four bytes are
enough.
read the first 8 bytes
bytes[0..4) == "CRIN" -> .crin, header 66, §1 bytes[0..8) == "CRYPTIN1" -> .cryptin, header 69, §3 anything else -> not a Crypt-in fileDo not dispatch on the file extension. A user who renames a file must still get the right reader, and a wrong extension must not turn a legible error into a corrupt-file error.
Check the version byte after the magic and reject anything other than 1 or
2, with a message that names the version you found — a future version 3
reaching an old reader should say so plainly.
5. Reader algorithm
Section titled “5. Reader algorithm” 1. length >= 82 (.crin) or >= 85 (.cryptin), else "file too short" 2. magic -> pick format (§4), else "not a Crypt-in file" 3. version == 1 or 2, else "unsupported version N" 4. .crin only: reject unknown flag bits 5. slice pubkey, salt, nonce at the offsets for that format 6. header = the bytes before the ciphertext version 2: this IS the AAD, bytes [0, 66) version 1: not authenticated at all (see Known limitation) 7. compare the header pubkey with the attached dongle's K_identity7a. version 2: K_file = HKDF(file_secret, salt, "cryptin-file-key-v2") standard AES-256-GCM, AAD = header[0:66] tag fails -> "corrupted or tampered with", stop. No steps 8-9. 8. version 1: hand ciphertext to the dongle for decryption8b. version 1, off-chip reader: try standard GCM with "cryptin-file-key-v1" and empty AAD; if the tag fails, retry with the chip's per-chunk model (§1, cuts at 27 then 55), comparing the tag in constant time; if that fails too, fall back to the legacy derivation of §3Step 7 is not an optimisation
Section titled “Step 7 is not an optimisation”Comparing the pubkey before sending anything to the dongle is what lets you tell “this file belongs to a different dongle” from “this file is damaged”. Underneath they are the same AEAD failure, and collapsing them is the one error that makes the warning useless: the two have opposite advice — find the right dongle, versus restore from backup.
The header is not authenticated, so a tampered pubkey is not caught by the tag: this comparison is the only thing that looks at it. If your reader reports “damaged” for a file encrypted by another dongle, the fast comparison is not running and your users are getting the wrong advice.
The comparison runs in the client, not in the firmware: the dongle never sees the header and decrypts for whichever unlocked dongle is physically connected. Writing your own client means implementing step 7 yourself — and it means the check is worth exactly what the machine running it is worth. See dongle binding.
6. What the format deliberately does not have
Section titled “6. What the format deliberately does not have”- No file name, see §1.
- No key wrapping. There is nothing in the file that a stolen key could unwrap. Without the dongle that wrote it, the route back to the plaintext is the 24-word recovery seed: the file key descends from it. Whoever holds the 24 words can open the files without the dongle, which is why the words are kept on paper and nowhere else.
- No compression, except the folder ZIP that flags bit 0 announces.
- No version negotiation. A reader that meets a version it does not know stops and says so.
This page is the normative source for the .crin format. The host-side
reader and writer are in the published repository (src/CryptinSDK/), but
CRYPTIN_FILE_FORMAT.md is an internal document and is not part of it, so
implement against what is written here. If you find an inconsistency, this page
is the bug — report it to
security@lake8.dev if the difference is
security-relevant, to info@lake8.dev otherwise. The
repository takes no issues during the beta.
Terms of ServicePrivacy PolicySecurity contact: security@lake8.devVulnerability Disclosure Policy
Alcuni contenuti sono stati redatti con il supporto di strumenti di intelligenza artificiale generativa e revisionati dall'autore. Le immagini hardware hanno scopo puramente illustrativo.
Some content was drafted with the support of generative AI tools and reviewed by the author. Hardware images are purely illustrative.
Einige Inhalte wurden mit Unterstützung generativer KI-Werkzeuge verfasst und vom Autor überprüft. Hardware-Abbildungen dienen ausschließlich illustrativen Zwecken.
Algunos contenidos han sido redactados con el apoyo de herramientas de IA generativa y revisados por el autor. Las imágenes de hardware tienen carácter meramente ilustrativo.
In caso di conflitto tra versioni linguistiche, prevale il testo in lingua italiana.