GramJS sessions: cifrado, rotación y los errores que matan tu producto
Una StringSession es la llave del reino. AES-256-GCM, rotación de auth_key, lock distribuido y los 5 errores que vemos quemar cuentas.
Una StringSession válida es literalmente las llaves del reino: con ella, cualquiera opera como esa cuenta, envía mensajes, accede a chats y secretos. Si tratas las sesiones de tus usuarios como guardas tu clave de WiFi, tu producto tiene un riesgo de seguridad inaceptable.
Esta guía cubre el camino correcto: cómo cifrarlas, cómo rotarlas, cómo no leakearlas en logs y los 5 errores que vemos quemar cuentas y reputación de SaaS de Telegram.
Qué es una StringSession exactamente
Cuando autenticas un cliente GramJS con un número de teléfono, Telegram te devuelve un auth_key permanente (256 bytes). GramJS empaqueta ese key + DC info + algunos metadatos en una string base64 que llamamos StringSession.
Esa string es permanente hasta que:
- El usuario revoque la sesión desde su app Telegram, o
- Telegram la invalide por seguridad (cambio de número, sospecha de spam, etc.)
Mientras viva, cualquier copia de esa string da acceso completo a la cuenta. Por eso es un activo crítico, no un dato más.
Lo mínimo para no leakear: cifrado en reposo
Guardar la StringSession en plain text en tu DB es como guardar contraseñas en una hoja de Excel. Es el error 1 y el más común en proyectos jóvenes.
El estándar correcto es AES-256-GCM con:
- Clave maestra en variable de entorno (
ENCRYPTION_KEY, 32 bytes hex) - IV (initialization vector) único por sesión, generado con
randomBytes - Authentication tag que verifica integridad del cifrado
- Almacenamiento = IV + tag + ciphertext en formato
iv:tag:cipher(hex)
En Node.js es directo con crypto:
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
const ALG = "aes-256-gcm";
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, "hex");
export function cifrar(plano: string): string {
const iv = randomBytes(16);
const cipher = createCipheriv(ALG, KEY, iv);
const enc = Buffer.concat([cipher.update(plano, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return `${iv.toString("hex")}:${tag.toString("hex")}:${enc.toString("hex")}`;
}
export function descifrar(payload: string): string {
const [iv, tag, enc] = payload.split(":").map((p) => Buffer.from(p, "hex"));
const decipher = createDecipheriv(ALG, KEY, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
}Cualquier solución que no use GCM o no firme con auth tag es inferior. Evita aes-256-cbc puro, te deja vulnerable a tampering.
Rotación de la clave maestra
La ENCRYPTION_KEY no es eterna. Rotarla cada 90 días limita el daño si se filtra.
Proceso correcto:
- Generar nueva
ENCRYPTION_KEY_V2 - Para cada sesión cifrada con V1, descifrar con V1 → cifrar con V2 → actualizar DB
- Una vez TODO está en V2, eliminar V1
- Mantener V1 + V2 disponibles durante la rotación (1-2 horas) para evitar downtime
Los proyectos serios automatizan esto con un job de Postgres + un endpoint admin que dispara la rotación. En operación con 10,000 cuentas, rotar puede tomar minutos.
AUTH_KEY_DUPLICATED: el ban silencioso
Telegram solo permite una conexión activa por auth_key a la vez. Si dos procesos abren la misma sesión simultáneamente, Telegram emite AUTH_KEY_DUPLICATED y mata las dos conexiones. Tu sesión queda en estado raro y, si pasa repetidamente, Telegram la invalida.
Detalle completo aquí, pero la solución corta:
Lock distribuido por cuenta con Redis:
async function conLockConexionMTProto(accountId: string, fn: () => Promise<T>) {
const lockKey = `mtproto:lock:${accountId}`;
const acquired = await redis.set(lockKey, "1", "EX", 30, "NX");
if (!acquired) {
// alguien ya tiene la cuenta, espera o falla
await esperarOFallar(lockKey);
}
try {
return await fn();
} finally {
await redis.del(lockKey);
}
}Sin lock, con 10 workers procesando cola, vas a tener AUTH_KEY_DUPLICATED en producción. Garantizado.
Los 5 errores que vemos quemar cuentas
Error 1: Sesiones en plain text en la DB
Ya cubierto arriba. Cifrar es obligatorio, no negociable. Audit log de cualquier acceso a la sesión.
Error 2: Sesiones serializadas en logs
logger.info(`Conectando cliente: ${JSON.stringify(account)}`);Si account incluye la sesión, ahora está en tus logs (Datadog, Sentry, CloudWatch). Los logs no son cifrados, son leíbles por cualquier dev con acceso. Vimos a un equipo perder 200 cuentas porque la sesión apareció en un dump de logs compartido en Slack.
Regla: nunca log la sesión completa, ni siquiera en debug. Si necesitas debug, log solo accountId + DC + fingerprint hash.
Error 3: Compartir sesiones entre servicios
Es tentador pasar la StringSession desde el backend al worker via mensaje en cola. Mal. Pasa el accountId y deja que el worker la lea del storage cifrado con la clave maestra que tiene. La sesión vive en un solo lugar: la DB.
Error 4: No invalidar sesión cuando el usuario cierra
Si el usuario cierra su cuenta en tu producto, tienes que revocar la sesión en Telegram llamando al método auth.LogOut. Si solo borras de tu DB, esa sesión sigue válida — y si alguien la copió antes, sigue teniendo acceso.
await client.invoke(new Api.auth.LogOut());Esa llamada le dice a Telegram "esta sesión murió, revócala". Solo después borras de tu DB.
Error 5: Compartir clave maestra en todos los servicios
Si la ENCRYPTION_KEY está disponible en todos tus contenedores (backend, worker, admin panel, scripts cron), un compromiso de cualquiera de ellos compromete todas las sesiones.
Mejor práctica: separar por servicio. Solo el servicio que necesita descifrar (worker MTProto) tiene la clave. El frontend nunca toca sesiones cifradas. Tener un servicio "vault" central que distribuye sesiones bajo demanda es lo ideal para escalar.
Auditoría de seguridad: lo mínimo
Antes de poner tu producto Telegram en producción, verifica:
- AES-256-GCM con IV único por sesión
- Clave maestra rotada cada 90 días
- Lock distribuido Redis con TTL antes de cada conexión MTProto
- Sesiones nunca en logs (audit con
grepen código) -
auth.LogOutal cerrar cuenta de usuario - Acceso a sesiones limitado a 1-2 servicios
- Monitoring de errores
AUTH_KEY_DUPLICATEDcon alerta
Si faltan items, no es opcional: es deuda de seguridad que se cobra sola.
El bonus: cifrado a nivel de aplicación + a nivel de DB
Para clientes con requisitos de compliance (PCI, GDPR, SOC 2), agrega doble cifrado:
- A nivel de aplicación: AES-256-GCM como describimos arriba
- A nivel de DB: TDE (Transparent Data Encryption) en Postgres con
pgcryptoo storage cifrado de tu proveedor (Supabase, Neon, RDS)
Esto te da defensa en profundidad: si alguien dumpea la DB, necesita ambas claves para descifrar.
Cómo lo hace Vega Punk
Vega Punk usa AES-256-GCM con ENCRYPTION_KEY en variable de entorno aislada del frontend, lock distribuido Redis con TTL 30s para cada conexión, audit log de cualquier acceso a sesiones, y auth.LogOut cuando el cliente desconecta una cuenta. La clave maestra rota cada 90 días con un job programado.
No es magia, es la receta correcta. La diferencia entre hacerlo bien y mal es la diferencia entre dormir tranquilo y tener una brecha de seguridad pública en seis meses.
Cierre
La sesión de Telegram es uno de los activos más valiosos de tu producto. Trátala como tal: cifrada, rotada, aislada, auditada. Si no te sale natural pensar en estos términos, contrata a alguien que sí — o usa una plataforma como Vega Punk que ya lo tiene resuelto.
El día que tengas tu primera brecha de seguridad, vas a desear haber leído este post seis meses antes.