SMSGate 001

Cifrado end-to-end

SMSGate 001 soporta cifrado opcional entre tu celular Android y tu código cliente. Nuestros servidores solo ven ciphertext, marcas de tiempo y números de teléfono — nunca el contenido del mensaje.

Modelo de amenazas

  • Nuestros servidores quedan fuera del alcance. No podemos leer ni entregar el contenido.
  • Los números de teléfono (remitente/destinatario) siguen siendo visibles para nosotros.
  • El carrier (Telcel / AT&T / Movistar / WhatsApp) ve el texto plano cuando viaja por la red celular — esto protege contra nosotros, no contra el carrier.
  • Si pierdes la passphrase, no podemos recuperar mensajes pasados.

Formato del envelope

Base64 de: "smsgw1" | salt[16] | iv[12] | AES-256-GCM(ciphertext + tag 16 bytes).

Derivación: PBKDF2-HMAC-SHA256(passphrase, salt, 100_000 iteraciones, 32 bytes).

Cifrado: AES-256-GCM, IV de 12 bytes, tag de 16 bytes.

Paso 1 — Configurar la passphrase en el celular

En la app Android, toca 🔒 Set E2E encryption passphrase e ingresa una passphrase fuerte (recomendamos 4+ palabras al azar o una cadena aleatoria de 20+ caracteres). Se guarda solo en la memoria privada del celular — nunca sale del dispositivo.

Paso 2 — Usar la misma passphrase en tu código

Node.js

import crypto from "node:crypto";

const ITER = 100_000;
const MAGIC = Buffer.from("smsgw1");

export function encrypt(passphrase, plaintext) {
  const salt = crypto.randomBytes(16);
  const iv = crypto.randomBytes(12);
  const key = crypto.pbkdf2Sync(passphrase, salt, ITER, 32, "sha256");
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  return Buffer.concat([MAGIC, salt, iv, ct, tag]).toString("base64");
}

export function decrypt(passphrase, envelopeB64) {
  const buf = Buffer.from(envelopeB64, "base64");
  if (!buf.slice(0, 6).equals(MAGIC)) throw new Error("envelope inválido");
  const salt = buf.slice(6, 22);
  const iv = buf.slice(22, 34);
  const tag = buf.slice(buf.length - 16);
  const ct = buf.slice(34, buf.length - 16);
  const key = crypto.pbkdf2Sync(passphrase, salt, ITER, 32, "sha256");
  const d = crypto.createDecipheriv("aes-256-gcm", key, iv);
  d.setAuthTag(tag);
  return Buffer.concat([d.update(ct), d.final()]).toString("utf8");
}

Python

import os, base64, hashlib
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

ITER = 100_000
MAGIC = b"smsgw1"

def encrypt(passphrase: str, plaintext: str) -> str:
    salt = os.urandom(16)
    iv = os.urandom(12)
    key = hashlib.pbkdf2_hmac("sha256", passphrase.encode(), salt, ITER, 32)
    ct = AESGCM(key).encrypt(iv, plaintext.encode(), None)
    return base64.b64encode(MAGIC + salt + iv + ct).decode()

def decrypt(passphrase: str, envelope_b64: str) -> str:
    buf = base64.b64decode(envelope_b64)
    assert buf[:6] == MAGIC
    salt, iv, ct = buf[6:22], buf[22:34], buf[34:]
    key = hashlib.pbkdf2_hmac("sha256", passphrase.encode(), salt, ITER, 32)
    return AESGCM(key).decrypt(iv, ct, None).decode()

Paso 3 — Enviar un mensaje cifrado

Cifra en tu lado, manda el envelope como message, marca isEncrypted: true:

CIPHERTEXT=$(node -e "import('./e2e.js').then(m=>console.log(m.encrypt('mi passphrase','hola mundo')))")

curl -X POST https://sms.001.com.mx/api/v1/send \
  -H "Authorization: Bearer sk_..." \
  -H "content-type: application/json" \
  -d "{
    \"channel\": \"sms\",
    \"phoneNumbers\": [\"+5215555555555\"],
    \"message\": \"$CIPHERTEXT\",
    \"isEncrypted\": true
  }"

El dispositivo Android descifra con la passphrase local y manda el texto plano al carrier. Nuestros servidores solo ven el envelope cifrado.

Paso 4 — Descifrar mensajes entrantes

Cuando E2E está activo en el celular, los SMS entrantes se cifran con la passphrase antes de enviarnos nada. Tu webhook receiver debe descifrar:

import { decrypt } from "./e2e.js";
app.post("/webhook", express.json(), (req, res) => {
  const { event, data } = req.body;
  if (event === "sms:received" && data.isEncrypted) {
    const plaintext = decrypt(process.env.E2E_PASSPHRASE, data.body);
    console.log("plaintext:", plaintext);
  }
  res.sendStatus(200);
});

Consideraciones

  • WhatsApp ya trae su propio E2E; isEncrypted aplica solo a SMS/MMS.
  • SMS multipart: el envelope agrega ~60 bytes de overhead.
  • Rotación de llaves: cambiar la passphrase invalida los mensajes pasados. Mantén un log de llaves.
  • Higiene: guarda la passphrase en un secrets manager (1Password, AWS Secrets Manager, GCP Secret Manager). Nunca la subas al repo.