End-to-end encrypted P2P chat, right in your browser. No server, no accounts, no stored history.
by Hardlint Cybersecurity Team
This project is distributed for educational and security research purposes. It does not guarantee network-level anonymity: it protects the content of conversations, not necessarily who is connecting. Read the Attack Surface and Known Limitations section before using it for sensitive communications.
Use of a trustworthy VPN on both devices is strongly recommended.
[1] INITIALIZE ROOM β copy the generated Room Key[2] CONNECT TO ROOM β paste the received Room KeyZero-Trace Terminal is a static web application (HTML/CSS/JS, no proprietary backend) that allows two devices to establish a direct peer-to-peer connection via WebRTC, exchanging end-to-end encrypted text messages.
Main components:
| Component | Role | Technology |
|---|---|---|
| User interface | Retro terminal UI | Plain HTML/CSS |
| Signaling | Makes the two peers "find" each other | PeerJS (public cloud broker) |
| Data transport | Encrypted P2P channel | WebRTC DataChannel |
| NAT traversal | Punching through firewalls/NAT | STUN + TURN (ICE) |
| Encryption | Message content protection | AES-GCM 256-bit + PBKDF2 |
| Hosting | Code distribution | GitHub Pages (static) |
There is no proprietary application server: the code runs entirely in each user's browser. The only external infrastructure involved is used to "introduce" the two devices to each other (signaling) and, if needed, to relay traffic when a direct connection isn't possible (TURN).
When a user clicks "INITIALIZE ROOM (HOST)":
generate100CharCode()), using crypto.getRandomValues() β a cryptographically secure random number generator (not Math.random(), which is unsuitable for cryptographic purposes).-_!@#$%^&*), maximizing entropy within 100 characters.This string (the Room Key) is the only shared secret the two parties need to exchange, out-of-band (e.g. voice message, in person, another encrypted channel).
Two independent values, each with a different purpose, are derived from the Room Key:
A. Message encryption key (PBKDF2 β AES-GCM)
PBKDF2(
password = Room Key,
salt = "p2p-zero-trace-salt-v1" (fixed, hardcoded),
iterations = 100,000,
hash = SHA-256
) β 256-bit AES-GCM key
B. PeerJS identifier (truncated SHA-256)
SHA-256(Room Key) β first 32 hex characters, prefixed with "ztt-"
This ID is used solely so that Host and Guest can "find" each other on the PeerJS signaling broker, without exchanging anything beyond the Room Key. It plays no cryptographic role.
Note on the fixed salt: the PBKDF2 salt is hardcoded and identical across all sessions. This is acceptable because the "password" (Room Key) already has very high entropy (100 random characters) β a fixed salt only weakens security in scenarios involving weak, reused passwords, which does not apply here.
Peer object, registering with the public PeerJS cloud broker using the ID derived from the Room Key.peer.connect(id).Once the two Peers have "introduced" themselves, WebRTC starts ICE negotiation to find a valid network path:
Configured ICE servers (in priority order):
stun.relay.metered.ca / global.relay.metered.caThe browser automatically tries every combination and selects the first one that establishes a working channel (standard ICE algorithm, handled internally by WebRTC).
Peer and DataConnection are destroyed (peer.destroy(), conn.close())[EXPIRED] Room Key no longer validEvery message is individually encrypted before being sent over the DataChannel:
1. Generate a random 12-byte IV (crypto.getRandomValues)
2. ciphertext = AES-GCM-Encrypt(key, IV, plaintext)
3. payload = IV || ciphertext (concatenated, IV in plaintext at the front)
4. Send payload as a Uint8Array via conn.send()
On receipt:
1. Extract the first 12 bytes as the IV
2. The rest is the ciphertext (includes the 16-byte GCM authentication tag at the end)
3. plaintext = AES-GCM-Decrypt(key, IV, ciphertext)
Security properties guaranteed by AES-GCM:
[ERR: DECRYPTION_FAILED]), rather than silently producing corrupted outputWhat this scheme does NOT cover:
| Data | Persistence | Notes |
|---|---|---|
| Room Key | None | Only in a JS variable, gone on close/reload |
| Derived AES-GCM key | None | Same, never written to disk |
| Chat messages | None | Only live in the DOM/RAM, no localStorage/IndexedDB |
| Cookies | None | The project uses none at all |
| Application logs | Local DevTools console only | Never sent anywhere, gone when the tab closes |
The "PANIC: PURGE SESSION" button explicitly forces:
PeerConnection/DataConnectionKey point to understand: encryption protects content, not connection metadata.
| Service | What it can see | What it CANNOT see |
|---|---|---|
| GitHub Pages | IP and timestamp of whoever loads the page | Message content, Room Key |
| PeerJS broker (public cloud) | IP of Host and Guest, when they connect, their Peer ID (a hash of the Room Key, not the Key itself) | Message content |
| Metered.ca TURN (if used as relay) | Source/destination IP, ports, amount of data transferred | Message content (already travels encrypted) |
| Each user's ISP | That a connection is being made to github.io / metered.ca / a PeerJS server | Message content |
Recommended mitigation (outside the code): use a trustworthy VPN (e.g. Mullvad, with an anonymously created account) on both devices, to avoid exposing real IP addresses to these third-party services. The project displays an explicit warning to this effect on the splash screen.
| Risk | Description | Mitigated? |
|---|---|---|
| Message content interception | MITM on TURN/network traffic | β Yes β end-to-end AES-GCM |
| Message tampering in transit | Packet manipulation | β Yes β GCM authentication tag |
| Deanonymization via IP metadata | IPβidentity correlation through third-party logs | β οΈ Partial β requires a VPN client-side, not solved by the code itself |
| Metered.ca account compromise | TURN credentials are in the public code (base64-obfuscated, not encrypted) | β οΈ Minimal deterrent, not real security |
| Dependency on third-party services | GitHub Pages, PeerJS broker, Metered TURN β if suspended, the app stops working | β οΈ Not mitigated (would require full self-hosting) |
| Room Key reuse | Would compromise forward secrecy across sessions that reuse it | β Not applicable in normal flow (a new Key every session) |
crypto.subtle) β PBKDF2, AES-GCM, SHA-256file:// or content://Hard-Chat is 100% free, open-source, and maintained by the Hardlint Cybersecurity Team. We don't run ads and we don't sell data. If you believe in our mission and want to help us fund our future self-hosted infrastructure (custom STUN/TURN servers), consider supporting us!
Solana (SOL) donation address:
GSsqZCtDC7rf53U6gC5cJ4weAYYT9g7twxz9t15mfRDV
This software is provided "as is", without warranties of any kind. The developers are not responsible for any improper or illegal use of this tool. Users are solely responsible for complying with applicable laws in their jurisdiction.
This document is provided for informational and technical documentation purposes only. It does not constitute legal advice regarding regulatory compliance, privacy, or liability for use.
Hardlint Cybersecurity Team
32 commits
HTML
100.0%
End-to-end encrypted P2P chat, right in your browser. No server, no accounts, no stored history.
by Hardlint Cybersecurity Team
This project is distributed for educational and security research purposes. It does not guarantee network-level anonymity: it protects the content of conversations, not necessarily who is connecting. Read the Attack Surface and Known Limitations section before using it for sensitive communications.
Use of a trustworthy VPN on both devices is strongly recommended.
[1] INITIALIZE ROOM β copy the generated Room Key[2] CONNECT TO ROOM β paste the received Room KeyZero-Trace Terminal is a static web application (HTML/CSS/JS, no proprietary backend) that allows two devices to establish a direct peer-to-peer connection via WebRTC, exchanging end-to-end encrypted text messages.
Main components:
| Component | Role | Technology |
|---|---|---|
| User interface | Retro terminal UI | Plain HTML/CSS |
| Signaling | Makes the two peers "find" each other | PeerJS (public cloud broker) |
| Data transport | Encrypted P2P channel | WebRTC DataChannel |
| NAT traversal | Punching through firewalls/NAT | STUN + TURN (ICE) |
| Encryption | Message content protection | AES-GCM 256-bit + PBKDF2 |
| Hosting | Code distribution | GitHub Pages (static) |
There is no proprietary application server: the code runs entirely in each user's browser. The only external infrastructure involved is used to "introduce" the two devices to each other (signaling) and, if needed, to relay traffic when a direct connection isn't possible (TURN).
When a user clicks "INITIALIZE ROOM (HOST)":
generate100CharCode()), using crypto.getRandomValues() β a cryptographically secure random number generator (not Math.random(), which is unsuitable for cryptographic purposes).-_!@#$%^&*), maximizing entropy within 100 characters.This string (the Room Key) is the only shared secret the two parties need to exchange, out-of-band (e.g. voice message, in person, another encrypted channel).
Two independent values, each with a different purpose, are derived from the Room Key:
A. Message encryption key (PBKDF2 β AES-GCM)
PBKDF2(
password = Room Key,
salt = "p2p-zero-trace-salt-v1" (fixed, hardcoded),
iterations = 100,000,
hash = SHA-256
) β 256-bit AES-GCM key
B. PeerJS identifier (truncated SHA-256)
SHA-256(Room Key) β first 32 hex characters, prefixed with "ztt-"
This ID is used solely so that Host and Guest can "find" each other on the PeerJS signaling broker, without exchanging anything beyond the Room Key. It plays no cryptographic role.
Note on the fixed salt: the PBKDF2 salt is hardcoded and identical across all sessions. This is acceptable because the "password" (Room Key) already has very high entropy (100 random characters) β a fixed salt only weakens security in scenarios involving weak, reused passwords, which does not apply here.
Peer object, registering with the public PeerJS cloud broker using the ID derived from the Room Key.peer.connect(id).Once the two Peers have "introduced" themselves, WebRTC starts ICE negotiation to find a valid network path:
Configured ICE servers (in priority order):
stun.relay.metered.ca / global.relay.metered.caThe browser automatically tries every combination and selects the first one that establishes a working channel (standard ICE algorithm, handled internally by WebRTC).
Peer and DataConnection are destroyed (peer.destroy(), conn.close())[EXPIRED] Room Key no longer validEvery message is individually encrypted before being sent over the DataChannel:
1. Generate a random 12-byte IV (crypto.getRandomValues)
2. ciphertext = AES-GCM-Encrypt(key, IV, plaintext)
3. payload = IV || ciphertext (concatenated, IV in plaintext at the front)
4. Send payload as a Uint8Array via conn.send()
On receipt:
1. Extract the first 12 bytes as the IV
2. The rest is the ciphertext (includes the 16-byte GCM authentication tag at the end)
3. plaintext = AES-GCM-Decrypt(key, IV, ciphertext)
Security properties guaranteed by AES-GCM:
[ERR: DECRYPTION_FAILED]), rather than silently producing corrupted outputWhat this scheme does NOT cover:
| Data | Persistence | Notes |
|---|---|---|
| Room Key | None | Only in a JS variable, gone on close/reload |
| Derived AES-GCM key | None | Same, never written to disk |
| Chat messages | None | Only live in the DOM/RAM, no localStorage/IndexedDB |
| Cookies | None | The project uses none at all |
| Application logs | Local DevTools console only | Never sent anywhere, gone when the tab closes |
The "PANIC: PURGE SESSION" button explicitly forces:
PeerConnection/DataConnectionKey point to understand: encryption protects content, not connection metadata.
| Service | What it can see | What it CANNOT see |
|---|---|---|
| GitHub Pages | IP and timestamp of whoever loads the page | Message content, Room Key |
| PeerJS broker (public cloud) | IP of Host and Guest, when they connect, their Peer ID (a hash of the Room Key, not the Key itself) | Message content |
| Metered.ca TURN (if used as relay) | Source/destination IP, ports, amount of data transferred | Message content (already travels encrypted) |
| Each user's ISP | That a connection is being made to github.io / metered.ca / a PeerJS server | Message content |
Recommended mitigation (outside the code): use a trustworthy VPN (e.g. Mullvad, with an anonymously created account) on both devices, to avoid exposing real IP addresses to these third-party services. The project displays an explicit warning to this effect on the splash screen.
| Risk | Description | Mitigated? |
|---|---|---|
| Message content interception | MITM on TURN/network traffic | β Yes β end-to-end AES-GCM |
| Message tampering in transit | Packet manipulation | β Yes β GCM authentication tag |
| Deanonymization via IP metadata | IPβidentity correlation through third-party logs | β οΈ Partial β requires a VPN client-side, not solved by the code itself |
| Metered.ca account compromise | TURN credentials are in the public code (base64-obfuscated, not encrypted) | β οΈ Minimal deterrent, not real security |
| Dependency on third-party services | GitHub Pages, PeerJS broker, Metered TURN β if suspended, the app stops working | β οΈ Not mitigated (would require full self-hosting) |
| Room Key reuse | Would compromise forward secrecy across sessions that reuse it | β Not applicable in normal flow (a new Key every session) |
crypto.subtle) β PBKDF2, AES-GCM, SHA-256file:// or content://Hard-Chat is 100% free, open-source, and maintained by the Hardlint Cybersecurity Team. We don't run ads and we don't sell data. If you believe in our mission and want to help us fund our future self-hosted infrastructure (custom STUN/TURN servers), consider supporting us!
Solana (SOL) donation address:
GSsqZCtDC7rf53U6gC5cJ4weAYYT9g7twxz9t15mfRDV
This software is provided "as is", without warranties of any kind. The developers are not responsible for any improper or illegal use of this tool. Users are solely responsible for complying with applicable laws in their jurisdiction.
This document is provided for informational and technical documentation purposes only. It does not constitute legal advice regarding regulatory compliance, privacy, or liability for use.
Hardlint Cybersecurity Team
32 commits
HTML
100.0%