# Willy Push E2EE – Technical Whitepaper

**Version:** 1.0  
**Status:** Formal protocol and key lifecycle documentation for security review.

---

## 1. Scope and Purpose

This document describes the end-to-end encryption (E2EE) protocol used in **Willy Push** for encrypted push notifications. It is intended for security auditors and implementers. No code is required to validate the mathematical and architectural robustness of the system.

**In scope:** Protocol definition, key lifecycle, storage model, threat model.  
**Out of scope:** Application business logic, transport (FCM), server-side non-E2EE flows.

---

## 2. Protocol Overview

The system uses **ephemeral ECDH (Elliptic Curve Diffie–Hellman)** plus **symmetric authenticated encryption**. It is **not** the Signal Double Ratchet; it is **not** RSA-based. Each message is encrypted with a **fresh ephemeral key pair** on the server; the client’s **long-term** key pair is used only to derive the same shared secret and decrypt.

| Component        | Choice              | Role |
|-----------------|---------------------|------|
| Key agreement    | ECDH on curve P-256 | Ephemeral (server) × static (client) → shared secret |
| Key derivation   | HKDF-SHA256         | Shared secret → 256-bit AES key |
| Encryption      | AES-256-GCM         | Authenticated encryption of payload |

---

## 3. Mathematical and Cryptographic Specification

### 3.1 Curves and Primitives

- **Curve:** NIST P-256 (secp256r1, prime256v1). Same as used in Web Crypto `ECDH` with `namedCurve: 'P-256'` and in OpenSSL `prime256v1`.
- **Hash for KDF:** SHA-256.
- **Symmetric cipher:** AES-256-GCM; 96-bit (12-byte) nonce, 128-bit (16-byte) authentication tag.

### 3.2 Roles

- **Client (device):** Holds a **long-term** ECDH key pair \((d_C, Q_C)\). Private key \(d_C\) never leaves the device; public key \(Q_C\) is sent to the server once at registration and stored there (per device).
- **Server:** For **each** message to a device, generates an **ephemeral** ECDH key pair \((d_S, Q_S)\), computes shared secret, derives AES key, encrypts payload, and sends ciphertext plus \(Q_S\) (as `enc` and `ephem`) to the delivery channel (e.g. FCM).

### 3.3 Per-Message Encryption (Server)

1. **Input:** Plaintext \(P\) (e.g. JSON: title, body, url, icon, badge, image, tag); client’s public key \(Q_C\) (SPKI, stored at registration).
2. **Ephemeral key:** Generate \((d_S, Q_S)\) on P-256.
3. **Shared secret:** \(Z = \mathrm{ECDH}(d_S, Q_C)\). Use the first 32 bytes of \(Z\) (raw shared point or derived bytes) as the ECDH output, consistent with 256-bit security.
4. **Key derivation:**  
   \(\mathit{AESkey} = \mathrm{HKDF\text{-}SHA256}(Z_{32}, \mathit{salt}=\varepsilon, \mathit{info}=\texttt{"WillyPush-E2EE-v1"}, 256)\).  
   Salt is empty; info is a fixed UTF-8 string; output is 32 bytes (256 bits).
5. **Encryption:** \(C = \mathrm{AES\text{-}256\text{-}GCM}(\mathit{AESkey}, N, P, T)\) with 12-byte nonce \(N\) (random), 16-byte tag \(T\).
6. **Output:** Send \(\mathit{enc} = N \| C \| T\) (base64url) and \(\mathit{ephem} = Q_S\) (SPKI, base64url). No long-term server secret is reused for encryption.

### 3.4 Per-Message Decryption (Client)

1. **Input:** \(\mathit{enc}\) (base64url), \(\mathit{ephem}\) (base64url); local private key \(d_C\) from secure storage.
2. **Parse:** Decode \(\mathit{enc}\) → \(N \| C \| T\) (12 + ciphertext + 16 bytes).
3. **Shared secret:** \(Z = \mathrm{ECDH}(d_C, Q_S)\) where \(Q_S\) is decoded from \(\mathit{ephem}\). Use first 32 bytes.
4. **Key derivation:** Same HKDF as server: \(\mathit{AESkey} = \mathrm{HKDF\text{-}SHA256}(Z_{32}, \mathit{salt}=\varepsilon, \mathit{info}=\texttt{"WillyPush-E2EE-v1"}, 256)\).
5. **Decryption:** \(P = \mathrm{AES\text{-}256\text{-}GCM\text{-}decrypt}(\mathit{AESkey}, N, C, T)\). Verify tag; if invalid, abort.
6. **Output:** Plaintext \(P\) (e.g. JSON). Decryption is performed **only on the device**; the server never has \(d_C\) and cannot decrypt.

### 3.5 Alignment Between Client and Server

- **ECDH output:** Both sides must use the same 32-byte value. In practice, the raw shared secret (or its first 32 bytes) is used consistently (e.g. Web Crypto `deriveBits` 256 bits; OpenSSL `PKEY_derive` then truncate to 32 bytes).
- **HKDF:** Same hash (SHA-256), salt (empty), info string (`WillyPush-E2EE-v1`), and output length (32 bytes).
- **AES-GCM:** Same nonce and tag lengths (12 and 16 bytes); \(\mathit{enc}\) format is exactly nonce ‖ ciphertext ‖ tag.

---

## 4. Key Lifecycle

### 4.1 Key Generation

