evb-e2ee-v1 — the wire format produced by the Everabyte apps and this SDK. Everything here happens client-side only; Everabyte’s servers store the resulting bytes verbatim.
1. Content format EVB1
- Chunk plaintext size: 64 KiB (65536), every chunk except the last; the last may be shorter. An empty file still has exactly one empty chunk (a lone 16-byte tag) — payload = 32 bytes.
- fileNonce: 12 random bytes per file. Bytes
[4..12]are transported but unused by nonce derivation (v1 quirk, kept for wire compatibility). - chunkNonce(i) =
fileNonce[0..4] || BE u64(i)(12 bytes). - AAD(i) = 8 bytes:
BE u32(i)then 4 zero bytes. Chunk position is authenticated — reordering or splicing chunks fails the GCM tag. - Decryption walks blocks sequentially; unknown magic, truncated tags and trailing garbage are hard errors.
- Minimum ciphertext for a plaintext of length L:
16 + L + 16*ceil(max(L,1)/65536).
2. Key hierarchy
3. DEK envelope — v2 (current)
HPKE RFC 9180 strict, mode PSK:base_nonce, derived from the key schedule — no nonce is transported. Senders and receivers compute the same DHKEM shared secret with kem_context = enc ‖ pkRm on both sides.
4. DEK envelope — legacy v1 (read-only)
shared = X25519(skE, pkR) and kemShared = ML-KEM.encaps(pkMlkem); wrapKey = HKDF-SHA256(shared ‖ kemShared, info="evb-e2ee-file-wrap-v1"); wrappedDek = AES-256-GCM(wrapKey, nonce, DEK). Envelopes without a v field are v1; every current client unwraps both. New wraps must use v2.
5. UMK wraps
All blobs are base64; framing isnonce‖ct‖tag (device) or salt‖iv‖ct‖tag (mnemonic). The plaintext of every UMK wrap is JSON with the four UMK fields (publicKeyX25519, privateKeyX25519, publicKeyMlkem, seedMlkem).
Unwraps try the candidates in order and let the AES-GCM tag decide — the parameters that open a blob are by construction those it was sealed with. Phrases are NFKD-normalized, trimmed, lowercased and whitespace-collapsed first.
Device wraps derive the wrap key with
HKDF-SHA256(deviceSecret32, info="evb-e2ee-device-wrap-v1") and frame as nonce12‖ct‖tag. The device secret is 32 random bytes kept exclusively on the device; the server sees only a label.
6. Names (metadata encryption)
- Wire filename (what the upload multipart carries):
evb-+ hex ofHMAC-SHA256(realName, key = SHA256(privateKeyX25519 ‖ seedMlkem)). Deterministic — the same real name maps to the same alias, which keeps server-side duplicate detection working without revealing the name. - Encrypted display name:
AES-256-GCM(realName, DEK)transported asb64(nonce12 ‖ ct ‖ tag16)in the envelope attach (encryptedNamefield).
7. Sharing wraps
Symmetric wraps under a 32-byte link key (public links) or invitation key share (email invites):wrapKey = HKDF-SHA256(key32, info), info = evb-e2ee-link-wrap-v1 or evb-e2ee-invite-wrap-v1; AES-256-GCM over the DEK. The key travels only in the URL fragment and is never stored server-side.
8. Versioning policy
- Fields are added with defaults; nothing is renamed in place. A format change is a new scheme id (or a bumped
v), shipped together with a dispatch path in every client. - Interop contract: a client that cannot open a scheme must fail loudly, never reinterpret bytes.
- The frozen vector grid (
evb-e2ee-v1.json) is the compatibility gate — every binding re-runs it in CI. Byte-exactness against RFC 9180 / NIST / RFC 5869 official vectors is tested separately from the grid.
PROTOCOL.md in the SDK repository — this page is its public summary.