Whitepaper · Security

End-to-end encryption
in Revier3D.

Technical documentation for security researchers, IT officers and customers who want to know exactly what gets encrypted, how, and why.

Status: Rolling out in waves since August 2026. The methods and parameters named in this document were last reconciled against the delivered code on 17 August 2026. Where something is not built yet, it says so here.

1. Summary

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

2. Threat model

2.1 What we defend against

2.2 What we do not defend against

3. Cryptographic design rationale

3.1 Argon2id (key wrapping function, KEK, and the login proof)

Algorithm
Argon2id, RFC 9106, via hash-wasm
Parameters
m=64 MiB, t=3, p=4Measured: about 1.0 s on the approximated old device, 201 ms on an S23 Ultra, 109 ms on a 9950X. The previous 256 MiB blew past the 2-second threshold (2.9 to 4.4 s). p=4 gains nothing in today's single-threaded WASM and costs nothing; it stands ready for a future threaded build.
Salt
128 bit, per hunting ground (revier.kek_salt)
Output
512 bit, split by HKDF into two halves: KEK (256 bit) and AUTH (256 bit)
Parameter storage
The four values travel with the record as 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.

3.2 XChaCha20-Poly1305 (data encryption)

Algorithm
XChaCha20-Poly1305 (AEAD), draft draft-irtf-cfrg-xchacha on top of ChaCha20-Poly1305 from RFC 8439
Key length
256 bit (the ground key RK itself)
Nonce
192 bit, random per message. At that length, random needs no collision bookkeeping.
Authentication
Poly1305 tag, 128 bit, integrated (AEAD)
Envelope
Byte 0 version number, bytes 1 to 24 nonce, then ciphertext and tag. Without the version byte, a later change of algorithm would be a data migration project instead of a case distinction.
Context (AAD)
A mandatory argument, not an add-on. Hunting ground, table, row number and key generation are authenticated along with the data.
Libraries
Client @noble/ciphers; server cryptography (ChaCha20-Poly1305, X25519, HKDF) plus about 30 hand-written lines of HChaCha20, checked against the draft's test vector

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

3.3 X25519 (asymmetric: able to seal, unable to open)

Algorithm
X25519 (Diffie-Hellman on Curve25519, RFC 7748), ephemeral-static, built like a sealed box: one throwaway key pair per sealing, its public half travelling in front
Use cases
(1) Trail-camera inbound: an image arrives by mail or from a portal while no client is open. The server seals it with 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.
Key derivation
The raw X25519 result is not used as a key directly: it is a curve point and not uniformly distributed, and two sealings to the same recipient would yield the same key without binding. It therefore passes through HKDF, with both public keys as salt.
Key pair
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.

3.4 HKDF (key separation)

Algorithm
HKDF-SHA256, RFC 5869
Use cases
(1) Splitting the 64 bytes from the Argon2id run into KEK and AUTH, with different 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.
Not built
Purpose-specific subkeys 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.
Algorithm
Ed25519, RFC 8032
Use case
Not in use yet. Intended for signing the offline licence tokens (wave 5, offline autarky): server signs, client verifies with a built-in public key. The delivered code contains no signature verification today.

4. Key hierarchy

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

5. Protocol details

5.1 Schema

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:

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.

5.2 Photo and tile delivery

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.

5.3 Map files and the tile lock

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:

6. Debugging without backdoor

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.

7. Honest limits

Encryption that promises everything lies. The following data stays clear text, the minimum without which the server cannot function:

Account emaillogin, billing, support
Paddle payment datatariff, status, amount, transaction
Hunting-ground slugURL routing for shared link
Hunting-ground statusaccess control (trial/active)
Membershipsaccount_id, role, wrapped RK per account
Row metadataid, revier_id, foreign keys, created_at, updated_at, deleted_at, created_by

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:

8. Verifiability (four levels)

"Trust us" is not enough for encryption. Four levels, ascending in persuasiveness:

  1. A guard that proves instead of claims (running). Test suites drive the real app against a real server and measure the write paths for whether any content field goes out in clear text, plus the read paths, the output window and the sealing run. With a counter-probe: switch the encryption off and the suites must go red. This is the in-house form of this project.
  2. Your own look at the network (any time, without us). Open the browser's developer tools, the "Network" tab, create a marker: the request body carries a single field of ciphertext, no name, no coordinate, no species.
  3. Open client code (still private today). Necessary but weak on its own: how does the user know the delivered JavaScript matches what is in the repository?
  4. Published bundle hash plus a rebuildable release. Every release will then name the hash of the delivered program; anyone can rebuild from the tag and compare. This is the level that makes the previous one carry weight.

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.

9. Comparison with alternatives

Market research August 2026, measured rather than claimed. Evidence in PLAN-APP-SYNC-E2EE.md part A.

ProviderEncryption promiseSource
Revierwelt (market leader)No E2EE promise measurable. Apple privacy notes declare location, photos, identity, all linked to user identity.App Store
WaidlyNo 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 RevierGeneric "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, MatrixE2EE 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.

10. Legal compatibility

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.

10.1 Data retention (IP addresses)

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.

10.2 Right of access (GDPR Art. 15)

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.

10.3 Right to data portability (GDPR Art. 20)

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.

10.4 Privacy through encryption (GDPR Art. 25, 32)

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.

10.5 § 132 BAO (accounting record retention)

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

10.6 Government disclosure (StPO)

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.

10.7 No backdoor mandate

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.

11. References