Technical
For anyone who wants the specifics: formats, parameters, constructions — and the places where we deliberately promise nothing.
A vault file is a plaintext but MAC-protected header and an encrypted body. The header carries only what deriving the key needs: the salt, the KDF parameters and the kinds of factor. Everything else — entries, notes, photos and the access conditions — sits in the body.
Up to version 3 the coordinates and the opening date sat in the plaintext header. For an app whose purpose is to hide where valuables are, that was the product giving away its own subject. Since version 4 they are in the body, and the consequence is deliberate: conditions can only be read after decryption.
The file always holds two slots. A second slot that was never set up is filled with random data of the same length and is indistinguishable from a real one — so the file does not reveal whether a second vault exists.
| Parameter | Value |
|---|---|
version | 4 |
| AEAD | AES-256-GCM |
| KDF | Argon2id (libsodium), opsLimit 2, memLimit 64 MiB, p = 1 |
| KDF (Altbestand) | PBKDF2-HMAC-SHA256, 600 000 |
| HKDF | HKDF-SHA256, reine Dart-Implementierung |
| Slots | 2 (immer beide vorhanden) |
| Wraps je Slot | 2 — Faktorpfad, Notfallpfad |
Argon2id turns the factors into a base key. Argon2id is memory-hard: 64 MiB per attempt hurts an attacker with special hardware far more than compute rounds alone. On a 2024 device an attempt costs about a second; PBKDF2 at a comparably defensible round count measured several seconds to over twenty, because Android offers no native path for it.
The base key does not decrypt anything directly. Encryption uses a random body key, wrapped twice: once under the factor path, once under the recovery path. Where a path is unused, a decoy of identical length stands in its place. Two consequences: the expensive Argon2id runs once no matter how many location cells are tried, and the header does not betray which kind of vault you are holding.
Files as part of the key go through Shamir over GF(28) with the
Rijndael polynomial 0x11B: either all N in a fixed order, or
any K of them as a fallback.
The place is not a question the app asks; it is part of the key schedule. The position is quantised onto a grid, the cell is hashed into an identifier, and that identifier enters the derivation of the path key as the salt:
masterKey = HKDF(baseKey, salt = cell id)
Outside the cell the key therefore does not exist — there is nothing for a modified app or a spoofed location to bypass. The vault is bound to exactly one cell; the search radius only says how many neighbouring cells the client tries, and so costs time rather than safety.
| Parameter | Value |
|---|---|
| Raster | 50 m, metrisch quantisiert |
| Zellkennung | SHA-256 über tva:geo:v1:… |
| Ableitung | HKDF-SHA256, salt = Zellkennung |
| Suchradius | 150 / 300 / 600 / 1200 m |
| Kandidaten | 25 / 121 / 529 / 2209 Zellen |
| Kosten (razr 50 ultra) | 72 / 162 / 220 / 385 ms |
| Gespeichert | nichts — weder Koordinaten noch Zelle noch Radius |
A time capsule is an implementation of beattime-seal-v1. The
note's key is split by Shamir; the beacon shares are encrypted with
identity-based encryption (Boneh–Franklin over BLS12-381) to an
identity derived from the opening moment. The matching "private" half is the
signature the beacon publishes at that moment — before then it does not
exist, not even for the operator.
The pairing arithmetic lives in a dedicated Rust library on
ark-bls12-381. The widely used tlock crate is
not used: it hardcodes a 16-byte sigma, because all it ever encrypts
is an age file key. The reference draws sigma at the length of the message —
at the 32 bytes sealed here the two produce different ciphertexts, and
neither opens the other's.
Nothing is allocated across the C boundary that anyone would have to free: both results have a length the caller can compute in advance, the caller provides the buffer, and every entry point catches panics.
| Parameter | Value |
|---|---|
| Format | beattime-seal-v1 |
| Hauptgeheimnis | 32 B, Shamir über GF(28), Polynom 0x11B |
| Nutzlast | AES-256-GCM-STREAM, Segment 65 536 B |
| Nonce | Präfix 7 B ‖ Zähler 4 B BE ‖ last 1 B |
| AAD | SHA-256 des kanonischen Kopfs |
| Schlüssel | HKDF-SHA256, info = beattime-seal-v1|aead |
| IBE | Boneh–Franklin über BLS12-381 |
| Kurvenschema | bls-unchained-g1-rfc9380 |
| Chiffrat | U 96 B (G2) ‖ V 32 B ‖ W 32 B |
| Beacons | drand quicknet + BeatTime-Schlüsselserver |
| Profil | standard — 2 von 3 |
A photo taken inside the app can be given a proof of existence. What travels is the SHA-256 checksum, never the picture. The service places the value in a Merkle leaf; once the week closes, the root is signed with Ed25519. The app stores the checksum, the time, the inclusion proof and the signature, and recomputes the proof offline: photo to leaf, leaf to root, signature against the public key.
This is proof pointing backwards — "this existed by then" — and therefore explicitly not a time lock. A time lock needs the opposite direction, and that is what the capsules are for.
A format that only talks to itself proves nothing. Three checks run against foreign code, one per layer:
tlock-js, and back
again; plus against a signature drand really published.On top of that, tests on real hardware: that the arm64 library loads inside the package, that a place-bound capsule does not open in another city, and what all of it costs — a pairing lands at roughly 10 ms.