Simple End-to-End Encrypted (E2EE) private messaging module for PHP websites.
Built with:
- PHP 8
- MySQL
- Vanilla JavaScript
- libsodium.js
The server acts only as a dumb relay and never decrypts messages.
Designed as a simple drop-in solution for small websites and communities.
- End-to-End Encryption (E2EE)
- X25519 key exchange
- XChaCha20-Poly1305 authenticated encryption
- Vanilla JS frontend
- No frameworks
- No build tools
- Simple PHP backend
- Easy integration into existing websites
- Uses existing PHP sessions (
$_SESSION['user_id']) - Self-hosted crypto library support
This project is designed for:
- small communities
- hobby projects
- forums
- entertainment websites
The server stores:
- encrypted messages
- public keys
The server does NOT store:
- plaintext messages
- private keys
Private keys are stored locally in the user's browser using localStorage.
This is not a military-grade messenger.
Security depends heavily on:
- XSS protection
- secure HTTPS configuration
- CSP headers
- trusted frontend code delivery
If your website is compromised, E2EE protection may fail.
- PHP 8+
- MySQL 5.7+ or MariaDB
- HTTPS
- Existing user authentication system
Copy the files to your hosting, enter the database access data in the php config file
Import install.sql into your MySQL database.
mysql -u root -p your_database < install.sqlDownload the official browser build from:
Place the files into:
/js/lib/
Required files:
sodium.js
sodium.wasm
Add messenger.php to your page:
<?php include 'e2ee-messenger/messenger.php'; ?>Your website must already have authentication.
The messenger uses:
$_SESSION['user_id']Example:
session_start();
if (!isset($_SESSION['user_id'])) {
exit('Unauthorized');
}e2ee-messenger/
├── README.md
├── install.sql
│
├── api/
│ ├── keys.php
│ ├── messages.php
│ └── contacts.php
│
├── js/
│ ├── lib/
│ │ └── sodium.js
│ │ └── sodium.wasm
│ │
│ ├── crypto.js
│ ├── storage.js
│ ├── api.js
│ └── messenger.js
│
├── css/
│ └── messenger.css
│
└── messenger.php
Stores one active public key per user.
CREATE TABLE e2ee_user_keys (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
public_key VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_user (user_id)
);Stores encrypted messages only.
CREATE TABLE e2ee_messages (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
sender_id INT UNSIGNED NOT NULL,
recipient_id INT UNSIGNED NOT NULL,
ciphertext TEXT NOT NULL,
is_read TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);This project uses:
- X25519 for key exchange
- XChaCha20-Poly1305 for encryption
- libsodium.js for browser cryptography
The server never receives plaintext messages.
- No multi-device sync
- No forward secrecy
- No encrypted backups
- Private keys are stored in browser localStorage
- If localStorage is deleted, message history may become unreadable
Example CSP:
Content-Security-Policy:
default-src 'self';
script-src 'self';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';Recommended:
- HTTPS only
- HttpOnly cookies
- SameSite cookies
- No inline scripts
To avoid integration confusion, it is crucial to understand the core UX workflow of this module. This messenger behaves similarly to Telegram's Secret Chats (it is strictly device-bound).
Because this is a zero-knowledge E2EE system, User A cannot send a message to User B until User B has visited the messenger page at least once.
- When a user opens
messenger.phpfor the first time, their browser automatically generates their X25519 keys and uploads the public key to your database. - If User A tries to open a chat with User B before User B has generated their keys, the API will return a
404 Key Not Founderror.
Recommended integration on your main site: On your user profile pages, check if the target user has an active public key before displaying the chat link:
// Inside your profile.php (or member card)
$stmt = $pdo->prepare("SELECT 1 FROM e2ee_user_keys WHERE user_id = ?");
$stmt->execute([$profile_user_id]);
$is_active = $stmt->fetchColumn();
if ($is_active) {
echo '<a href="/messenger.php?chat_with=' . $profile_user_id . '" class="btn">Send Secure Message</a>';
} else {
echo '<button disabled title="This user hasn\'t activated their secure chat yet">Chat Unavailable</button>';
}To initiate or open a chat with a specific user from anywhere on your website, simply pass their user ID as a chat_with GET parameter:
[https://yourwebsite.com/e2ee-messenger/messenger.php?chat_with=12927](https://yourwebsite.com/e2ee-messenger/messenger.php?chat_with=12927)
Educate your users that clearing their browser's localStorage or logging in from a completely different device/browser will:
- Permanently delete their current private key from that device.
- Automatically generate a new keypair upon their next chat visit (updating the public key on the server).
- Render all previous chat history unreadable (old messages will show a
⚠️ [Message cannot be decrypted]warning). However, all new messages sent after this point will work perfectly.
MIT License
This project is provided for educational and small community usage.
Do not use this messenger for:
- critical infrastructure
- government communications
- high-risk threat environments
- sensitive corporate secrets