{"version":1,"relay_protocol_version":1,"generated_at":"2026-10-31T00:08:22.610281+00:00","server_version":"PrivacyPortal Messenger Relay 1.0.0","code_repository":"https://github.com/privacyportalUK/encrytedpp","code_revision":"e618ae48b8b2203b1cd1ed2dc9d74844ba5a8e46","release_identity":{"attested":true,"schema_version":1,"source":{"repository":"https://github.com/privacyportalUK/encrytedpp","commit":"e618ae48b8b2203b1cd1ed2dc9d74844ba5a8e46"},"artifact":{"name":"graphite-backend-e618ae48b8b2203b1cd1ed2dc9d74844ba5a8e46.tar.gz","sha256":"56d65d3808019c0065a4100c3c02c7a7b1c719cb48caca1af11809dbd929a3b1","extracted_tree_sha256":"dede9ec7661926576c3dbbd7be9161f19f12e2ca3736fe4badb71d88efc3cdfd"},"dependency_lock":{"path":"backend/requirements.lock","sha256":"a818a25de93848b02f300a0ab1e842f933c5de0726a28590c259d836023bc377"},"sbom":{"format":"CycloneDX 1.5 JSON","path":"graphite-backend-e618ae48b8b2203b1cd1ed2dc9d74844ba5a8e46.cdx.json","sha256":"e1e23f6ae8b9dd1b0f5b0340137ae16aa0206b6d93ab4d351e2b612e9de4f775"},"provenance":{"format":"https://slsa.dev/provenance/v1","path":"provenance.slsa.json","sha256":"94d9780a182f71f7c11de1ed0ea263b4ae5b727e11cae1d1a11c6557905aab01"},"transparency_manifest":{"path":"backend/transparency_manifest.py","sha256":"b9930571d8b99080272161ba7cf6a81cf84d4385f19aaf4291e21cadcac5eafc"},"release_certificate_sha256":"a28b4f5d3a72fc5a1b42e89ce4f617cff686d11193bb2b59b31183729d0a21c0","builder":{"id":"https://privacyportal.co.uk/build/relay/2026-09-16","build_started_at":"2026-09-16T21:54:23Z","build_finished_at":"2026-09-16T21:57:23Z"}},"is_canonical":false,"relay_metadata":{"operator_name":"","operator_contact":"","operator_jurisdiction":"","uptime_pledge":"","operator_added_features":[],"subpoena_policy_url":null},"alternate_endpoints":{"clearnet":"https://messenger.privacyportal.co.uk","onion":"http://ux24z2gggqr6j4zntooai3db264xnlrgwqd7uitoz45gcibeaolsx6qd.onion"},"data_we_store":[{"name":"users","fields":["user_id (random UUID string)","account_number (16-char server-issued id from a 32-symbol alphabet; independent from cryptographic keys)","account_number_history (optional array, capped at 5: past numbers with valid_until ~7 days for lookup rerouting after rotation)","box_pub (Curve25519/X25519 public key, base64)","mlkem_public_key (optional ML-KEM-768 public key for hybrid PQ encapsulation, ~1184 bytes raw, base64)","sign_pub (Ed25519 verify key, base64)","username (optional lowercase public handle; may be null)","discoverable_by_username (boolean — whether this user appears in @username lookups via POST /identity/by-username)","username_changed_at (optional; enforces at most one username change per 24h via POST /account/username)","display_name (optional opaque nickname chosen by client)","created_at","last_seen","supported_envelope_versions (optional client capability integers, e.g. 4 = per-message ratchet)"],"retention":"Until DELETE /api/account (or manual DB wipe). Usernames are freed when cleared or when the user row is deleted.","purpose":"Public identity directory and signed-challenge lookups for account operations. Account number is independent from cryptographic identity; rotating it swaps the lookup mapping — existing conversations keep working via box_pub."},{"name":"messages","fields":["message_id (random UUID)","to_box_pub (recipient routing key)","created_at","received_at (relay enqueue time; BSON Date for Mongo TTL)","expires_at (optional; Mongo TTL index when set)","delivered / delivered_at (offline queue bookkeeping until ack/TTL)","kind (opaque client label, e.g. text | image)","ttl_seconds (optional duplicate of client intent)","meta_ciphertext (optional opaque blob)","Legacy rows: from_box_pub, envelope { ephemeral_pub, nonce ciphertext }","Sealed-sender rows: sealed=true, blob_b64 (opaque outer ciphertext; no sender field)"],"retention":"Deleted on first successful delivery acknowledgement (POST /api/messages/ack or optional WebSocket type=delivered), OR 7 days from receipt (relay enqueue time in received_at) for offline-only fallback, whichever first; also removed when expires_at TTL fires (disappearing messages), or when deleted with the recipient account.","purpose":"1:1 ciphertext routing and short-lived offline queue."},{"name":"discoverability_toggle_rl","fields":["box_pub","day","count","created_at","expires_at"],"retention":"Approximately 2 days via Mongo TTL.","purpose":"Per-account daily abuse limit for public username discoverability changes."},{"name":"group_messages","fields":["protocol_version (2 for current writes)","message_id","group_id","epoch and state_hash (membership/key state bound into client AEAD additional data)","from_box_pub","to_box_pub (per-recipient fan-out copy)","nonce","ciphertext","ciphertext_sha256","kind","meta_ciphertext","ttl_seconds","created_at","received_at (relay enqueue time; BSON Date for Mongo TTL)","expires_at (Mongo TTL when set)"],"retention":"Deleted on recipient acknowledgement (same paths as direct messages), OR 7 days from received_at, whichever first; plus expires_at TTL and account deletion as applicable.","purpose":"Store encrypted group payloads per recipient for offline delivery."},{"name":"groups","fields":["group_id","protocol_version (legacy rows are marked 1; current rows are 2)","epoch (monotonic integer) and state_hash (canonical roster/key-state digest)","owner_box_pub","successor_box_pub (canonical successor used for signed owner leave)","members (list of member box_pub)","wrapped_keys (one fresh NaCl-box group-key wrap per current member; opaque to server)","name_ciphertext (optional opaque)","rekey_pending and pending transition metadata (temporary send suspension after removal)","created_at"],"retention":"Until every member leaves or accounts are deleted (group row deleted when empty).","purpose":"Client-side group key distribution metadata and compare-and-swap membership epochs; member lists and transitions are visible to the relay as public keys and timing metadata."},{"name":"files","accepting_new_uploads":false,"fields":["file_id","owner_box_pub","size","data (raw ciphertext bytes)","created_at","expires_at (Mongo TTL; default ~30 days when client omits TTL)"],"retention":"Until TTL deletes the row or manual/admin intervention.","purpose":"Temporary ciphertext blob hosting for attachments referenced inside E2EE messages. New uploads are disabled; retained ciphertext remains until TTL or deletion."},{"name":"file_objects_v2","fields":["_id (SHA-256 of a random 256-bit file capability; capability itself is not stored)","state (authorized, uploading, ready, or failed)","permit_hash (removed when the one-time upload begins)","idempotency_hash","scope (upload)","ciphertext_length and ciphertext_sha256","chunk_count","permit_expires_at, created_at, completed_at, expires_at"],"retention":"Mongo TTL on expires_at; incomplete objects are shortened to approximately one hour.","purpose":"Single-use authorization and ciphertext-only manifest for v2 attachments; no identity, filename, MIME type, EXIF, or plaintext is stored."},{"name":"file_chunks_v2","fields":["file_key (SHA-256 capability hash)","chunk_index","data (fixed-size raw ciphertext chunk)","created_at and expires_at"],"retention":"Mongo TTL on expires_at; partial chunks are deleted immediately when length or digest verification fails.","purpose":"Bounded streaming storage for encrypted v2 attachments without BSON-sized whole-file documents."},{"name":"username_nonces","fields":["box_pub","purpose","nonce","expires_at (TTL ~5 minutes)"],"retention":"Mongo TTL on expires_at (single-use signed challenges).","purpose":"Replay-resistant proof-of-possession for registration, username changes, account delete, account-number rotation, optional username set/clear, and ML-KEM key publication."},{"name":"spent_tokens","fields":["_id (SHA-256 of the scoped, expiring Privacy Pass token; opaque)","scope (message or upload)","spent_at","expires_at (issuer-epoch acceptance end plus a replay-ledger grace period)"],"retention":"Replay ledger only; TTL deletes a row only after the token's complete bounded acceptance lifetime.","purpose":"Ensure each messaging or upload token is used at most once and cannot cross scopes."},{"name":"spent_inline_sigs","fields":["_id (SHA-256 of purpose, box_pub, and the public Ed25519 request signature)","expires_at (Mongo TTL just beyond the accepted signature clock-skew window)"],"retention":"Approximately 6 minutes via Mongo TTL.","purpose":"Reject replay of timestamp-signed authenticated API requests."},{"name":"pp_issue_rl","fields":["_id (SHA-256 of token scope and box_pub)","scope","atomic lease_id and lease_until while evaluation is in progress","last_issue_at and next_issue_at","expires_at (Mongo TTL rolling)"],"retention":"Ephemeral atomic rate-limit ledger only; blinded requests and issued tokens are not stored.","purpose":"Permit exactly one bounded Privacy Pass issuance window per identity and scope during each cooldown."},{"name":"guest_rooms","fields":["room_id (server-generated alphanumeric)","protocol_version (2)","invite_verify_key (public Ed25519 verifier deterministically derived client-side from the URL-fragment secret; the secret and signing seed are never sent)","created_at","last_activity_at","participants (dedicated ephemeral Ed25519 sign_pub, X25519 eph_pub, optional ML-KEM public key, signed invite proof, signed announcement nonce, optional display_name ≤32 chars, joined_at, last_seen_at)","expires_at (Mongo TTL)"],"retention":"Up to 1 hour after creation, or when empty shortened to ~5 minutes after the last participant leaves (whichever applies); TTL deletes the row automatically.","purpose":"Ephemeral invite-only chat rooms with no accounts. The relay never receives the shared URL secret (clients keep it in the URL fragment only). A one-way public verifier checks signed enrolment proofs, and dedicated participant signatures bind announce, reannounce, send, leave, WebSocket auth, and ack actions."},{"name":"guest_messages","fields":["protocol_version (2)","room_id","message_id","sender_sign_pub and recipient_sign_pub","sender_eph_pub","recipient_eph_pub","ciphertext (opaque base64)","ciphertext_sha256, action_nonce, and participant signature","sent_at (BSON Date)"],"retention":"Deleted when the recipient acknowledges delivery over the guest WebSocket, or after ~1 hour via Mongo TTL on sent_at.","purpose":"Short-lived ciphertext queue for guest rooms when the recipient is briefly offline."},{"name":"guest_action_nonces","fields":["_id (domain-separated SHA-256 of room, participant signing key, and action nonce)","expires_at"],"retention":"Until the guest room expiry via Mongo TTL.","purpose":"Single-use replay ledger for signed guest v2 actions."},{"name":"spent_group_v2_actions","fields":["_id (domain-separated SHA-256 of actor and signed action nonce)","expires_at"],"retention":"Just beyond the accepted signed-action freshness window via Mongo TTL.","purpose":"Single-use replay ledger for signed group v2 state and send actions."},{"name":"relay_metrics","fields":["_id (messages_delivered_YYYY-MM-DD, privacy_pass_tokens_YYYY-MM-DD, or client_usage_YYYY-MM-DD)","delivered (integer count of queue rows removed after recipient acknowledgement; older historical rows may contain legacy successful live-socket push counts)","issued (integer aggregate count of Privacy Pass tokens evaluated)","sessions.android|desktop|web|unknown (authenticated socket sessions/reconnects; integer aggregates)","confirmed_receipts.android|desktop|web|unknown (messages removed after a recipient acknowledgement; integer aggregates)","download_requests.android|desktop|web|unknown (accepted APK/EXE download requests; integer aggregates, not completed installs)","expires_at (client_usage rows only; Mongo TTL at approximately 90 days)"],"retention":"client_usage rows expire automatically after approximately 90 days. Legacy messages_delivered and Privacy Pass aggregate rows are operational counters that the operator may prune.","purpose":"Daily privacy-safe aggregate totals for confirmed message acknowledgements (with older legacy push-count history), Privacy Pass evaluation, coarse client-platform sessions/reconnects, confirmed message receipts, and accepted download requests. These counters contain no account/public keys, device identifiers, IP addresses, User-Agent strings, or per-user histories, and must not be interpreted as unique users, devices, installs, or completed downloads."},{"name":"desktop_links","fields":["token_hash (domain-separated SHA-256; raw single-use token is returned once and not stored)","used (boolean)","created_at","used_at (when claimed)","expires_at (Mongo TTL; default ~60 days)"],"retention":"Until the configured expiry via Mongo TTL; claimed rows remain only until that expiry.","purpose":"Single-use capability links for release downloads without retaining the bearer token."},{"name":"bug_reports","fields":["report_id (random UUID)","created_at","expires_at (Mongo TTL; default ~90 days)","status and staff signoff","message (user-submitted plaintext, capped at 4,000 characters)","app_version and platform (optional user-submitted strings)","handle (optional user-submitted public contact handle)"],"retention":"Approximately 90 days by default via Mongo TTL (operator-configurable).","purpose":"User-requested support and bug triage. The UI warns users not to include secrets or chat content."},{"name":"identity_events","fields":["pseudonymous identity_id and immutable box public key","sequence, previous-event hash, event type, status, and signed_at","Ed25519 signing public key and ML-KEM public key","owner continuity signatures, event hash, log index, and Merkle leaf hash"],"retention":"Append-only while the relay operates the identity log. Account deletion removes directory discoverability but does not erase already committed public-key events.","purpose":"Client-verifiable public-key history and key-substitution detection. Usernames, account numbers, display names, messages, and social-graph data are not logged."},{"name":"identity_log_state","fields":["singleton append-only log size","Merkle frontier hashes and current root hash"],"retention":"For the lifetime of the identity transparency log.","purpose":"Allocates contiguous Merkle-log indexes; contains no account or public-key data."},{"name":"identity_merkle_nodes","fields":["Merkle subtree level and index","SHA-256 subtree hash","append index that completed the subtree"],"retention":"For the lifetime of the identity transparency log.","purpose":"Generates logarithmic inclusion and consistency proofs without scanning identity events."},{"name":"voprf_issuer_key","fields":["NIST P-384 private key PEM on disk (`backend/.voprf_secret_key.pem`, gitignored, chmod 0600 on Unix)"],"retention":"Until the operator rotates or deletes the PEM file.","purpose":"RFC 9497 VOPRF (P384-SHA384) issuer secret — proves authenticity of anonymous send-tokens. Compromise enables forged tokens (spam), not decryption of ciphertext."}],"data_we_never_store":["Plaintext message bodies","Plaintext file contents","Sender identity for sealed-send message rows (no from_box_pub stored for sealed transport)","User private encryption/signing keys or user recovery phrases","ML-KEM secret keys never leave the user's device — they are derived from the user's seed phrase locally.","Per-user IP address persistence by the relay application database or application logger; network and infrastructure providers may process connection metadata","Geolocation","Third-party advertising or analytics identifiers","Per-user or per-device platform, build-version, session, receipt, or download histories; client-platform metrics are daily aggregate integers only","Any mapping between Privacy Pass tokens issued in a batch and the box_pub that requested issuance after the HTTP response completes"],"privacy_limitations":["Privacy Pass cryptographically blinds token inputs and the relay does not persist a token-to-account mapping after issuance.","Cryptographic blinding does not hide connection metadata: network observers, hosting providers, and timing analysis may correlate issuance and later spending.","The relay still learns each message recipient and sees ciphertext size and timing; v2 attachment capabilities are presented when uploading or downloading but are stored only as hashes."],"relay_without_storage":["Typing indicator JSON may be forwarded between online WebSocket peers (not written to MongoDB).","Read receipt JSON may be forwarded between online WebSocket peers (not written to MongoDB)."],"client_features":["Per-message ratchet (v=4 envelope) — message keys advance per-message and are deleted after the user's chosen retention window.","Server stores no ratchet state; forward secrecy is entirely client-side.","Optional hardware-backed unlock (StrongBox / Android Keystore) — encryption key wrapping is delegated to the device's secure element where available."],"client_side_ai":{"summary":"All AI features in the official PrivacyPortal Messenger client run on-device using bundled ONNX Runtime (WebAssembly). No message content, embeddings, vectors, or derived metadata are transmitted to the relay or to any third-party model API.","bundled_models":[{"name":"gte-tiny.onnx","purpose":"Sentence embeddings for offline semantic search","approx_on_disk_mb":22}]},"client_app":{"reproducible_build":false,"reproducible_build_status":"verification_pending","build_instructions_url":"https://github.com/privacyportalUK/encrytedpp/blob/main/APK_BUILD.md","release_cert_sha256":"a28b4f5d3a72fc5a1b42e89ce4f617cff686d11193bb2b59b31183729d0a21c0"},"logs":{"what_is_logged":["Per-minute aggregate counters only (HTTP requests counted, successful websocket push deliveries, inbound websocket JSON messages, active websocket connections snapshot).","Aggregate Privacy Pass issuance totals (integer batch sizes / daily counters — never blinded element contents or tokens).","Daily coarse client-platform aggregate counters (sessions/reconnects, confirmed receipts, and accepted download requests) stored as relay_metrics integers, never per-event records.","Validation failures as integer counts of error items/types (no field values).","Sparse operational warnings with exception class names only (no client payloads, IPs, or keys)."],"what_is_NEVER_logged":["box_pub, sign_pub, username, account_number, message_id, group_id, raw IP addresses, request bodies, query strings","Privacy Pass pp_token payloads, blinded VOPRF elements, or finalized token bytes","Client platform or build version tied to a box_pub, IP address, User-Agent, or persistent device identifier","Per-connection identifiers","Mongo document contents"],"access_logs":"The hardened deployment disables uvicorn access logs and configures Caddy to discard request logs. This response reports runtime attestation separately; hosting, SSH, firewall, database-provider, and certificate-authority logs are outside the application log policy.","on_disk_persistence":"Not attested by this response. The repository contains hardened journald, Caddy, uvicorn, and core-dump settings, but operators must verify the running host separately.","runtime_hardening_attested":false,"log_retention_days":null,"note":"This section covers relay application logging, not infrastructure-provider records. Self-hosted operators control their host logging; see backend/deploy/ for the hardened systemd unit and journald/Caddy configuration."},"third_parties":["Your hosting provider (e.g. AWS Lightsail) sees TLS connections and volume timing.","The operator-selected MongoDB provider stores relay ciphertext and routing/account metadata and may retain its own operational or access records.","Let's Encrypt or your CA sees the public hostname for certificate issuance.","Tor directory authorities and onion-routing relays are contacted by the bundled Tor runtime inside the official Android client when the user enables Tor mode. The relay itself does not contact them."],"no_third_parties":["No analytics SDK in the reference relay application code","No crash-reporting SDK in the reference relay application code","No advertising SDK or network in the reference relay application code"],"subpoena_response_capability":"Operator-dependent: with valid legal process and access to live MongoDB backups, the server may produce ciphertext blobs and routing metadata retained within TTL windows (queued direct/group rows kept at most ~7 days unless acknowledged sooner; encrypted files; group member public keys and wrapped key blobs). The relay cannot derive plaintext from ciphertext. Sealed-send rows lack sender identity at rest.","code_audit_invitation":"Source is published at https://github.com/privacyportalUK/encrytedpp. Independent audits welcome — contact: security@privacyportal.co.uk"}