Skip to content
🔧 Developer beta launches 30 September 2026 — free for developers. Flash your own ESP32-S3 and test it. Learn more →

.crin File Format

Version 1 · Status: stable

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
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
  • AAD is bytes [0, 66) — the whole header, exactly as it appears on disk. Keep the slice; do not rebuild it from parsed fields.
  • 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 + 16 bytes, identical to what a combined-tag API produces.

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.


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 4.8 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.


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 file

Do 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, with a message that names the version you found — a future v2 reaching an old reader should say so plainly.


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, 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 — keep the exact slice, it is the AAD
7. compare the header pubkey with the attached dongle's K_identity
8. hand ciphertext to the dongle for decryption

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.

Because the header is the AAD, a tampered pubkey would fail the tag anyway. 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 there is no route back to the plaintext, and that is the entire design.
  • 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.

The normative source for this page is CRYPTIN_FILE_FORMAT.md in the SDK repository. Where this page and that document disagree, the document is right and this page is the bug — please report it to security@lake8.dev if the difference is security-relevant, or open an issue otherwise.

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.