- **Who:** Client (browser or native app).
- **When:** At “enable E2EE” / “generate keys” (e.g. first time or after key loss).
- **How:** Generate one ECDH P-256 key pair with a cryptographically secure RNG (e.g. `crypto.subtle.generateKey` in browser; equivalent in native). Mark key as non-extractable if the platform supports it; at minimum, private key must never be sent over the network or stored on the server.
- **Output:** Public key exported as SPKI (base64url or base64); private key stored only in the client’s secure storage (see 4.2).

### 4.2 Key Storage

- **Web (browser):**  
  - **Where:** IndexedDB, same-origin only (e.g. object store `keys`, key `devicePrivate`).  
  - **What:** Private key in PKCS#8 form (or equivalent); optionally a short **Key ID** (e.g. derived from public key or timestamp) for logging/audit only, not for cryptography.  
  - **Access:** Only scripts from the same origin and the same site’s service worker can access this storage. No server has access.

- **Native (mobile) – recommended for production:**  
  - **Android:** Store private key in **Android Keystore** (hardware-backed when available). Use KeyStore API; do not export raw private key to app storage.  
  - **iOS:** Store private key in **Secure Enclave** (or iOS Keychain with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` or stricter). Use Security framework; do not export to network or server.

- **Server:** Stores only **public keys** (and optional metadata such as device id, user_agent). Server never has access to any private key.

### 4.3 Initial “Exchange” (Registration)

There is **no** classical key “exchange” (no DH handshake with the server’s key). The flow is one-way:

1. Client generates \((d_C, Q_C)\) and stores \(d_C\) locally (see 4.2).
2. Client sends \(Q_C\) (and optionally a Key ID for audit) to the server over a protected channel (HTTPS), together with the push token and other registration data.
3. Server stores \(Q_C\) associated with that device/token. Server can later use \(Q_C\) to encrypt messages for that device; it cannot decrypt any message.

No server long-term private key is involved in this step. Forward secrecy for a **single** message is provided by the server’s **ephemeral** key per message; the server does not retain ephemeral secrets after sending.

### 4.4 Key Rotation and Revocation

- **Rotation:** If the client generates a new key pair, it must re-register the new public key with the server (and the server should replace the old public key for that device). Old messages encrypted to the old public key remain decryptable only by the old private key (if still present).
- **Revocation:** Removing the device (or its token) on the server invalidates future sends; it does not retroactively destroy keys on the device. Clearance of keys on device is an application policy (e.g. “log out” clears local key store).

---

## 5. Threat Model and Guarantees

### 5.1 Assumptions

- Client device is not persistently compromised during key generation and storage.
- Server and transport (e.g. FCM) are honest-but-curious or under control of the same organisation; they do not actively replace or forge ciphertext (integrity is assumed for the channel from server to device, in addition to AES-GCM authentication).
- RNG on client and server is cryptographically secure.

### 5.2 What the Protocol Protects

- **Confidentiality of payload:** Only the device holding \(d_C\) can derive the same AES key and decrypt. The server, FCM, and any intermediate cannot read the plaintext (title, body, url, etc.) without \(d_C\).
- **Integrity and authenticity of payload:** AES-GCM ensures that ciphertext is not modified without detection. Ephemeral \(Q_S\) is bound to the ciphertext; an attacker without \(d_C\) cannot produce a valid (enc, ephem) pair that decrypts to a chosen plaintext.

### 5.3 What Is Not Claimed

- **Metadata:** Who receives a message, when, and from which endpoint can be visible to the server and the push provider (e.g. FCM). The protocol does not hide “this device received an E2EE message at time T.”
- **Forward secrecy across many messages:** Each message uses a new ephemeral key on the server, but the same client long-term key. If \(d_C\) is later compromised, all past messages encrypted to \(Q_C\) can be decrypted by an attacker who has stored the (enc, ephem) pairs. True long-term forward secrecy would require a different design (e.g. ratcheting).
- **Deniability:** Not designed for deniability; the document does not analyse deniability.

---

## 6. Logical Architecture Summary

```
[Client]                          [Server]                    [FCM / Delivery]
   |                                   |                              |
   | 1. Generate (d_C, Q_C)            |                              |
   | 2. Store d_C (IndexedDB/          |                              |
   |    Keystore/Secure Enclave)        |                              |
   | 3. Send Q_C + token (HTTPS) ----->| 4. Store Q_C, token          |
   |                                   |                              |
   |                                   | 5. For each message:         |
   |                                   |    - Generate (d_S, Q_S)      |
   |                                   |    - Z = ECDH(d_S, Q_C)       |
   |                                   |    - K = HKDF(Z)              |
   |                                   |    - enc = AES-GCM(K, P)      |
   |                                   | 6. Send enc, ephem=Q_S ------>| 7. Deliver to device
   |                                   |                              |
   | 8. Receive enc, ephem <-------------------------------------------|
   | 9. Z = ECDH(d_C, Q_S), K = HKDF(Z)                               |
   | 10. P = AES-GCM-decrypt(K, enc)                                   |
   | 11. Show notification P (only on device)                          |
```

An expert can validate robustness by checking: (i) use of standard curves and algorithms (P-256, HKDF-SHA256, AES-256-GCM), (ii) no long-term server secret used for encryption, (iii) private key never leaves the device, (iv) one-way registration of public key and per-message ephemeral key on the server.

---

## 7. References

- NIST FIPS 186-4 (P-256).
- NIST SP 800-56A (ECDH).
- RFC 5869 (HKDF).
- NIST SP 800-38D (AES-GCM).
- Web Crypto API: ECDH, HKDF, AES-GCM (W3C / WHATWG).

---

*End of whitepaper.*
