A Firefox extension that adds end-to-end AES-256-GCM encryption on top of Telegram Web. Per-chat toggle, automatic encryption on send, automatic decryption on receive, clickable links, mentions and hashtags preserved in decrypted view.
- Per-chat toggle β encryption activates only in chats you explicitly enable; other chats remain unchanged
- Automatic encryption on send β just press Enter as usual; the extension encrypts before the message leaves your browser
- Automatic decryption on receive β incoming encrypted messages are decrypted in place
- View mode toggle β switch between the decrypted view and the raw ciphertext to verify what's actually being sent
- Clickable URLs, @mentions, #hashtags in decrypted view, with HTML escaping to prevent injection
- Passphrase strength meter with entropy estimation (~bits)
- Strong cryptography defaults
- AES-256-GCM (authenticated encryption)
- PBKDF2-HMAC-SHA256 with 600,000 iterations (OWASP 2023)
- Random 16-byte salt and 12-byte IV per message
- GCM authentication tag detects tampering
There are three ways to install the extension, ranked from easiest to most permanent.
- Download
telegram-crypto.xpi(or clone this repo and zip the source folder) - Open
about:debugging#/runtime/this-firefoxin Firefox - Click Load Temporary Add-onβ¦
- Select the
manifest.jsonfile inside the extension folder - Reload Telegram Web with Ctrl+F5
The extension stays loaded until you close Firefox.
Regular release Firefox requires Mozilla-signed extensions. The easiest workaround is to use Firefox Developer Edition or Nightly, where unsigned extensions can be enabled:
- Install Firefox Developer Edition or Nightly
- Open
about:config, accept the warning, and setxpinstall.signatures.requiredtofalse - Drag
telegram-crypto.xpiinto the Firefox window - Confirm the install dialog
The extension stays installed across restarts.
For installation in stable Firefox, the extension must be signed by Mozilla:
- Create a free account at addons.mozilla.org
- Submit the extension as self-distribution (unlisted) for signing
- After Mozilla's automated review you receive a signed
.xpi - Install the signed
.xpiin any Firefox by drag-and-drop
This is the only way to get a permanent install in regular release Firefox.
-
Click the π icon in the Firefox toolbar to open the extension popup (if the icon is not visible, click the puzzle π§© icon and pin Telegram Crypto)
-
Enter a passphrase. The popup shows a strength meter:
Bits Label Meaning <40 Very weak Crackable in seconds 40β59 Weak Crackable in hours by a motivated attacker 60β79 Acceptable Reasonable for most uses 80β99 Strong Infeasible to brute force β₯100 Very strong Effectively impossible -
The recommended approach is 5β6 random words separated by spaces, e.g.
correct horse battery staple pine. This is easier to remember than a complex string and reaches β₯80 bits of entropy with minimal effort. -
The passphrase is saved automatically as you type, and persists across sessions.
The other party must have the extension installed with the same passphrase.
β οΈ Share the passphrase via a separate secure channel β in person, by phone call, via another encrypted messenger β never through Telegram itself. Anyone who can read the channel where the passphrase is shared can read all your encrypted messages.
When you open a Telegram chat, two floating controls appear to the left of the message input:
| Button | State | Meaning |
|---|---|---|
| π No Key | yellow | No passphrase configured. Set one in the extension popup. |
| π Clear Channel | grey | Passphrase set, encryption disabled in this chat. Messages send and receive in clear. |
| π Encrypted Channel | blue | Encryption enabled in this chat. Messages send encrypted; incoming encrypted messages decrypt automatically. |
| π Eye (icon) | blue circle | Showing decrypted text. Click to hide. Only visible when Encrypted Channel is active. |
| πβπ¨ Eye-off (icon) | grey circle | Showing raw ciphertext. Click to reveal decrypted text. Only visible when Encrypted Channel is active. |
The state is remembered per chat (keyed by the chat's URL hash), so it persists across page reloads and chat switches.
- Open the target chat
- Click the Clear Channel button β it turns into Encrypted Channel (blue)
- Type your message and press Enter as usual
- The extension encrypts the message before Telegram sends it
- The recipient sees the encrypted form unless they also have the extension with the same passphrase
If the other party has the same setup, their encrypted messages appear in your chat as:
π 5kF2bN9pQwVxYr3z... (long base64 string)
The extension automatically detects these and displays the decrypted text with a π prefix and a blue dashed outline. URLs, @mentions and #hashtags become clickable.
To toggle between decrypted view and raw view, click the eye / eye-off icon.
If the recipient does not have the extension installed (or has a different passphrase), they will see the raw ciphertext as a long base64 string prefixed by π. They cannot read the content.
Every encrypted message is sent through Telegram as:
π<base64-encoded payload>
The payload, after base64 decoding, is:
ββββββββββββββββ¬ββββββββββββ¬ββββββββββββββββββββββββββββββ
β salt (16B) β IV (12B) β ciphertext + GCM tag β
ββββββββββββββββ΄ββββββββββββ΄ββββββββββββββββββββββββββββββ
passphrase ββΆ PBKDF2-HMAC-SHA256 (600k iter, random salt) ββΆ AES-256 key
β
plaintext ββΆ AES-256-GCM (key, random IV) ββΆ ciphertext + tag
β
βΌ
base64(salt + IV + ct + tag) ββΆ π prefix added ββΆ Telegram
Each message uses a fresh random salt and IV, so encrypting the same plaintext twice produces completely different ciphertexts. The GCM authentication tag detects any modification to the ciphertext.
The extension scans the chat DOM looking for text nodes that either:
- Start directly with
π, or - Are pure base64 strings whose nearest DOM ancestor contains a
πemoji (Telegram often renders the emoji as a separate<img>element)
When a match is found:
- The base64 is decoded back into salt | IV | ciphertext + tag
- PBKDF2 derives the AES key from the passphrase and the message's salt
- AES-GCM verifies the tag and decrypts
- The original text node is replaced with a
<span>containing the decrypted plaintext with URLs, mentions and hashtags converted to clickable elements
If decryption fails (wrong passphrase, corrupted data, tampered ciphertext), the message remains visible in its raw form with a tooltip explaining the failure.
Messages encrypted by older versions of the extension (0.2.x, which used 100,000 PBKDF2 iterations) are still decryptable. The extension tries the current iteration count first and falls back to the legacy value if needed.
This is an honest assessment of what the extension protects against and what it does not.
| Adversary | Protection |
|---|---|
| Telegram (the company, servers, employees with DB access) | Message content is opaque ciphertext on Telegram's servers |
| Network observers (ISPs, governments, MITM) | TLS already protects in transit; this adds another encryption layer |
| Compelled disclosure of Telegram's stored data | Stored ciphertext is useless without the passphrase |
| Active modification of stored ciphertext | GCM authentication tag detects tampering |
| Adversary | Why not |
|---|---|
| Local malware, OS-level keyloggers | Plaintext exists in memory before encryption |
Other browser extensions with access to web.telegram.org |
They can read the input field's DOM directly |
| A malicious Telegram Web client modifying its own JS to capture plaintext | The plaintext exists in Telegram-controlled DOM before the extension cifrates it |
| Metadata analysis (who, when, message size) | All visible to Telegram regardless of payload encryption |
| Weak or leaked passphrases | No passphrase = no security |
| Forward secrecy | The same passphrase is used for all messages; if leaked, all past messages become readable |
This extension provides reasonable defense against Telegram as a passive adversary (reading stored messages on their servers, complying with subpoenas, or being breached).
It is not a defense against Telegram as an active adversary that could modify their
own web client to capture plaintext before the extension encrypts it. The plaintext exists
in a contenteditable element controlled by Telegram's own JavaScript before our key
handler fires; in principle, Telegram could intercept it there if motivated.
| Component | Choice |
|---|---|
| Cipher | AES-256-GCM (AEAD: confidentiality + authenticity) |
| Key derivation | PBKDF2-HMAC-SHA256, 600,000 iterations (OWASP 2023 recommendation) |
| Salt | 16 random bytes per message (crypto.getRandomValues) |
| IV / Nonce | 12 random bytes per message (crypto.getRandomValues) |
| Authentication tag | 16 bytes (default), appended to ciphertext |
| Encoding | Base64 |
| Random source | crypto.getRandomValues (CSPRNG) |
All primitives use the browser's Web Crypto API. No external cryptographic libraries are used.
telegram-crypto/
βββ manifest.json # Manifest V3, host permissions for web.telegram.org
βββ content.js # Main content script: crypto + DOM manipulation + UI
βββ content.css # Floating button styles
βββ popup.html # Configuration popup (passphrase, strength meter)
βββ popup.js # Popup logic (storage, entropy estimation)
βββ README.md # This file
βββ icons/
βββ icon48.png
βββ icon96.png
Configuration is persisted in browser.storage.local:
| Key | Type | Description |
|---|---|---|
passphrase |
string | The user's passphrase (stored unencrypted at rest) |
showDecrypted |
boolean | View mode preference |
tc_chat_<hash> |
boolean | Per-chat enable/disable state |
β οΈ The passphrase is stored unencrypted in the extension's local storage. Anyone with filesystem access to the Firefox profile can read it. This is acceptable for the documented threat model (Telegram as adversary) but is a real concern if your local machine is at risk.
- Firefox 109+ (Manifest V3 with
host_permissions) - Other Chromium-based browsers (Chrome, Edge, Brave) β should work with minor manifest adjustments, not officially tested
git clone https://github.com/<your-user>/telegram-crypto.git
cd telegram-crypto
zip -r ../telegram-crypto.xpi \
manifest.json content.js content.css \
popup.html popup.js README.md icons/- Open
about:debugging#/runtime/this-firefoxin Firefox - Click Load Temporary Add-onβ¦
- Select
manifest.jsonfrom the source folder
Open DevTools (F12) on a Telegram Web tab. If the extension is active, you will see:
[Telegram Crypto] v5.7 loaded (AES-256-GCM, PBKDF2 600k)
The encrypted message marker π is defined as the constant PREFIX near the top of
content.js. It can be changed to any string, but both sender and recipient must use
the same prefix.
If the floating buttons overlap with Telegram's UI on your screen layout, edit the
margin constant in the positionUI function in content.js (default 60).
Larger values push the buttons further left.
Pull requests welcome, especially for:
- Support for Telegram Web versions A (
/a/) and Z (/z/) β currently optimized for K - Markdown formatting in decrypted view (bold, italic, code)
- Argon2id as an alternative key derivation function (currently PBKDF2 only)
- Per-chat passphrases instead of a single global one
- Chromium/Chrome compatibility shim
When reporting bugs, please include:
- Firefox version
- Telegram Web version (
/k/,/a/,/z/) - Console output from DevTools
- Steps to reproduce
MIT. See LICENSE file for details.
This software is provided "as is", without warranty of any kind, express or implied.
This extension was written as a learning project to explore browser-based end-to-end encryption using AI vibecoding. Audit the source code yourself before relying on it for anything that matters.



