mrhardlint/Hard-Chat

12

stars

32

commits

HTML

primary language

Sep 6, 2026

updated

README

πŸ”’ Zero-Trace Terminal

End-to-end encrypted P2P chat, right in your browser. No server, no accounts, no stored history.

by Hardlint Cybersecurity Team


⚠️ Important Notice

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.


✨ Features

  • πŸ” End-to-end encryption β€” AES-GCM 256-bit, key derived via PBKDF2 (100,000 iterations)
  • 🌐 True P2P connection β€” direct WebRTC link between the two devices, no central server relaying messages
  • 🚫 Zero persistence β€” no cookies, no localStorage, no database: close the tab and nothing remains
  • πŸ”‘ Single shared secret β€” a randomly generated Room Key (100 characters), no manual technical configuration required
  • 🧹 Panic Purge β€” one button instantly wipes keys, connection state, and visible chat history
  • πŸ“‘ Reliable connectivity β€” 18 STUN/TURN servers configured as fallbacks to work even behind restrictive NATs (4G/5G, corporate networks)

πŸš€ How to Use

  1. Open the page (must be served over HTTPS β€” e.g. via GitHub Pages, not opened as a local file)
  2. Host: click [1] INITIALIZE ROOM β†’ copy the generated Room Key
  3. Send the Room Key to your contact through a different channel (in person, voice call, another encrypted app)
  4. Guest: click [2] CONNECT TO ROOM β†’ paste the received Room Key
  5. Wait for the connection (usually a few seconds) β†’ the chat opens
  6. If the connection isn't established within 2 minutes, the Room Key expires automatically: generate a new one with the dedicated button

πŸ“‹ Requirements

  • Modern browser with WebRTC and Web Crypto API support (recent Chrome, Firefox, Edge, Safari)
  • Internet access on both devices
  • The page must be served over HTTPS (Secure Context is required for Web Crypto API and WebRTC) β€” it does not work when opened as a local file
  • Both parties must have the page open at the same time during the connection attempt
  • The Room Key must be copied in full, exactly 100 characters, with no extra spaces or line breaks

πŸ—οΈ Architectural Overview

Zero-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:

ComponentRoleTechnology
User interfaceRetro terminal UIPlain HTML/CSS
SignalingMakes the two peers "find" each otherPeerJS (public cloud broker)
Data transportEncrypted P2P channelWebRTC DataChannel
NAT traversalPunching through firewalls/NATSTUN + TURN (ICE)
EncryptionMessage content protectionAES-GCM 256-bit + PBKDF2
HostingCode distributionGitHub 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).


πŸ”„ Operational Flow

Room Key Generation

