A zero-knowledge, ephemeral messaging engine built with pure native PHP & Web Crypto API. No frameworks, no dependencies, no server access to secrets.
0
stars
1
commits
PHP
primary language
Sep 12, 2026
updated
Pure Native PHP. No frameworks. No bloat. No trust required.
Secret Gate is a self-hostable, ephemeral messaging engine for people who want an anonymous inbox without handing a server the keys to read it. You generate a link, share it, and anyone can drop an encrypted message into it β but the encryption/decryption happens entirely in the visitor's browser using the Web Crypto API. The server only ever stores ciphertext it cannot decrypt.
There's no signup, no email, no phone number, and no tracking. Just a keypair, a password, and a link. When a message or link expires, it's gone β enforced at the database and cache layer, not by a "trust us" policy.
It was built deliberately without a framework. No Composer dependency tree, no node_modules, no build step. ~10 PHP files you can read top to bottom in an afternoon, deployable on almost any LAMP-style stack.
window.crypto.subtle. The server stores only ciphertext and a public key; it is mathematically incapable of reading message contents.expires_at columns and Redis TTL keys β not a soft "please delete this" toggle..htaccess.Secret Gate uses hybrid encryption: an RSA-OAEP keypair identifies the inbox, while every individual message gets its own single-use AES-256-GCM key β the best of both worlds for a multi-sender, single-recipient model.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. INBOX CREATION (Owner) β
β β
β Browser β
β ββ Generate RSA-OAEP 2048-bit keypair (window.crypto.subtle) β
β ββ Derive AES-256-GCM wrapping key (PBKDF2, password) β
β ββ Encrypt(private_key, wrapping_key) βββΊ store in IndexedDB β
β β (never leaves device) β
β ββ POST public_key (JWK) + Argon2id(password) βββΊ Server β
β β
β Server (api.php) β
β ββ INSERT INTO sb_links (public_id, public_key, password_hash) β
β (server never sees the private key or the plaintext password) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2. SENDING A MESSAGE (Sender) β
β β
β Browser Server β
β ββ GET public_key(JWK) for link βββΊ fetch from sb_links β
β ββ Generate one-time AES-256-GCM key β
β ββ Encrypt(message, AES key) β
β ββ Encrypt(AES key, RSA-OAEP public_key) β
β ββ POST { encrypted_message, wrapped_key } βββΊ INSERT sb_messages β
β (ciphertext only) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3. READING MESSAGES (Owner) β
β β
β Browser Server β
β ββ Unlock local vault with password β
β β ββ Derive wrapping key (PBKDF2) βββΊ decrypt private key locally β
β ββ GET encrypted messages βββΊ SELECT WHERE not expired β
β ββ Decrypt(AES key, private_key) βββΊ Decrypt(message, AES key) β
β (all decryption happens on-device βββΊ server output stays opaque) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why hybrid encryption? RSA-OAEP lets any anonymous sender encrypt a message using only the public key β no shared secret, no prior handshake. AES-256-GCM keeps per-message encryption fast and keeps ciphertext size sane, since RSA alone isn't built for encrypting arbitrary-length payloads.
pdo_mysql, redis, and openssl extensions enabledstorage/ with the same behavior, so the app still runs without itmod_rewrite and mod_headersgit clone https://github.com/sasiru-mindaka/Secret-Gate.git
cd Secret-Gate
Create an .env file (outside the web root is strongly recommended) with the following keys:
APP_ENV=production
# Database
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=your_db_user
DB_PASS=your_db_password
DB_NAME=schema
# Redis (optional β leave as-is if you're not running Redis;
# the app falls back to file-based rate limiting automatically)
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASS=
# Cloudflare Turnstile
CF_TURNSTILE_SECRET_KEY=your_turnstile_secret
# Telegram feedback relay
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_CHAT_ID=your_chat_id
By default,
config.phploads.envfrom one directory above your web root (__DIR__ . '/../.env') β e.g. if your site lives at/var/www/html, place.envat/var/www/.env. This keeps credentials outside the publicly served directory even if the web server itself is ever misconfigured. Only change the path inloadEnv()if your server layout is different from this.
mysql -u your_db_user -p schema < schema.sql
This creates the two core tables:
| Table | Purpose |
|---|---|
sb_links | Stores the inbox's public key, Argon2id password hash, and TTL settings β never the private key |
sb_messages | Stores ciphertext only, with an optional expires_at for self-destructing messages |
Apache is expected β .htaccess handles routing, PHP file whitelisting, and blocks direct access to everything except the entry-point scripts (index.php, api.php, feedback.php, etc.). Make sure AllowOverride All is set for the vhost.
| Layer | Protection | Implementation |
|---|---|---|
| Message Content | Never readable by the server | Client-side AES-256-GCM, per-message single-use key |
| Private Keys | Never transmitted | RSA-OAEP private key wrapped locally with a password-derived AES-256-GCM key, stored only in browser IndexedDB |
| Passwords | Never stored in plaintext | Argon2id hashing (PASSWORD_ARGON2ID) |
| Database Access | No injection surface | 100% PDO prepared statements, zero raw query concatenation |
| State-Changing Requests | CSRF-protected | Per-session CSRF tokens with expiry, validated via hash_equals() |
| Cross-Origin Requests | Rejected by default | Strict Origin/Referer host matching, same-origin-only CORS headers |
| Bot / Abuse Traffic | Rate-limited & blocked, no message-linked IP logging | Redis-backed sliding-window rate limiting (auto-falls back to file storage if Redis is unavailable) β request counters expire on their own within the rate window (~30β60s); temporary IP blacklist entries auto-expire (default 24h); repeat offenders can be permanently "burned" and are kept blocked until manually cleared. IPs are used only for this abuse-prevention logic β never stored alongside messages or link data. |
| Data Expiry | Enforced, not optional | Per-message TTL and inactivity-based link auto-deletion (MySQL + Redis) |
| File/Config Exposure | Locked down | .htaccess denies direct PHP execution outside a strict whitelist, blocks .env, .sql, .log, and other sensitive extensions |
| Personal Data | Not collected | No accounts, no email, no phone number, no analytics/tracking scripts |
| Sessions | Hardened cookies | HttpOnly, Secure, SameSite=Lax, strict mode, read_and_close session handling |
Secret Gate is released under the GNU Affero General Public License v3.0 or later.
In short: you're free to use, modify, and self-host this project β but if you run a modified version as a network service, you must make your modified source available to your users too. See LICENSE for the full text.
Built by Sasiru Mindaka
If Secret Gate is useful to you, consider dropping a β β it helps the project reach more people who care about privacy.
1 commits
PHP
100.0%
A zero-knowledge, ephemeral messaging engine built with pure native PHP & Web Crypto API. No frameworks, no dependencies, no server access to secrets.
0
stars
1
commits
PHP
primary language
Sep 12, 2026
updated
Pure Native PHP. No frameworks. No bloat. No trust required.
Secret Gate is a self-hostable, ephemeral messaging engine for people who want an anonymous inbox without handing a server the keys to read it. You generate a link, share it, and anyone can drop an encrypted message into it β but the encryption/decryption happens entirely in the visitor's browser using the Web Crypto API. The server only ever stores ciphertext it cannot decrypt.
There's no signup, no email, no phone number, and no tracking. Just a keypair, a password, and a link. When a message or link expires, it's gone β enforced at the database and cache layer, not by a "trust us" policy.
It was built deliberately without a framework. No Composer dependency tree, no node_modules, no build step. ~10 PHP files you can read top to bottom in an afternoon, deployable on almost any LAMP-style stack.
window.crypto.subtle. The server stores only ciphertext and a public key; it is mathematically incapable of reading message contents.expires_at columns and Redis TTL keys β not a soft "please delete this" toggle..htaccess.Secret Gate uses hybrid encryption: an RSA-OAEP keypair identifies the inbox, while every individual message gets its own single-use AES-256-GCM key β the best of both worlds for a multi-sender, single-recipient model.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. INBOX CREATION (Owner) β
β β
β Browser β
β ββ Generate RSA-OAEP 2048-bit keypair (window.crypto.subtle) β
β ββ Derive AES-256-GCM wrapping key (PBKDF2, password) β
β ββ Encrypt(private_key, wrapping_key) βββΊ store in IndexedDB β
β β (never leaves device) β
β ββ POST public_key (JWK) + Argon2id(password) βββΊ Server β
β β
β Server (api.php) β
β ββ INSERT INTO sb_links (public_id, public_key, password_hash) β
β (server never sees the private key or the plaintext password) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2. SENDING A MESSAGE (Sender) β
β β
β Browser Server β
β ββ GET public_key(JWK) for link βββΊ fetch from sb_links β
β ββ Generate one-time AES-256-GCM key β
β ββ Encrypt(message, AES key) β
β ββ Encrypt(AES key, RSA-OAEP public_key) β
β ββ POST { encrypted_message, wrapped_key } βββΊ INSERT sb_messages β
β (ciphertext only) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3. READING MESSAGES (Owner) β
β β
β Browser Server β
β ββ Unlock local vault with password β
β β ββ Derive wrapping key (PBKDF2) βββΊ decrypt private key locally β
β ββ GET encrypted messages βββΊ SELECT WHERE not expired β
β ββ Decrypt(AES key, private_key) βββΊ Decrypt(message, AES key) β
β (all decryption happens on-device βββΊ server output stays opaque) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why hybrid encryption? RSA-OAEP lets any anonymous sender encrypt a message using only the public key β no shared secret, no prior handshake. AES-256-GCM keeps per-message encryption fast and keeps ciphertext size sane, since RSA alone isn't built for encrypting arbitrary-length payloads.
pdo_mysql, redis, and openssl extensions enabledstorage/ with the same behavior, so the app still runs without itmod_rewrite and mod_headersgit clone https://github.com/sasiru-mindaka/Secret-Gate.git
cd Secret-Gate
Create an .env file (outside the web root is strongly recommended) with the following keys:
APP_ENV=production
# Database
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=your_db_user
DB_PASS=your_db_password
DB_NAME=schema
# Redis (optional β leave as-is if you're not running Redis;
# the app falls back to file-based rate limiting automatically)
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASS=
# Cloudflare Turnstile
CF_TURNSTILE_SECRET_KEY=your_turnstile_secret
# Telegram feedback relay
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_CHAT_ID=your_chat_id
By default,
config.phploads.envfrom one directory above your web root (__DIR__ . '/../.env') β e.g. if your site lives at/var/www/html, place.envat/var/www/.env. This keeps credentials outside the publicly served directory even if the web server itself is ever misconfigured. Only change the path inloadEnv()if your server layout is different from this.
mysql -u your_db_user -p schema < schema.sql
This creates the two core tables:
| Table | Purpose |
|---|---|
sb_links | Stores the inbox's public key, Argon2id password hash, and TTL settings β never the private key |
sb_messages | Stores ciphertext only, with an optional expires_at for self-destructing messages |
Apache is expected β .htaccess handles routing, PHP file whitelisting, and blocks direct access to everything except the entry-point scripts (index.php, api.php, feedback.php, etc.). Make sure AllowOverride All is set for the vhost.
| Layer | Protection | Implementation |
|---|---|---|
| Message Content | Never readable by the server | Client-side AES-256-GCM, per-message single-use key |
| Private Keys | Never transmitted | RSA-OAEP private key wrapped locally with a password-derived AES-256-GCM key, stored only in browser IndexedDB |
| Passwords | Never stored in plaintext | Argon2id hashing (PASSWORD_ARGON2ID) |
| Database Access | No injection surface | 100% PDO prepared statements, zero raw query concatenation |
| State-Changing Requests | CSRF-protected | Per-session CSRF tokens with expiry, validated via hash_equals() |
| Cross-Origin Requests | Rejected by default | Strict Origin/Referer host matching, same-origin-only CORS headers |
| Bot / Abuse Traffic | Rate-limited & blocked, no message-linked IP logging | Redis-backed sliding-window rate limiting (auto-falls back to file storage if Redis is unavailable) β request counters expire on their own within the rate window (~30β60s); temporary IP blacklist entries auto-expire (default 24h); repeat offenders can be permanently "burned" and are kept blocked until manually cleared. IPs are used only for this abuse-prevention logic β never stored alongside messages or link data. |
| Data Expiry | Enforced, not optional | Per-message TTL and inactivity-based link auto-deletion (MySQL + Redis) |
| File/Config Exposure | Locked down | .htaccess denies direct PHP execution outside a strict whitelist, blocks .env, .sql, .log, and other sensitive extensions |
| Personal Data | Not collected | No accounts, no email, no phone number, no analytics/tracking scripts |
| Sessions | Hardened cookies | HttpOnly, Secure, SameSite=Lax, strict mode, read_and_close session handling |
Secret Gate is released under the GNU Affero General Public License v3.0 or later.
In short: you're free to use, modify, and self-host this project β but if you run a modified version as a network service, you must make your modified source available to your users too. See LICENSE for the full text.
Built by Sasiru Mindaka
If Secret Gate is useful to you, consider dropping a β β it helps the project reach more people who care about privacy.
1 commits
PHP
100.0%