Pinwheel FreeChat is a floating group-chat widget you can drop into any PHP site. Client-generated ML-KEM-1024 (post-quantum) keypairs, a group secret only members ever see unwrapped, and an AES-256-GCM message ratchet that rotates every 20 seconds — the server relays ciphertext it structurally cannot read.
One CSS file, one JS file, one small PHP API. It builds its own UI, handles its own auth, and runs its own key exchange — no framework, no build step, no external service.
A stylesheet and a script tag. The widget injects its own floating panel into the page on load — nothing else to wire up.
The server never holds the group secret or any client's private key. It relays opaque AES-GCM ciphertext, full stop.
Username + PIN accounts, or a one-tap throwaway guest identity — both run through the exact same encrypted room.
Four tables, one schema.sql. No message queue, no Redis, no third-party chat backend to stand up.
Point two different sites' configs at the same database and they share one live room automatically. Same accounts, same messages.
Messages render as small rasterized PNGs client-side, not plain text — a deliberate property carried through from the original build.
CSRF, rate limiting, and IDOR checks on every session-scoped endpoint — including a fix for IP-spoofing that used to bypass rate limits entirely, and self-healing when a cached CSRF token goes stale.
Layer 1 is the real confidentiality boundary. Layer 2 is optional and explicitly server-visible — it exists to let the server itself verify a PIN, something pure client-side E2EE structurally cannot do.
The first member of a room generates a random 256-bit group secret entirely client-side. As each new member joins, an already-onboarded member encapsulates a fresh shared secret to the new member's ML-KEM-1024 public key (FIPS 203, the NIST-standardized successor to CRYSTALS-Kyber), then AES-256-GCM-wraps a copy of the group secret under a key derived from that shared secret. The server stores and forwards these wrapped blobs — it never sees the secret itself, wrapped or not, and the exchange stays secure even against a future quantum computer, unlike RSA or elliptic-curve key exchange.
The group secret derives a fresh "epoch key" every 20 seconds, and each epoch derives a unique per-message key. Every message is AES-256-GCM encrypted under a key that's used exactly once. A server authenticity tag (HMAC, rotated per poll) lets clients verify a response really came from the real server — without giving the server anything it could use to read the message.
If a user sets a local PIN, it's RSA-encrypted to a per-session server keypair and used to derive a one-time "secureId" key that re-encrypts the already-encrypted message again before storage. This is explicitly not end-to-end — the server sees the PIN in the clear by design, which is what lets it verify a PIN belongs to a specific sender on request. Entirely optional, per message, off by default.
No build tooling, no package manager. Copy files, fill in four config values, run one SQL file.
Drop chat/ and freechat/ at the same directory level as the page(s) that should show the widget.
Edit chat/config.php — fill in DB_HOST, DB_NAME, DB_USER, DB_PASS.
mysql -u root -p your_db_name < chat/schema.sql
Every statement is CREATE TABLE IF NOT EXISTS — safe to re-run.
<link rel="stylesheet" href="freechat/freechat.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"> ... <script src="freechat/js/freechat-integration.js"></script>
That's it. The widget builds its own floating button and panel — no other markup needed.
crypto.subtle (Web Crypto) only exists in a secure context — https://, or
http:// on localhost/127.0.0.1 for local dev. On a plain
http:// LAN address the widget shows a clear setup error instead of failing silently.
freechat/ freechat.css # widget styling js/freechat-integration.js # self-injecting widget js/vendor/mlkem/ # crystals-kyber-js v2.7.0 (ML-KEM-1024), vendored chat/ config.php # DB credentials + CSRF/rate-limit helpers schema.sql # the 4 tables (+ rate_limits) lib/ secure_chat.php # server-side Pinwheel protocol session.php # shared PHP-session auth helpers api/ register.php login.php guest.php logout.php me.php secure_chat_create_session.php secure_chat_join.php secure_chat_get_members.php secure_chat_distribute_secret.php secure_chat_get_wrapped_secret.php secure_chat_send.php secure_chat_poll.php secure_chat_unwrap_secure_layer.php secure_chat_retire_session.php