Skip to content

Security: SYR-ROOT/syrotp

Security

SECURITY.md

Security

Reporting a vulnerability

Please do not open public GitHub issues for security problems. Instead, open a private security advisory at https://github.com/SYR-ROOT/syrotp/security/advisories/new or email info@mhd-shekho.com (or use the operator address listed in your self-hosted deployment's SECURITY.md). We aim to respond within 72 hours.

Coordinated disclosure: we ask for 90 days before public details are posted, or sooner once a fix is shipped.

Encrypted reports (PGP)

For sensitive reports, or to verify signatures on our disclosures, encrypt to the maintainer's PGP key.

Always verify the full fingerprint above before trusting a downloaded copy of the key.

-----BEGIN PGP PUBLIC KEY BLOCK-----

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
DND8WKvar4aGpbRHZIYVBA==
=/W8E
-----END PGP PUBLIC KEY BLOCK-----

Threat model

SYROTP is designed to be safe against the following adversaries:

# Attacker Capability Mitigated by
1 Network observer Sees TLS-encrypted traffic TLS 1.2+, no auth tokens in URLs, HSTS recommended at the proxy.
2 Replay attacker on inbound endpoint Captures a valid signed request Per-request X-SYROTP-Nonce is single-use (SET NX EX in Redis). Timestamps must be within INBOUND_TIMESTAMP_SKEW_SECONDS.
3 Forger of inbound SMS Tries to verify a number they don't own Inbound is signed with the gateway's signing key (HMAC-SHA256 over ts.nonce.sha256(body)). The server requires the sender phone in the matched DB row to equal the verification's target phone, so a stranger's SMS cannot be repurposed.
4 DB-only breach Reads PostgreSQL contents API keys are stored as HMAC(MASTER_ENCRYPTION_KEY, "api_key:" + raw); MASTER_ENCRYPTION_KEY is in env, not DB. Without it, the hashes are not invertible into raw keys.
5 Brute-force on verification code Tries codes against a known phone 6-char codes from a 31-char unambiguous alphabet (~887M space). 10-minute TTL. Rate limits per IP and concurrent-pending caps per phone. Even with optimal guessing, the expected hit rate is below 1 in 100k per pending window. Increase VERIFICATION_CODE_LENGTH for higher assurance.
6 Public-key abuse from browsers Reads a pk_live_* from a website Public keys can only start verifications and read masked status — they cannot cancel, list, or read internal fields. Tighten further by adding origin/referrer checks at the proxy.
7 Phone number enumeration Probes whether a phone is registered We never confirm/deny phone existence — startVerification always returns a fresh pending row regardless of any user state in the developer's app.
8 Side-channel timing on key compare Distinguishes match from miss All key/signature comparisons use crypto.timingSafeEqual.

Out of scope (by design)

  • Compromise of the user's phone or SIM: If an attacker has your physical SIM, they can verify as you. This is the same property as traditional OTP and is unavoidable.
  • Compromise of the master key + DB simultaneously: Yields full access. Store MASTER_ENCRYPTION_KEY in a real secret manager (AWS Secrets Manager, HashiCorp Vault, sealed-secrets, etc.) when running production.
  • Compromise of a gateway device: The signing key on a paired Android gateway can post arbitrary inbound SMS to the API. Mitigation: revoke the receiver in the admin DB and rotate the signing key. Future work: per-message signing chains anchored to the device's hardware-backed keystore.

Cryptographic primitives

Use Primitive
API key hashing HMAC-SHA256 with MASTER_ENCRYPTION_KEY (deterministic so the column is indexable).
Gateway request auth HMAC-SHA256 over ts.nonce.sha256(body).
Verification codes crypto.randomInt over a 31-char alphabet.
API key generation 32 random bytes mapped into a 36-char alphabet (~187 effective bits).
Nonces 16 bytes from crypto.randomBytes.
Constant-time compare crypto.timingSafeEqual.

We do not use bcrypt/argon2 for API keys because the keys themselves are uniformly random — they are not human-chosen secrets and cannot be guessed offline.

Operator checklist (production)

  • MASTER_ENCRYPTION_KEY and COOKIE_SECRET are 64-hex random and not the placeholder values.
  • NODE_ENV=production (server refuses to boot with placeholder secrets in this mode).
  • PostgreSQL is reachable only from the server (private network or 127.0.0.1 bind).
  • Redis is reachable only from the server.
  • TLS terminates at a reverse proxy (nginx, Caddy, Cloudflare). Set TRUSTED_PROXIES to the CIDR of the proxy (e.g. 10.0.0.0/8) so req.ip reflects the real client. Production refuses to boot if TRUST_PROXY=true is set without a non-empty TRUSTED_PROXIES allowlist.
  • HSTS, X-Content-Type-Options: nosniff, and a strict CSP for any HTML are set at the proxy.
  • CORS_ORIGINS is an explicit allowlist, not *.
  • Database backups are encrypted at rest, and audit_log retention is at least 90 days.
  • MASTER_ENCRYPTION_KEY is in a secret manager, not on disk.
  • You have a documented procedure for rotating MASTER_ENCRYPTION_KEY (currently: re-issue all API keys + receiver signing keys).

There aren't any published security advisories