When a user clicks "INITIALIZE ROOM (HOST)":

  1. A random 100-character string is generated (generate100CharCode()), using crypto.getRandomValues() β€” a cryptographically secure random number generator (not Math.random(), which is unsuitable for cryptographic purposes).
  2. The character set includes uppercase/lowercase letters, digits, and special symbols (-_!@#$%^&*), 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).

Deriving Keys from the Room Key

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.

Signaling Phase (PeerJS)

  1. The Host creates a Peer object, registering with the public PeerJS cloud broker using the ID derived from the Room Key.
  2. The Guest, after pasting the same Room Key, computes the same ID and calls peer.connect(id).
  3. The PeerJS broker only mediates this initial exchange (who wants to talk to whom) β€” it never sees or transmits message content, which by that point travels over a separate WebRTC channel.

ICE Negotiation (NAT Traversal)

Once the two Peers have "introduced" themselves, WebRTC starts ICE negotiation to find a valid network path:

  1. Host candidates β€” the device's local IP addresses
  2. Server-reflexive (srflx) candidates β€” public IP discovered via STUN
  3. Relay candidates β€” allocated via TURN, used only if a direct connection fails

Configured ICE servers (in priority order):

  • Dedicated Metered.ca TURN (own credentials, not shared) β€” stun.relay.metered.ca / global.relay.metered.ca
  • 7 public STUN fallbacks (Google Γ—3, Cloudflare, Twilio, Nextcloud, stunprotocol.org, freestun)
  • 10 additional public TURN fallback endpoints (OpenRelay, freestun, numb.viagenie, ExpressTurn) β€” used only if the dedicated TURN also fails

The browser automatically tries every combination and selects the first one that establishes a working channel (standard ICE algorithm, handled internally by WebRTC).

Timeout and Session Expiry

  • If the connection isn't established within 120 seconds, the session is considered expired:
    • The Peer and DataConnection are destroyed (peer.destroy(), conn.close())
    • The status shows [EXPIRED] Room Key no longer valid
    • A button appears to generate a new Room Key (Host) or enter a new one (Guest)
  • This prevents a Room Key from remaining "listening" indefinitely on the public broker.

πŸ” Message Cryptographic Model

Every 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:

  • Confidentiality β€” nobody without the key can read the content
  • Integrity/authenticity β€” any tampering with the packet in transit causes decryption to fail ([ERR: DECRYPTION_FAILED]), rather than silently producing corrupted output

What this scheme does NOT cover:

  • Forward secrecy across sessions β€” if the same Room Key were reused across multiple sessions (not the normal flow, which generates a new one every time), all those sessions would share the same derived key
  • Peer identity authentication β€” anyone who knows the Room Key can connect; there is no cryptographic verification of "who" is on the other end beyond possession of the shared key

πŸ’Ύ Data Persistence (Client-Side)

DataPersistenceNotes
Room KeyNoneOnly in a JS variable, gone on close/reload
Derived AES-GCM keyNoneSame, never written to disk
Chat messagesNoneOnly live in the DOM/RAM, no localStorage/IndexedDB
CookiesNoneThe project uses none at all
Application logsLocal DevTools console onlyNever sent anywhere, gone when the tab closes

The "PANIC: PURGE SESSION" button explicitly forces:

  • Closure of the PeerConnection/DataConnection
  • Zeroing of the encryption key in memory
  • Wiping of all UI fields and the displayed message history

πŸ‘οΈ What External Infrastructure Can See (Metadata)

Key point to understand: encryption protects content, not connection metadata.

ServiceWhat it can seeWhat it CANNOT see
GitHub PagesIP and timestamp of whoever loads the pageMessage 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 transferredMessage content (already travels encrypted)
Each user's ISPThat a connection is being made to github.io / metered.ca / a PeerJS serverMessage 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.


🎯 Attack Surface and Known Limitations

RiskDescriptionMitigated?
Message content interceptionMITM on TURN/network trafficβœ… Yes β€” end-to-end AES-GCM
Message tampering in transitPacket manipulationβœ… Yes β€” GCM authentication tag
Deanonymization via IP metadataIP↔identity correlation through third-party logs⚠️ Partial β€” requires a VPN client-side, not solved by the code itself
Metered.ca account compromiseTURN credentials are in the public code (base64-obfuscated, not encrypted)⚠️ Minimal deterrent, not real security
Dependency on third-party servicesGitHub Pages, PeerJS broker, Metered TURN β€” if suspended, the app stops working⚠️ Not mitigated (would require full self-hosting)
Room Key reuseWould compromise forward secrecy across sessions that reuse itβœ… Not applicable in normal flow (a new Key every session)

πŸ› οΈ Full Technology Stack

  • Frontend: HTML5, CSS3 (no framework)
  • Cryptography: Browser-native Web Crypto API (crypto.subtle) β€” PBKDF2, AES-GCM, SHA-256
  • P2P/Signaling: PeerJS v1.5.4 (a wrapper library over native WebRTC), loaded from a public CDN (unpkg.com)
  • NAT Traversal: WebRTC ICE (STUN/TURN) β€” 18 endpoints configured in total
  • Hosting: GitHub Pages (static, automatic HTTPS)
  • Browser requirements: WebRTC support, Web Crypto API, ES6+ β€” requires a secure context (HTTPS); does not work from file:// or content://

πŸ“ Changelog of Major Versions

  1. v1 β€” Native WebRTC with manual SDP exchange (copy/paste offer/answer)
  2. v2 β€” Migrated to PeerJS, automatic connection based on the Room Key, removed manual SDP fields
  3. v3 β€” Added dedicated Metered.ca TURN + multiple public STUN/TURN fallbacks
  4. v4 β€” Detailed ICE diagnostics (candidate logging, connection states) β€” fixed a bug that overwrote PeerJS's internal event handlers
  5. v5 β€” Timeout extended to 120s, Room Key expiry system with manual regeneration, VPN warning on splash screen, TURN credential obfuscation

πŸ’š Support the Project

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

πŸ“œ License and Disclaimer

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

Contributors

mrhardlint

32 commits

mrhardlint/Hard-Chat

12

stars

32

commits

HTML

primary language

Sep 6, 2026

updated

README

πŸ”’ Zero-Trace Terminal

End-to-end encrypted P2P chat, right in your browser. No server, no accounts, no stored history.

by Hardlint Cybersecurity Team


⚠️ Important Notice

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.


✨ Features

  • πŸ” End-to-end encryption β€” AES-GCM 256-bit, key derived via PBKDF2 (100,000 iterations)
  • 🌐 True P2P connection β€” direct WebRTC link between the two devices, no central server relaying messages
  • 🚫 Zero persistence β€” no cookies, no localStorage, no database: close the tab and nothing remains
  • πŸ”‘ Single shared secret β€” a randomly generated Room Key (100 characters), no manual technical configuration required
  • 🧹 Panic Purge β€” one button instantly wipes keys, connection state, and visible chat history
  • πŸ“‘ Reliable connectivity β€” 18 STUN/TURN servers configured as fallbacks to work even behind restrictive NATs (4G/5G, corporate networks)

πŸš€ How to Use

  1. Open the page (must be served over HTTPS β€” e.g. via GitHub Pages, not opened as a local file)
  2. Host: click [1] INITIALIZE ROOM β†’ copy the generated Room Key
  3. Send the Room Key to your contact through a different channel (in person, voice call, another encrypted app)
  4. Guest: click [2] CONNECT TO ROOM β†’ paste the received Room Key
  5. Wait for the connection (usually a few seconds) β†’ the chat opens
  6. If the connection isn't established within 2 minutes, the Room Key expires automatically: generate a new one with the dedicated button

πŸ“‹ Requirements

  • Modern browser with WebRTC and Web Crypto API support (recent Chrome, Firefox, Edge, Safari)
  • Internet access on both devices
  • The page must be served over HTTPS (Secure Context is required for Web Crypto API and WebRTC) β€” it does not work when opened as a local file
  • Both parties must have the page open at the same time during the connection attempt
  • The Room Key must be copied in full, exactly 100 characters, with no extra spaces or line breaks

πŸ—οΈ Architectural Overview

Zero-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:

ComponentRoleTechnology
User interfaceRetro terminal UIPlain HTML/CSS
SignalingMakes the two peers "find" each otherPeerJS (public cloud broker)
Data transportEncrypted P2P channelWebRTC DataChannel
NAT traversalPunching through firewalls/NATSTUN + TURN (ICE)
EncryptionMessage content protectionAES-GCM 256-bit + PBKDF2
HostingCode distributionGitHub 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).


