Technical documentation for security researchers, IT officers and customers who want to know exactly what gets encrypted, how, and why.
Revier3D encrypts all hunting data client-side before it reaches the server. The server is a dumb, encrypted storage: it sees only ciphertext and cannot decrypt it. It does not know where your boundary is, nor what you harvested.
Every hunting ground created through the setup assistant is sealed from the second it is created: key material and ground row come into being in a single write, so there is no window of "ground exists, key does not yet". The ground password is mandatory there, and it is also the encryption password; the server refuses to let a diverging second password be established beside it.
Architecture: row-level encryption instead of one _enc column per field. Each row is stored as a single encrypted JSON blob (data_enc). Only the columns the server needs for sync, tenant isolation and foreign keys stay in clear text: id, revier_id, the *_id references, created_at, updated_at, deleted_at, created_by. All content (coordinates, game species, date, name, note and so on) sits in the blob.
The ground key (RK) is generated once at setup as 256-bit random and is not derived from the password; instead it is wrapped with a key derived from the password (KEK). That is the precondition for password changes not making data unreadable. The change itself is not built yet and is currently rejected with 409, see section 7.
What the server ultimately knows: account email (login), Paddle payment data (billing), hunting-ground slug (URL), hunting-ground status (access control), memberships (who, in which role), storage used (quota). No hunting data, not even the position. 3D terrain, aerial imagery stock and the boundary file follow a second route (section 5.3), because they are produced on the server rather than on the device.
There is no operator emergency key and no backdoor, not even during the beta. The database holds no column for a second, operator-owned wrapping of the RK; it was planned and explicitly dropped on 9 August 2026 (section 6).
hash-wasmp=4 gains nothing in today's single-threaded WASM and costs nothing; it stands ready for a future threaded build.revier.kek_salt)revier.kek_params and are delivered by /api/r/<slug>/info. Hard-coding them would mean that a later change makes every existing rk_wrapped impossible to unwrap.The password no longer leaves the client. Before encryption it travelled to the server in clear text at /api/login/shared, where the server could have derived the KEK itself and unwrapped rk_wrapped. So a single Argon2id run produces 64 bytes, which HKDF-SHA256 splits with different labels into two keys: KEK stays on the device and unwraps the RK, AUTH goes to the server, which keeps only a password hash of it. KEK cannot be computed back from AUTH. A second Argon2id run would be another second of waiting on a phone with no security gain.
Why not PBKDF2 or scrypt? PBKDF2 is GPU- and ASIC-friendly and therefore inferior for password-based keys today. scrypt is a strong alternative, but Argon2id is the winner of the 2015 Password Hashing Competition with broader library support. The hybrid mode (id) protects both against side-channel attacks (memory-hard at the front) and GPU cracking (time-hard at the back).
Deliberately memory-hungry. 64 MiB per attempt makes mass cracking on GPUs uneconomical. On a user's end device 64 MiB is a negligible load; for an attacker with millions of attempts it is a volume-limiting barrier. On the server a plain brake is added: ten attempts per connection in ten minutes, then 429.
draft-irtf-cfrg-xchacha on top of ChaCha20-Poly1305 from RFC 8439@noble/ciphers; server cryptography (ChaCha20-Poly1305, X25519, HKDF) plus about 30 hand-written lines of HChaCha20, checked against the draft's test vectorWhy XChaCha20 and not AES-256-GCM (NIST SP 800-38D)? The 192-bit nonce allows random nonces without bookkeeping. AES-GCM has 96 bits, and a counter would have to stay unique across all devices: four friends editing the same hunting ground offline and syncing later cannot reliably manage that, and a repeated nonce costs GCM not only the confidentiality of the two messages but the authentication key. On top of that, ChaCha20 is pure integer arithmetic, needs no AES-NI, is faster than AES on the five-year-old Android in the field, and has no table-based side channels.
The price of that choice is stated in section 2.2: WebCrypto does not know XChaCha20, so the non-extractable key handle that crypto.subtle would offer for AES-GCM is out of reach. We consider nonce safety across device boundaries the heavier argument, but we name the loss instead of hiding it.
What the mandatory context is for. The data model is row-level: an entire row sits as one blob in data_enc. Without authenticated context, whoever holds the database could swap blobs, write the blob of marker 7 into marker 9, or move a row from strecke into ansitz, and the client would decrypt it without complaint: the encryption intact and the data wrong regardless. What the context does not prevent is dropping or duplicating whole rows, or replaying an older blob of the same row. Only a version vector helps there, and that is not built.
revier.pubkey and cannot open it afterwards. (2) Build artefacts: terrain, imagery and the tile stock are sealed by the server itself after the build, because routing them through the client would be 150 MB in each direction.revier.pubkey sits openly on the server, the private half beside it as revier_priv_enc, encrypted with the RK. Any client holding the RK unwraps it; the server never does.What this does not provide: sender authentication. Anyone holding the public key of the hunting ground, which sits in the open, can put something in. For the camera inbound that is exactly the requirement, since the server is meant to be able to. Proving origin needs a signature.
Why X25519 and not ECDH P-256? P-256 (FIPS 186-4) would have been the route to a non-extractable private key via crypto.subtle. Since the symmetric side (section 3.2) cannot use that advantage anyway, it no longer outweighs the rest: Curve25519 has the simpler implementations, no parameter choices to get wrong, and shorter keys. Both sides, browser and Python server, use the same construction.
info labels, so that one half does not betray the other. (2) Deriving the actual box key from the X25519 result and binding it to that one key pair.RK_data / RK_cam / RK_meta from the RK. An earlier version of this document named them; today the RK encrypts rows directly, and the separation is achieved by the mandatory context from 3.2.# Generated once when the hunting ground is created, in the same write as the # ground row (no window of "ground there, key not yet"): RK = random(256 bit) # ground key, generated ONCE, then constant # One Argon2id run, two keys. The password itself goes nowhere. raw = Argon2id(ground_password, revier.kek_salt, m=64 MiB, t=3, p=4) # 64 bytes KEK = HKDF(raw, info="kek") # stays on the device AUTH = HKDF(raw, info="auth") # goes to the server, which keeps hash(AUTH) only # Wrapping 1: ground password (four friends share link + password) revier.rk_wrapped = XChaCha20-Poly1305(Enc, RK, KEK) # Wrapping 2 (optional): per account, with that account's public key membership.wrapped_rk = X25519-Seal(RK, account_pubkey) # Wrapping 3 (optional): 12 recovery words, BIP39 construction words = 12 of 2048 # 128 bit random + 4 bit checksum recovery_enc = Enc(RK, key_from_words) # the way back to the RK recovery_words_enc = Enc(words, RK) # the way back to the words # NO operator emergency key. No backdoor. The promise applies from day 1, # and the column for it does not exist in the schema. # Data encryption (per row, row-level): aad = "revier3d.e2ee.v1|" + ground + "|" + table + "|" + row + "|" + gen envelope = 0x01 || nonce(192 bit, random) || XChaCha20-Poly1305(Enc, row_json, RK, aad) # Server inbound with no client open (camera, build artefacts): envelope = X25519-Seal(file, revier.pubkey) # putting in yes, opening no
The separation between RK (constant) and KEK (from the password) solves the main problem of a derivation-based architecture: a password change need not cost any data. It means only that the single wrapping rk_wrapped is regenerated, a small blob operation, leaving the encrypted data untouched. That change is not built yet: because the ground password and the encryption password are one and the same, it has to switch both atomically, and until then the server rejects a password change on an encrypted ground with 409 and an explanatory message rather than silently separating access from key.
rk_version carries the key generation and travels in the context of every row. It stands at 1 everywhere today: rotating the RK and re-encrypting the stock does not exist yet. Only that would render an old, possibly leaked RK worthless.
26 content tables are encrypted, among them marker, foto, strecke, ansitz, wartung, schaden, belegung, buchung, sketch and sketch_stroke, kontrolle, nachsuche, fuetterung_log, zone, line, cam_bild. Each gets one column:
data_enc: the whole row as one encrypted JSON package. The previous content columns remain and carry neutral placeholders in a sealed ground (coordinates 0/0, dates 1970-01-01, amounts 0), so that foreign keys, indexes and database constraints keep working.Staying in clear text per row: id, revier_id, all *_id references, the timestamps created_at / updated_at / deleted_at, created_by, and a few ordering values the server has to check (receipt number and year in the ledger because of the gapless numbering required by § 131 BAO, file size and type of photos because of the storage quota, timestamp and camera identifier of trail-camera images).
On the hunting ground itself: kek_salt (128 bit), kek_params, rk_wrapped, rk_version, pubkey (X25519, public), revier_priv_enc (private half, encrypted with the RK), recovery_enc and recovery_woerter_enc, e2ee_aktiv. Per membership: wrapped_rk.
⚠️ The revier table is not itself a row-encrypted table. It has no data_enc, appears in no clearing fragment, and finalize touches it only with SET e2ee_aktiv=TRUE. Its subject-matter columns therefore stay in clear text permanently, among them name, bundesland and land. The full enumeration and its consequences are in 5.3.
In a sealed hunting ground the route /foto/<id> delivers ciphertext, with a header noting which of the two seal types applies. A service worker (sw.js) intercepts the request, decrypts client-side and returns a normal response. This keeps <img src="/foto/…"> in HTML unchanged, and native lazy-loading, caching and memory management of the browser continue to work. In a sealed ground the same worker also computes the elevation tiles for the 2D hillshading out of the decrypted heightmap, because the server may neither query an outside source for them nor read its own file.
Boundary, terrain model and aerial imagery stock are produced on the server during the 3D build, out of official survey data. They therefore cannot be encrypted on the device; the server seals them itself with the public ground key (3.3). The timing is a lock, not an accident:
The location part of the world file is split off and sealed along with the rest at closing time. It is exactly nine fields: lat_c, lon_c, bbox3857, env_bbox3857, merc_edge, minH, maxH, env_minH, env_maxH. What remains stays readable so that stock counters and asset versions keep working; edge_ground (ground edge length in metres) is among them and is location-free on its own, because computing the latitude back needs merc_edge, and that is sealed.
Unsolved and named here, in order of reach:
.enc and renames nothing. What sits on disk afterwards is reviere/<rid>/tiles/<z>/<x>/<y>.jpg.enc: the content is closed, the path is a Web Mercator tile coordinate. From the minimum and maximum of the folder and file names the diorama section falls out, at z19 to about 50 m, and via the <rid> folder it belongs to exactly one hunting ground. Two circumstances make it worse: after closing, the stock contains only the diorama window, because _vorrat_im_fenster returns constantly false without bbox3857 and nothing is mirrored in any more, so there is no noise; and _e2ee_klartext_offen classifies file contents only and never looks at a directory name. A remedy would be a name HMAC over (z,x,y) with the ground key; it would have to be carried through character-for-character in _e2ee_datei_zeile, _vorrat_kachel and sw.js, and the AAD today hangs on precisely this clear-text identifier tiles/z/x/y. As long as that is not built, the location of the 3D model is listed as a named exception on /security.dem, env_dem, ortho_quelle, quellen{} and veg_stichtag stay in clear text (for example bev1m, at-basemap) and thereby reveal the country, and in Germany, via the survey office, the federal state as well.revier itself. The revier table is none of the row-encrypted tables and is not touched by the sealing run: slug, name, bundesland, land, betriebsart and the authority master data jab_anschrift, jab_bezirk, jab_bevollm_ort, jab_behoerde and jagdgebiets_nr stay readable. _bl_key reads bundesland without any E2EE branch.cam has the primary key (revier_id, slug); name is cleared to '', the slug stays as the row identifier and as the folder name on disk. It is the slugified camera name, that is, hunting-ground geography in prose.meldungs_empfaenger.bezeichnung is not in E2EE_TABELLEN; an entry such as "Polizeiposten Pernitz" reveals the region.There is no operator emergency key and no backdoor. The promise "we cannot look inside" applies from day 1, not only after a beta phase. A second, operator-owned wrapping of the RK was part of the first draft of this project and was explicitly dropped on 9 August 2026; the column for it does not exist in the schema, and the relevant section of the old plan survives only as a record of the rejected variant.
Honest about the operator role: a support access that puts the operator side into a session inside another hunting ground still exists. It grants a session, not a key: without a membership wrapping, without a KEK and without the ground password it yields ciphertext and placeholders in a sealed ground. That is the difference between "may access" and "can read", and only the second half is secured by cryptography rather than by a rule.
Planned, not built: a "report error with data" button. The client would pack the broken record with its surrounding context, encrypt it with the public operator key and upload it; only the private key on the operator's machine could read it, and only that one record. Until then, troubleshooting runs on descriptions and screenshots. The delivered code does not contain this button today.
Encryption that promises everything lies. The following data stays clear text, the minimum without which the server cannot function:
Concrete consequence: the server does not know where your boundary is, not where a marker stands, not what you harvested. It knows only: "Account #31 pays for Hunting Ground #71, which uses 2.3 GB."
Further limits, named rather than omitted:
.enc, the server cannot read it and does not fall back to clear text in order to render./api/cam/stats) still needs it, since 04.09.2026 as the only route. The client therefore sends it in the request body ({"e2ee_ort":{"lat":…,"lon":…}}, rounded to 0.01°, roughly one kilometre). Deliberately in the body and never as a query parameter: the query would sit in the access log, and tenant and location could be reconstructed from it. In an unsealed ground the server does not accept a body location at all. It is not stored, neither in the database nor in a log; in memory it sits as the key of the sun-times cache (per day and location, until the service restarts). If the location is missing, it names the state (ort_fehlt) and the location-dependent blocks stay empty instead of being computed for a substitute location. It is the same window as editing the boundary, not a second concession. What the rounding does and does not achieve: at 47.5° N, 0.01° is a grid 1,112 m tall and 753 m wide, a cell area of 0.84 km², with a maximum offset from the true centre of 672 m. It is a grid snap, not a blur: the same ground always lands in the same cell, repetitions add nothing and do not average out either. And it is not location concealment at rest: the paths of the tile stock (5.3) name the same place permanently and about twenty times more precisely. The rounding limits only what this path additionally sends down the wire. One residual path: between finalize and the files step, world.json still sits in clear text with lat_c; branch 1 of _revier_coords() then takes hold, the server computes the sun times with the true centre, and the rounded body value is never read because of the or. That value no longer goes to any outside service, now that the browser fetches the weather itself."Trust us" is not enough for encryption. Four levels, ascending in persuasiveness:
Deliberately not: an external audit (Cure53 and others). Too expensive for the current stage, not necessary without paying customers. May follow later and is declared in this whitepaper as "not performed", not concealed.
Market research August 2026, measured rather than claimed. Evidence in PLAN-APP-SYNC-E2EE.md part A.
| Provider | Encryption promise | Source |
|---|---|---|
| Revierwelt (market leader) | No E2EE promise measurable. Apple privacy notes declare location, photos, identity, all linked to user identity. | App Store |
| Waidly | No cloud at all, everything local on the device, no registration. 12.99 € one-time. Trade-off: no sharing, no backup, no web, no 3D. | iphone-ticker |
| RevierBuch, JagdCom, Mein Revier | Generic "data security", no E2EE promise beyond TLS. | Provider pages |
| Jagdgefährte | "Encrypted chat". Refers to communication, not hunting-ground data. | Store description |
| CoHunt (international) | E2EE for peer-to-peer communication (mesh chat); hunting-ground data is not stored centrally (no account, no cloud storage). The promise covers communication, not cloud-stored hunting-ground data. | Vendor privacy policy 03/2026 |
| HuntStand, onX Hunt (international) | No E2EE or zero-knowledge promise found. | Market overviews 2026 |
| Signal, Matrix | E2EE for communication. Not designed for hunting-ground data and 3D maps. | Project pages |
Defensible wording: No cloud-based hunting app was found that promises end-to-end encryption for hunting-ground data.
Revier3D positions itself between Waidly (local, isolated) and Revierwelt (cloud, observable): private like an offline app, but still shareable, backed up and reachable from anywhere.
This section examines whether full encryption is compatible with Austrian law and the GDPR. The assessment is based on the legal situation as of August 2026 and does not replace legal advice in individual cases.
The Austrian Constitutional Court (VfGH) declared data retention unconstitutional in 2014. There is no statutory requirement for web applications to store IP addresses. Revier3D is not a telecommunications provider under the TKG. Not storing IP addresses is not only legal but required by data protection law (GDPR Art. 5(1)(c), data minimization).
In criminal proceedings (§ 76 StPO), authorities can demand what is available. If nothing is stored, there is nothing to hand over. You cannot be compelled to store data that you do not want to store.
The user has the right to information about the processing of their data. With E2EE, Revier3D can provide information about metadata (account, email, tariff, storage, hunting-ground ID). The encrypted content cannot be disclosed because Revier3D cannot decrypt it. The user can view the content themselves (decrypting locally). Art. 15 is fulfilled.
The user can export their data in a structured, commonly used format. The browser decrypts it locally and sends the decrypted rows for that one request to the server, which typesets the file and keeps nothing of it (output window, section 7). The resulting file is unencrypted, because it lands with the user and is meant to be processed further there. Art. 20 is fulfilled.
The GDPR explicitly requires encryption as a technical and organizational measure (Art. 32(1)(a)) and privacy by design (Art. 25). Full E2EE exceeds the requirements. E2EE is not only legal but the data protection optimum.
The user has a statutory 7-year retention obligation for accounting records. With E2EE, receipts and bookings are encrypted. On key loss without recovery code, this data is irretrievably lost.
This is the user's obligation, not Revier3D's. The terms (§ 2.4) explicitly state: "The responsibility for the tax accuracy of the entries and for compliance with retention obligations (in particular § 132 BAO) lies with the customer." E2EE does not change this, but increases the risk on key loss.
Revier3D mitigates this risk through: (1) the 12 recovery words at setup, viewable again at any time in the planner, (2) the export as an additional backup outside the product, (3) the ledger export, which the ground's management pulls decrypted and hands to the accountant (section 7).
Public authorities can seize data (§ 76 StPO) or request information. Revier3D hands over what it has: account metadata and encrypted data blocks. The encrypted content cannot be decrypted and therefore cannot be disclosed. This is comparable to ProtonMail (Switzerland) and Tutanota (Germany), both of which operate legally in German-speaking jurisdictions.
Revier3D cannot rule out that a statutory obligation to log connection data (IP addresses) may be introduced in the future. Such an obligation would apply to metadata, not to hunting data. Even then, the encrypted content would remain unreadable.
There is no Austrian legal basis that compels a software provider to weaken its encryption or build a backdoor. At EU level, the CSAR regulation ("Chat Control") is being discussed, which would provide for scanning of encrypted communication. As of August 2026 it is not adopted, targets messengers and not niche SaaS. With open code and reproducible builds, a covert backdoor would be detectable.
@noble/ciphers (XChaCha20-Poly1305), @noble/curves (X25519), @noble/hashes (HKDF-SHA256), hash-wasm (Argon2id, WASM-based). On the server cryptography (ChaCha20-Poly1305, X25519, HKDF) plus about 30 hand-written lines of HChaCha20, because the library does not ship the XChaCha20 variant.revier3d/src/crypto/, delivered as webmap/static/revier-crypto.js; server side webmap/e2ee.py.webmap/PLAN-APP-SYNC-E2EE.md in the repository.