on eeea40e help sign in
skrepka/PROTOCOL.md
680 lines 23 kB history raw
1# Skrepka Protocol Specification
3**Version:** 0.1 (Draft)
5---
7## 1. Overview
9Skrepka is a decentralized, end-to-end encrypted messaging protocol. It has no user accounts, no home servers, and no central authority.
11### Design Goals
13- **No accounts.** A user's identity is their cryptographic keypair. No registration, no passwords, no servers storing credentials.
14- **No home server.** Users connect to any server in the mesh and can switch freely. Servers are disposable relays.
15- **End-to-end encryption.** Servers see only envelope metadata. Message content is opaque.
16- **Open federation.** Any server can join the mesh. Operators may optionally blocklist specific peers.
17- **Ephemeral.** All messages expire after 30 days — on servers and on devices. No permanent history.
18- **Simplicity.** Plain HTTP transport, no session negotiation, no ratchets.
20### Non-Goals
22- Forward secrecy (per-message ephemeral keys provide partial protection, but no ratchet)
23- Metadata privacy (servers see sender, recipient, and timestamp)
24- Durable message storage (servers queue temporarily, not permanently)
25- Peer-to-peer delivery (at least one server must be reachable)
27---
29## 2. Identity
31### Key Generation
33On first launch, a client generates an **Ed25519 keypair** (the identity key). The corresponding **X25519 public key** for key agreement is derived from the Ed25519 key using the birational map between the two curves (RFC 7748 / RFC 8032). This means a single keypair serves both signing and encryption — only the Ed25519 key needs to be stored and shared.
35The user's **public key is their identity**. There are no usernames, no global namespace, and no registration step.
37Each device generates its own independent keypair and is treated as a separate identity.
39### Key Format
41Public keys are encoded as **base64url** (RFC 4648 §5, no padding).
43### Identity URI
45```
46skrepka://<base64url(ed25519_pubkey)>@<server_host>
47```
49Example:
51```
52skrepka://dGhpcyBpcyBhIHRlc3Q@relay.example.com
53```
55The URI contains everything needed to reach someone:
57- **Public key** — who they are
58- **Server host** — a server where they were last seen or any known relay
60Since there is no home server, the server component is a hint, not an authority. The recipient may be connected to a different server by the time a message is sent. The gossip layer resolves current location.
62Identity URIs are designed to be shared out-of-band: copied as text, embedded in links, or scanned as QR codes.
64### Trust Model
66**Trust-on-First-Use (TOFU).** The first time a client encounters a public key, it is accepted and cached. Optional out-of-band verification (e.g., comparing fingerprints in person) is supported but not required by the protocol.
68### Contacts
70There is no global directory. Users assign local nicknames to public keys on their own device.
72---
74## 3. Cryptography
76### Algorithms
78| Purpose | Algorithm |
79|------------------|-----------------------|
80| Signing | Ed25519 |
81| Key agreement | X25519 |
82| Key derivation | HKDF-SHA256 |
83| Symmetric cipher | XChaCha20-Poly1305 |
85### Per-Message Encryption
87Each message is independently encrypted. There are no sessions, no ratchet, and no state between sender and recipient.
89**Encryption flow:**
911. Generate an ephemeral X25519 keypair:
92 ```
93 ephemeral_private, ephemeral_public = x25519_generate()
94 ```
952. Compute the raw shared secret using the ephemeral private key and the recipient's long-term X25519 public key:
96 ```
97 raw_secret = x25519(ephemeral_private, recipient_x25519_public)
98 ```
993. Derive the encryption key using HKDF-SHA256:
100 ```
101 key = hkdf_sha256(
102 ikm = raw_secret,
103 salt = ephemeral_public || recipient_x25519_public,
104 info = "skrepka-v1",
105 len = 32
106 )
107 ```
1084. Generate a random 24-byte nonce:
109 ```
110 nonce = random_bytes(24)
111 ```
1125. Encrypt the plaintext message:
113 ```
114 ciphertext = xchacha20_poly1305_encrypt(key, nonce, plaintext)
115 ```
1166. Sign the ciphertext with the sender's Ed25519 private key:
117 ```
118 signature = ed25519_sign(sender_ed25519_private, ciphertext)
119 ```
121**Decryption flow:**
1231. Verify the signature against the sender's Ed25519 public key.
1242. Compute the raw shared secret using the recipient's long-term X25519 private key and the ephemeral public key from the message.
1253. Derive the encryption key using HKDF-SHA256 with the same parameters.
1264. Decrypt the ciphertext using the derived key and nonce.
128### Encrypted Blob Format
130```
131ephemeral_x25519_pubkey (32 bytes)
132nonce (24 bytes)
133ciphertext (variable)
134sender_ed25519_pubkey (32 bytes)
135signature (64 bytes)
136```
138Total overhead: 152 bytes per message.
140---
142## 4. Message Format
144### Envelope
146The envelope is what servers see. It is transmitted in cleartext (not encrypted) so servers can route messages.
148```json
150 "to": "<base64url(recipient_ed25519_pubkey)>",
151 "from": "<base64url(sender_ed25519_pubkey)>",
152 "encrypted_blob": "<base64url(encrypted_blob)>",
153 "timestamp": 1679000000
155```
157| Field | Type | Description |
158|------------------|--------|--------------------------------------------------|
159| `to` | string | Recipient's Ed25519 public key (base64url) |
160| `from` | string | Sender's Ed25519 public key (base64url) |
161| `encrypted_blob` | string | The encrypted blob from §3 (base64url) |
162| `timestamp` | number | Unix timestamp (seconds) when the message was sent |
164Servers route based on `to` and `from`. They never inspect `encrypted_blob`.
166### Plaintext Payload
168After decryption, the plaintext is a JSON object with a `type` field:
170```json
172 "type": "<message_type>",
173 "id": "<unique_message_id>",
174 "timestamp": 1679000000,
175 ...
177```
179The `id` field is a client-generated UUID v4. Clients MUST deduplicate incoming messages by `id` — the same message may arrive more than once due to presence fan-out (see §7).
181Clients MUST silently ignore message types they do not recognize. This allows newer clients to introduce new types without breaking older ones.
183### Message Types
185| Type | Description | Additional Fields |
186|-------------------|------------------------------------------|-------------------------------------------------|
187| `text` | A text message | `body` (string) |
188| `media` | An inline media attachment | `media_type` (MIME), `filename`, `data` (base64)|
189| `group.invite` | Invite to a group (see §8) | `group_id`, `group_name`, `members` |
190| `group.leave` | Leave a group | `group_id` |
191| `group.update` | Update group metadata | `group_id`, `group_name`, `members` |
192| `delivery.ack` | Delivery acknowledgement | `ack_ids` (array of message IDs) |
193| `read` | Read receipt | `read_ids` (array of message IDs) |
194| `profile` | Share display name and/or photo | `display_name` (string), `photo` (base64, optional) |
196Media files are embedded inline in the encrypted payload as base64-encoded `data`. There is no separate media hosting or upload endpoint.
198### Profiles
200Users can share a display name and photo with contacts by sending a `profile` message. This is sent as a regular encrypted message — servers never see profile data.
202```json
204 "type": "profile",
205 "id": "msg_456",
206 "timestamp": 1679000000,
207 "display_name": "Alice",
208 "photo": "<base64(jpeg_data)>"
210```
212Clients SHOULD send a `profile` message to a contact on first interaction and whenever the user updates their name or photo. The recipient's client caches the profile locally. The `photo` field is optional and MAY be omitted to update only the display name.
214### Blocking
216Blocking is client-side only. A blocked sender's messages are still received and decrypted but are not displayed to the user. The protocol does not provide a mechanism to tell a server to reject messages from a specific pubkey.
218---
220## 5. Transport
222All communication uses **plain HTTP**.
224| Direction | Mechanism |
225|--------------------|----------------------|
226| Client → Server | HTTP POST |
227| Server → Client | Long polling |
228| Server → Server | HTTP POST |
230**Why HTTP + long polling:**
231- No special protocols. Works through proxies, load balancers, and CDNs.
232- Long polling provides near-real-time delivery over plain HTTP request/response cycles.
233- No persistent connections, no WebSocket upgrade, no gRPC toolchain, no raw TCP.
235### Server Discovery
237New clients discover servers via a well-known URL:
239```
240https://skrepka.org/servers.json
241```
243The response is a JSON array of server hostnames:
245```json
247 "servers": [
248 { "host": "relay1.example.com", "region": "eu" },
249 { "host": "relay2.example.com", "region": "us" },
250 { "host": "relay3.example.com", "region": "ap" }
251 ]
253```
255The client picks a server from the list (e.g., by latency or region preference) and connects. This list is a bootstrap mechanism only — once connected, the client may learn about other servers through the mesh.
257### Content Type
259All request and response bodies use `application/json`.
261### Message Size Limit
263The maximum size of an encrypted blob is **20 MB**. Servers MUST reject messages exceeding this limit. This applies to the entire encrypted payload, including inline media.
265---
267## 6. Client ↔ Server Protocol
269### Authentication (Challenge-Response)
271When a client connects, it proves ownership of its keypair:
2731. Client sends its public key:
274 ```
275 POST /auth/challenge
276 { "pubkey": "<base64url(ed25519_pubkey)>" }
277 ```
2792. Server responds with a random challenge (valid for 60 seconds):
280 ```json
281 { "challenge": "<base64url(random_32_bytes)>" }
282 ```
2843. Client signs the challenge and sends it back:
285 ```
286 POST /auth/verify
287 {
288 "pubkey": "<base64url(ed25519_pubkey)>",
289 "challenge": "<base64url(challenge)>",
290 "signature": "<base64url(ed25519_sign(private_key, challenge))>"
291 }
292 ```
2944. Server verifies the signature. On success, returns a session token valid for **1 hour**:
295 ```json
296 { "token": "<session_token>", "expires_at": 1679003600 }
297 ```
299The session token is included in all subsequent requests as a bearer token:
300```
301Authorization: Bearer <session_token>
302```
304When a token expires, the server responds with `401`. The client re-authenticates by repeating the challenge-response flow. Servers SHOULD broadcast an `offline` gossip event only if the client does not re-authenticate within a grace period (e.g., 60 seconds).
306### Endpoints
308#### `POST /auth/challenge` — Request auth challenge
310**Request:**
311```json
312{ "pubkey": "<base64url(ed25519_pubkey)>" }
313```
315**Response:**
316```json
317{ "challenge": "<base64url(random_32_bytes)>" }
318```
320#### `POST /auth/verify` — Complete auth
322**Request:**
323```json
325 "pubkey": "<base64url(ed25519_pubkey)>",
326 "challenge": "<base64url(challenge)>",
327 "signature": "<base64url(signature)>"
329```
331**Response:**
332```json
333{ "token": "<session_token>", "expires_at": 1679003600 }
334```
336### Error Responses
338All endpoints return errors in a consistent format:
340```json
342 "error": "<error_code>",
343 "message": "<human-readable description>"
345```
347| HTTP Status | Error Code | Meaning |
348|-------------|----------------------|--------------------------------------|
349| 400 | `bad_request` | Malformed request body |
350| 401 | `unauthorized` | Missing, invalid, or expired token |
351| 403 | `challenge_expired` | Auth challenge has expired (>60s) |
352| 404 | `not_found` | Unknown pubkey or resource |
353| 413 | `payload_too_large` | Encrypted blob exceeds 20 MB |
354| 429 | `rate_limited` | Too many requests |
355| 500 | `internal_error` | Server error |
357#### `GET /poll` — Long poll for new events
359The client makes a blocking GET request. The server holds the connection open until new events are available or a timeout is reached.
361**Headers:** `Authorization: Bearer <token>`
363**Query parameters:**
365| Parameter | Type | Description |
366|--------------|--------|--------------------------------------------------|
367| `timeout` | number | Max seconds to wait (default: 30, max: 60) |
368| `after` | string | Cursor from the previous poll response (optional) |
370**Response:**
371```json
373 "events": [
374 {
375 "type": "message",
376 "data": { "to": "...", "from": "...", "encrypted_blob": "...", "timestamp": 1679000000 },
377 "id": "evt_001"
378 },
379 {
380 "type": "ack",
381 "data": { "message_ids": ["msg_abc123"] },
382 "id": "evt_002"
383 }
384 ],
385 "cursor": "evt_002"
387```
389- If events are available immediately, the server responds right away.
390- If no events are available, the server holds the connection until one arrives or `timeout` expires.
391- On timeout with no events, the server returns `{ "events": [], "cursor": "..." }`.
392- The client should immediately issue the next poll after receiving a response.
394**Event types:**
396| Type | Data |
397|------------|-------------------------------------|
398| `message` | Envelope JSON (see §4) |
399| `ack` | `{ "message_ids": ["id1", "id2"] }`|
401#### `POST /messages` — Send a message
403**Headers:** `Authorization: Bearer <token>`
405**Request:**
406```json
408 "to": "<base64url(recipient_ed25519_pubkey)>",
409 "encrypted_blob": "<base64url(encrypted_blob)>",
410 "timestamp": 1679000000
412```
414The server fills in `from` from the authenticated session.
416**Response:**
417```json
419 "status": "delivered" | "queued",
420 "message_id": "<server_assigned_id>"
422```
424- `delivered` — recipient is online, message was forwarded.
425- `queued` — recipient is offline, message is queued for later delivery.
427#### `POST /messages/ack` — Acknowledge receipt
429**Headers:** `Authorization: Bearer <token>`
431**Request:**
432```json
433{ "message_ids": ["id1", "id2"] }
434```
436**Response:**
437```json
438{ "ok": true }
439```
441The server deletes acknowledged messages from its queue and forwards the ACK to the sender's server.
443#### `GET /lookup/:pubkey` — Look up a user's presence
445**Response (online):**
446```json
448 "online": true,
449 "server": "other-server.example.com"
451```
453**Response (offline):**
454```json
456 "online": false
458```
460---
462## 7. Server ↔ Server Protocol
464### Mesh Formation
466Federation is open. A server joins the mesh by connecting to one or more known peers. No approval is required.
468Server operators configure a list of initial peers. When a server starts, it announces itself to its peers, which propagate the announcement. Over time, each server builds a view of the full mesh.
470Operators may maintain an optional **blocklist** of server hostnames or IPs to refuse connections from.
472### Gossip (Presence Announcements)
474When a user connects to or disconnects from a server, the server broadcasts a presence update to all known peers:
476**Online:**
477```json
479 "type": "online",
480 "pubkey": "<base64url(ed25519_pubkey)>",
481 "server": "this-server.example.com",
482 "timestamp": 1679000000,
483 "ttl": 3600
485```
487**Offline:**
488```json
490 "type": "offline",
491 "pubkey": "<base64url(ed25519_pubkey)>",
492 "server": "this-server.example.com",
493 "timestamp": 1679000000
495```
497Each server maintains a **presence table** — a mapping of public keys to a **set of servers** where they are currently connected. A single pubkey may appear at multiple servers simultaneously (e.g., multiple devices with different keypairs connecting independently, or stale gossip). Presence entries expire after `ttl` seconds if not refreshed.
499Gossip messages are forwarded to peers with a decrementing hop counter to prevent infinite propagation.
501### Gossip Endpoint
503```
504POST /federation/gossip
505```
507**Request:**
508```json
510 "events": [
511 { "type": "online", "pubkey": "...", "server": "...", "timestamp": ..., "ttl": ... },
512 { "type": "offline", "pubkey": "...", "server": "...", "timestamp": ... }
513 ],
514 "hops_remaining": 3
516```
518### Message Forwarding
520When a server receives a message for a recipient, it checks the presence table. If the pubkey appears at multiple servers, the message is forwarded to **all** of them. This ensures delivery even with stale gossip or multi-server presence.
522```
523POST /federation/forward
524```
526**Request:**
527```json
529 "to": "<base64url(recipient_ed25519_pubkey)>",
530 "from": "<base64url(sender_ed25519_pubkey)>",
531 "encrypted_blob": "<base64url(encrypted_blob)>",
532 "timestamp": 1679000000,
533 "origin_server": "sender-server.example.com",
534 "message_id": "<id>"
536```
538The receiving server delivers the message to the connected client on its next poll.
540### ACK Forwarding
542When a recipient ACKs a message, the ACK is forwarded back to the origin server so it can delete the queued blob:
544```
545POST /federation/ack
546```
548**Request:**
549```json
551 "message_ids": ["id1", "id2"],
552 "origin_server": "sender-server.example.com"
554```
556---
558## 8. Group Messaging
560Groups are a **client-side concept**. Servers are entirely unaware of groups — they see only individual messages between pairs of public keys.
562### Group Structure
564A group is defined locally on each member's device:
566```json
568 "group_id": "<random_uuid>",
569 "group_name": "Weekend Plans",
570 "members": [
571 "<base64url(pubkey_alice)>",
572 "<base64url(pubkey_bob)>",
573 "<base64url(pubkey_carol)>"
574 ]
576```
578### Sending to a Group
580When a user sends a message to a group, the client **fans out** — it encrypts and sends the message individually to each group member. Each fan-out message includes the `group_id` in the plaintext payload so the recipient's client can display it in the correct conversation:
582```json
584 "type": "text",
585 "id": "msg_123",
586 "timestamp": 1679000000,
587 "group_id": "550e8400-e29b-41d4-a716-446655440000",
588 "body": "See you Saturday!"
590```
592From the server's perspective, a group message of N members is N separate 1:1 messages.
594### Membership Management
596- **Invite:** Send a `group.invite` message to the new member containing the group ID, name, and member list.
597- **Leave:** Send a `group.leave` message to all remaining members.
598- **Update:** Send a `group.update` message to all members when the name or membership changes.
600All membership operations are performed by the client. There is no server-side group state, no group admin role enforced by the protocol, and no consensus mechanism. Clients trust the membership updates they receive from other members.
602---
604## 9. Offline Delivery
606### Queuing
608When a server receives a message for a recipient who is not currently online (not in the presence table), the **sender's server queues** the encrypted blob.
610The sender's server continues to listen to gossip. When a presence announcement indicates the recipient is online at some server, the sender's server forwards the queued message to that server.
612### TTL
614All messages have a **30-day TTL**, enforced in two places:
616- **Server-side:** Queued messages are deleted 30 days after their timestamp. Servers are temporary relays, not long-term storage.
617- **Client-side:** Clients MUST delete messages from local storage 30 days after their timestamp. There is no permanent message history.
619### Delivery Flow
621```
6221. Alice sends a message to Bob (offline).
6232. Alice's server queues the encrypted blob.
624 Returns: { "status": "queued" }
6263. ...time passes...
6284. Bob connects to Server C.
6295. Server C gossips: ONLINE { bob_pubkey, server_c }
6306. Alice's server sees the gossip, forwards the queued message to Server C.
6317. Server C delivers to Bob on his next poll.
6328. Bob's client sends ACK to Server C.
6339. Server C forwards ACK to Alice's server.
63410. Alice's server deletes the blob.
63511. Next time Alice connects, she receives delivery confirmation.
636```
638Alice does not need to stay online. Her server handles delivery independently.
640### Failure Modes
642- If the sender's server goes down before delivering, queued messages are **lost**. This is an accepted tradeoff — servers are disposable.
643- If the recipient never comes back online within 30 days, the message is deleted.
644- The sender sees `queued` status until ACK is received, at which point it becomes `delivered`.
646---
648## 10. Security Considerations
650### Threat Model
652Skrepka protects message **content** from servers and network observers. It does not attempt to hide **metadata**.
654### What Is Protected
656| Property | Mechanism |
657|-----------------------|----------------------------------------------|
658| Message confidentiality | E2E encryption (X25519 + HKDF-SHA256 + XChaCha20-Poly1305) |
659| Sender authenticity | Ed25519 signature on every message |
660| Replay protection | Unique nonce per message |
661| Key compromise (partial) | Ephemeral keys per message — past messages whose blobs were already discarded cannot be decrypted even if the recipient's long-term key is compromised |
663### What Is NOT Protected
665| Risk | Details |
666|------------------------------|-----------------------------------------------------|
667| **No forward secrecy** | If a message blob is captured *and* the recipient's long-term X25519 key is later compromised, that specific message can be decrypted. No ratchet means no post-compromise recovery. |
668| **Metadata exposure** | Servers see `{to, from, timestamp}` in the envelope. They know who is talking to whom and when. Message size is also visible. |
669| **Presence gossip leaks location** | All servers in the mesh learn which server a user is connected to. |
670| **No durable delivery** | If the sender's server goes down, queued messages are lost. |
671| **No server authentication** | Servers are not authenticated to each other by default. A malicious server could join the mesh and observe gossip. |
672| **Group membership visible to members** | Group member lists are shared in plaintext among members. A compromised member reveals the full group. |
674### Recommendations for Implementers
676- Store private keys in platform-secure storage (Keychain, Keystore, etc.).
677- Warn users when a contact's public key changes (TOFU violation).
678- Consider pinning server TLS certificates for additional transport security.
679- Implement rate limiting on server endpoints to mitigate spam.
680- Servers SHOULD delete message blobs immediately after successful delivery and ACK.