πŸ”„ Operational Flow

Room Key Generation

When a user clicks "INITIALIZE ROOM (HOST)":

  1. A random 100-character string is generated (generate100CharCode()), using crypto.getRandomValues() β€” a cryptographically secure random number generator (not Math.random(), which is unsuitable for cryptographic purposes).
  2. The character set includes uppercase/lowercase letters, digits, and special symbols (-_!@#$%^&*), 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).

Deriving Keys from the Room Key

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.

Signaling Phase (PeerJS)

  1. The Host creates a Peer object, registering with the public PeerJS cloud broker using the ID derived from the Room Key.
  2. The Guest, after pasting the same Room Key, computes the same ID and calls peer.connect(id).
  3. The PeerJS broker only mediates this initial exchange (who wants to talk to whom) β€” it never sees or transmits message content, which by that point travels over a separate WebRTC channel.

ICE Negotiation (NAT Traversal)

Once the two Peers have "introduced" themselves, WebRTC starts ICE negotiation to find a valid network path:

  1. Host candidates β€” the device's local IP addresses
  2. Server-reflexive (srflx) candidates β€” public IP discovered via STUN
  3. Relay candidates β€” allocated via TURN, used only if a direct connection fails

Configured ICE servers (in priority order):

  • Dedicated Metered.ca TURN (own credentials, not shared) β€” stun.relay.metered.ca / global.relay.metered.ca
  • 7 public STUN fallbacks (Google Γ—3, Cloudflare, Twilio, Nextcloud, stunprotocol.org, freestun)
  • 10 additional public TURN fallback endpoints (OpenRelay, freestun, numb.viagenie, ExpressTurn) β€” used only if the dedicated TURN also fails

The browser automatically tries every combination and selects the first one that establishes a working channel (standard ICE algorithm, handled internally by WebRTC).

Timeout and Session Expiry

  • If the connection isn't established within 120 seconds, the session is considered expired:
    • The Peer and DataConnection are destroyed (peer.destroy(), conn.close())
    • The status shows [EXPIRED] Room Key no longer valid
    • A button appears to generate a new Room Key (Host) or enter a new one (Guest)
  • This prevents a Room Key from remaining "listening" indefinitely on the public broker.

πŸ” Message Cryptographic Model

Every 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:

  • Confidentiality β€” nobody without the key can read the content
  • Integrity/authenticity β€” any tampering with the packet in transit causes decryption to fail ([ERR: DECRYPTION_FAILED]), rather than silently producing corrupted output

What this scheme does NOT cover:

  • Forward secrecy across sessions β€” if the same Room Key were reused across multiple sessions (not the normal flow, which generates a new one every time), all those sessions would share the same derived key
  • Peer identity authentication β€” anyone who knows the Room Key can connect; there is no cryptographic verification of "who" is on the other end beyond possession of the shared key

πŸ’Ύ Data Persistence (Client-Side)

DataPersistenceNotes
Room KeyNoneOnly in a JS variable, gone on close/reload
Derived AES-GCM keyNoneSame, never written to disk
Chat messagesNoneOnly live in the DOM/RAM, no localStorage/IndexedDB
CookiesNoneThe project uses none at all
Application logsLocal DevTools console onlyNever sent anywhere, gone when the tab closes

The "PANIC: PURGE SESSION" button explicitly forces:

  • Closure of the PeerConnection/DataConnection
  • Zeroing of the encryption key in memory
  • Wiping of all UI fields and the displayed message history

πŸ‘οΈ What External Infrastructure Can See (Metadata)

Key point to understand: encryption protects content, not connection metadata.

ServiceWhat it can seeWhat it CANNOT see
GitHub PagesIP and timestamp of whoever loads the pageMessage 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 transferredMessage content (already travels encrypted)
Each user's ISPThat a connection is being made to github.io / metered.ca / a PeerJS serverMessage 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.


🎯 Attack Surface and Known Limitations

RiskDescriptionMitigated?
Message content interceptionMITM on TURN/network trafficβœ… Yes β€” end-to-end AES-GCM
Message tampering in transitPacket manipulationβœ… Yes β€” GCM authentication tag
Deanonymization via IP metadataIP↔identity correlation through third-party logs⚠️ Partial β€” requires a VPN client-side, not solved by the code itself
Metered.ca account compromiseTURN credentials are in the public code (base64-obfuscated, not encrypted)⚠️ Minimal deterrent, not real security
Dependency on third-party servicesGitHub Pages, PeerJS broker, Metered TURN β€” if suspended, the app stops working⚠️ Not mitigated (would require full self-hosting)
Room Key reuseWould compromise forward secrecy across sessions that reuse itβœ… Not applicable in normal flow (a new Key every session)

πŸ› οΈ Full Technology Stack

  • Frontend: HTML5, CSS3 (no framework)
  • Cryptography: Browser-native Web Crypto API (crypto.subtle) β€” PBKDF2, AES-GCM, SHA-256
  • P2P/Signaling: PeerJS v1.5.4 (a wrapper library over native WebRTC), loaded from a public CDN (unpkg.com)
  • NAT Traversal: WebRTC ICE (STUN/TURN) β€” 18 endpoints configured in total
  • Hosting: GitHub Pages (static, automatic HTTPS)
  • Browser requirements: WebRTC support, Web Crypto API, ES6+ β€” requires a secure context (HTTPS); does not work from file:// or content://

πŸ“ Changelog of Major Versions

  1. v1 β€” Native WebRTC with manual SDP exchange (copy/paste offer/answer)
  2. v2 β€” Migrated to PeerJS, automatic connection based on the Room Key, removed manual SDP fields
  3. v3 β€” Added dedicated Metered.ca TURN + multiple public STUN/TURN fallbacks
  4. v4 β€” Detailed ICE diagnostics (candidate logging, connection states) β€” fixed a bug that overwrote PeerJS's internal event handlers
  5. v5 β€” Timeout extended to 120s, Room Key expiry system with manual regeneration, VPN warning on splash screen, TURN credential obfuscation

πŸ’š Support the Project

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

πŸ“œ License and Disclaimer

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

Contributors

mrhardlint

32 commits

Languages

HTML

100.0%