Secret chats (E2E)
Secret chats are end-to-end encrypted: not even Telegram's servers see plaintext. They use a separate schema, separate methods, and require a Diffie–Hellman handshake on every new chat.
How secret chats work
Regular Telegram chats are client-server encrypted: TLS-equivalent on the wire, but the server sees plaintext. Secret chats run a second DH handshake over the regular MTProto channel; the resulting shared key is used for AES-256 IGE on every message in that chat. The server only ever sees ciphertext.
The E2E schema is separate from the main API. Constructors live under DecryptedMessage, DecryptedMessageMedia, etc. (Go package github.com/amarnathcjd/gogram/telegram/e2e); you can browse the full set at tl.gogram.sbs/e2e.
Starting a secret chat
chat, err := client.RequestSecretChat(userID)
if err != nil { log.Fatal(err) }
fmt.Println("waiting for accept, chat id:", chat.ID)chat, err := client.RequestSecretChat(userID)
if err != nil { log.Fatal(err) }
fmt.Println("waiting for accept, chat id:", chat.ID)RequestSecretChatinitiates the handshake and returns the pending chat record. The other side has to accept on their device; until then the chat is "waiting." When they accept, a UpdateEncryption arrives and the chat becomes usable. On the receiving side, call AcceptSecretChat(chat, gA) to complete the handshake.
Sending and receiving
_, err := client.SendSecretMessage(chat.ID, "this is end-to-end encrypted", 0 /* ttl */)
_, err := client.SendSecretMessage(chat.ID, "this is end-to-end encrypted", 0 /* ttl */)SendSecretMessage takes the chat id (an int32), the message, and an optional TTL in seconds. It encrypts locally and submits ciphertext via messages.sendEncrypted. For files, use SendSecretFile(chatID, path, opts)— the file is encrypted in-place before upload and the recipient decrypts on download via DecryptSecretFile.
There is no high-level "secret message" event on the dispatcher; register raw handlers and forward encryption updates to the secret-chat manager:
// Secret-chat updates are not routed through the high-level NewMessage
// dispatcher. Register a raw handler and forward to the secret chat manager.
client.AddRawHandler(&telegram.UpdateNewEncryptedMessage{}, func(u telegram.Update, c *telegram.Client) error {
return c.HandleSecretChatUpdate(u)
})
client.AddRawHandler(&telegram.UpdateEncryption{}, func(u telegram.Update, c *telegram.Client) error {
return c.HandleSecretChatUpdate(u)
})// Secret-chat updates are not routed through the high-level NewMessage
// dispatcher. Register a raw handler and forward to the secret chat manager.
client.AddRawHandler(&telegram.UpdateNewEncryptedMessage{}, func(u telegram.Update, c *telegram.Client) error {
return c.HandleSecretChatUpdate(u)
})
client.AddRawHandler(&telegram.UpdateEncryption{}, func(u telegram.Update, c *telegram.Client) error {
return c.HandleSecretChatUpdate(u)
})Or drop straight to the plaintext:
// If you want to poke at ciphertext directly:
plain, err := client.DecryptSecretMessage(chatID, encryptedBytes)
if err != nil { return err }
if msg, ok := plain.Message.(*e2e.DecryptedMessageObj); ok {
fmt.Println("plaintext:", msg.Message)
}// If you want to poke at ciphertext directly:
plain, err := client.DecryptSecretMessage(chatID, encryptedBytes)
if err != nil { return err }
if msg, ok := plain.Message.(*e2e.DecryptedMessageObj); ok {
fmt.Println("plaintext:", msg.Message)
}Key rotation
Telegram's spec recommends rotating the chat key every ~100 messages or once a week. The library sends a DecryptedMessageActionNotifyLayer when appropriate and both sides re-run a smaller DH exchange, switching to the new key without interrupting the conversation.
Limitations
- User accounts only. Bots cannot participate in secret chats.
- One device. A secret chat lives on a single device per side. Open it on another device and you get a fresh chat with a new key — old history does not sync.
- No groups. Secret chats are 1:1 only.
- Self-destruct timers. Each message can carry a TTL; after that, both sides delete locally. The server enforces the timer in metadata only.
