End-to-end encryption
SMSGate 001 supports optional end-to-end encryption between your Android device and your client code. Our server only sees ciphertext, timestamps, and phone numbers — never the message body.
Threat model
- Our servers are out of scope. We can’t read, log, or hand over message contents.
- Phone numbers (sender/recipient) are still visible to us.
- The carrier (Telcel / AT&T / Movistar / WhatsApp) sees plaintext on the wire — this feature protects against us, not the carrier.
- If you lose the passphrase, we cannot recover past messages.
Envelope format
Base64 of: "smsgw1" | salt[16 bytes] | iv[12 bytes] | AES-256-GCM(ciphertext + 16-byte tag).
Key derivation: PBKDF2-HMAC-SHA256(passphrase, salt, iterations=100_000, keyLen=32 bytes).
Cipher: AES-256-GCM, 12-byte IV, 16-byte tag.
Step 1 — Set the passphrase on your Android device
In the SMS Gateway Android app, tap 🔒 Set E2E encryption passphrase and enter a strong string (we recommend 4+ random words or a 20+ character random string). The passphrase is stored only in your phone’s private SharedPreferences — it never leaves the device.
Step 2 — Use the same passphrase in your client code
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("bad magic");
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) # ct includes 16-byte tag
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()
Step 3 — Send an encrypted message
Encrypt on your side, send the envelope as the message, and set isEncrypted: true:
CIPHERTEXT=$(node -e "import('./e2e.js').then(m=>console.log(m.encrypt('my 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
}"
The Android device will decrypt the envelope with its local passphrase and forward the plaintext to the carrier. Our servers only ever hold the envelope.
Step 4 — Decrypt inbound messages
When E2E is enabled on the phone, inbound SMS bodies are encrypted with the passphrase before being sent to us. Your webhook receiver must decrypt:
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);
});
Caveats
- WhatsApp uses its own E2E already;
isEncryptedis for SMS/MMS only. - Multipart SMS: Envelope format adds ~60 bytes of overhead. A 140-byte SMS becomes ~100 chars of plaintext budget.
- Key rotation: changing the passphrase invalidates old inbound messages. Keep a key log.
- Passphrase hygiene: store it in a secrets manager (1Password, AWS Secrets Manager, GCP Secret Manager). Never commit to source.