Skip to content

Security: YogeshDPalve/valutToken

Security

docs/SECURITY.md

SECURITY.md

Threat Model

VaultToken is designed to defend against the following attack classes.


Attack 1 — alg:none Token Forgery

What: Attacker sets alg: "none" in the JWT header and removes the signature. Libraries that trusted the header's algorithm field would accept unsigned tokens.

JWT status: Several major libraries were historically vulnerable (pre-2015).

VaultToken defense: PASETO tokens have no algorithm field. The version prefix (v4.local. or v4.public.) is compared against a hardcoded constant in your verification code — not a value from the token. There is no code path that accepts v4.none..


Attack 2 — Algorithm Confusion (RS256 → HS256)

What: Server expects RS256. Attacker takes the server's public key (which is public by design), uses it as the HMAC secret, and signs a forged token with HS256. Vulnerable libraries verify it as valid because the public key bytes match the HMAC input.

JWT status: Real and exploited. Required careful library-level mitigations.

VaultToken defense: There is no algorithm negotiation in PASETO. v4.public means Ed25519, always. The key type is enforced by how you call the library — AsymmetricSecretKey for signing, AsymmetricPublicKey for verification. There is no API surface for confusion.


Attack 3 — Payload Snooping

What: JWT payloads are Base64URL encoded, not encrypted. Anyone who sees the token — in logs, browser history, XSS output, HTTP access logs — can decode the payload and read all claims including roles, user IDs, and sensitive metadata.

JWT status: By design. JWE is required for encryption, but rarely used.

VaultToken defense: v4.local encrypts the payload with XChaCha20-Poly1305 (256-bit AEAD). The ciphertext reveals nothing without the symmetric key. Even the token length leaks minimal information.


Attack 4 — Brute-Force / Offline Cracking

What: HS256 allows password-derived secrets. Developers use "secret", "password", or short env vars. An attacker captures any valid token and runs hashcat offline — no server contact required.

JWT status: Very common in practice. Tools like jwt_tool and hashcat handle this automatically.

VaultToken defense: v4.local keys are 256-bit uniformly random — no API accepts a password. v4.public keys are Ed25519 keypairs generated by the library. There is no way to accidentally use a weak key.


Attack 5 — Footer / Header Injection

What: JWT's kid (Key ID) header is sometimes used to look up keys from a database or file. Attackers inject SQL (' OR 1=1--) or path traversal (../../etc/passwd) into this field.

JWT status: Exploited in several real systems.

VaultToken defense: The footer kid field is validated against a strict regex before use (/^key-v4[lp]-[0-9A-Z]{26}$/). No dynamic SQL or filesystem lookups from key IDs. Additionally, the footer is PAE-bound — any tampering with the footer bytes causes the AEAD tag to fail.


Attack 6 — Token Replay

What: A valid token captured in transit (MITM, compromised log, XSS) is replayed to impersonate the original user.

JWT status: Only mitigated by short exp values — no built-in replay prevention.

VaultToken defense (layered):

  1. Short exp (default 1 hour access tokens)
  2. JTI revocation on logout
  3. Implicit assertions (bind token to IP + User-Agent — stolen token fails from different context)
  4. Token family tracking for refresh tokens

Attack 7 — Refresh Token Theft

What: A refresh token is intercepted. Attacker uses it to silently obtain new access tokens indefinitely, even after the victim logs out.

JWT status: No standard mechanism for detection.

VaultToken defense — Token Family Tracking:

  • Every refresh token belongs to a family
  • A refresh token is consumed on use (rotation-on-use)
  • If the same refresh token is presented twice, the server knows one copy is stolen
  • The entire family is immediately revoked — both attacker and victim lose access
  • Victim is forced to re-authenticate, removing the attacker's foothold

Attack 8 — Timing Attacks on API Key Verification

What: Measure how long the server takes to reject API keys. Shorter rejection time for short mismatches can leak key length or prefix bytes.

VaultToken defense: API key comparison uses crypto.timingSafeEqual() — constant time regardless of where the mismatch occurs. Additionally, random per-request jitter (0–2ms) is added to verification responses.


Attack 9 — Key Exfiltration

What: Attacker gains read access to Redis (exposed port, misconfiguration) and extracts raw key material.

VaultToken defense: All symmetric and private key material is encrypted before writing to Redis using AES-256-GCM with a separate KEY_ENCRYPTION_SECRET. Redis only contains ciphertext. Without KEY_ENCRYPTION_SECRET, the Redis dump is useless.


Attack 10 — Cross-Tenant Token Acceptance

What: Token issued for tenant A is presented to tenant B's service and accepted.

VaultToken defense: Each tenant has a completely isolated key namespace in Redis. A token encrypted with tenant A's key will always fail the AEAD MAC check when verified under tenant B's key. This is structurally impossible to bypass — it is enforced by the cryptographic primitive, not application logic.


Cryptographic Primitives

v4.local — XChaCha20-Poly1305

Algorithm : XChaCha20-Poly1305 (IETF)
Key size  : 256 bits (32 bytes), uniformly random
Nonce size: 192 bits (24 bytes), randomly generated per token
Tag size  : 128 bits (16 bytes)
Security  : IND-CCA2 — ciphertext indistinguishable from random
Additional: PAE(header, nonce, footer, implicitAssertion)

Why XChaCha20 over AES-CBC or AES-GCM?

  • No padding oracle risk (stream cipher — no padding)
  • Constant-time — not vulnerable to cache-timing attacks on AES S-box
  • 192-bit nonce — collision probability negligible even with random nonce generation
  • Used in TLS 1.3, WireGuard, Signal Protocol — extensively audited
  • 64-bit nonce advantage over ChaCha20 — safer with large volumes of tokens

v4.public — Ed25519

Algorithm   : EdDSA over Curve25519 (Ed25519)
Secret key  : 32-byte seed (private scalar)
Public key  : 32 bytes
Signature   : 64 bytes
Security    : ~128-bit (equivalent) against best known attacks
Deterministic: Yes — no random nonce in signing, no nonce-reuse risk

Why Ed25519 over RSA-2048 or ECDSA P-256?

  • Deterministic — ECDSA with a repeated nonce leaks the private key (happened to Sony PlayStation)
  • Shorter keys and signatures than RSA — no performance tradeoff
  • Faster than RSA-2048 signing and ECDSA P-256 on modern hardware
  • Resistant to fault attacks — determinism means no timing variability to exploit
  • Twist-secure curve — resistant to invalid-curve attacks

Key Storage Security

Encryption at Rest

encrypt(rawKey):
  iv         = randomBytes(12)
  cipher     = AES-256-GCM(KEY_ENCRYPTION_SECRET, iv)
  ciphertext = cipher.encrypt(JSON.stringify(keyRecord))
  tag        = cipher.authTag()
  stored     = base64(iv || tag || ciphertext)

decrypt(stored):
  buf        = base64decode(stored)
  iv         = buf[0:12]
  tag        = buf[12:28]
  ciphertext = buf[28:]
  decipher   = AES-256-GCM(KEY_ENCRYPTION_SECRET, iv)
  decipher.setAuthTag(tag)
  keyRecord  = JSON.parse(decipher.decrypt(ciphertext))

KEY_ENCRYPTION_SECRET requirements:

  • 32 bytes (256-bit), uniformly random
  • Never stored alongside the encrypted keys
  • Managed via environment variable — injected by secrets manager in production
  • Rotated separately from PASETO keys on a different schedule

What is never stored in Redis (plaintext)

  • Raw symmetric key bytes
  • Ed25519 private key seed
  • KEY_ENCRYPTION_SECRET
  • Full token values
  • API keys (stored as HMAC-SHA256 hashes)

Security Headers

Every response includes:

Cache-Control: no-store
Pragma: no-cache
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'none'
Referrer-Policy: no-referrer

Cache-Control: no-store ensures tokens are never cached by proxies or browsers.


Reporting Security Issues

Do not open a public GitHub issue for security vulnerabilities.

Email: security@vaulttoken.dev

Include:

  • Description of the vulnerability
  • Steps to reproduce
  • Potential impact
  • Your suggested fix (optional)

We aim to respond within 48 hours and patch within 7 days for critical issues.

There aren’t any published security advisories