Chapter 1.3 Quiz Cryptography in Network Security
Quiz Mode All answers are hidden under collapsible sections. Attempt each question before revealing the answer.
Question 1
A developer stores API keys by hashing them with SHA-256 before storing in the database. They argue this is as secure as bcrypt because "SHA-256 is a strong algorithm." Explain precisely why SHA-256 is the wrong choice for this use case, and what the correct approach is. Include specific attack rates.
Reveal Answer & Explanation
Answer: SHA-256 is wrong for credential storage because it is designed to be fast enabling brute-force at billions of hashes/second. bcrypt or Argon2 are correct because they are deliberately slow.
Explanation:
Speed comparison GPU brute force rates (RTX 4090):
| Algorithm | Hashes/second | Time to crack 8-char lowercase hash |
|---|---|---|
| MD5 | ~164 billion/sec | < 1 second |
| SHA-256 | ~23 billion/sec | ~2 seconds |
| bcrypt (cost 12) | ~184,000/sec | ~48 years |
| Argon2id | ~1,000/sec | Astronomical |
SHA-256 was designed for speed it is used to hash gigabytes of data quickly for integrity verification. 23 billion guesses/second means an 8-character API key using only lowercase letters (26^8 = 208 billion combinations) is cracked in under 10 seconds on a single GPU.
Additional problem no salt:
Without a salt (random bytes added to each credential before hashing), identical inputs produce identical outputs. An attacker can precompute rainbow tables billions of hash→plaintext mappings and look up any credential in the database instantly.
The correct approach:
# Correct: Argon2id with salt (Argon2 handles salt internally)
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # 3 iterations
memory_cost=65536, # 64MB RAM per hash GPU has limited VRAM per thread
parallelism=2, # 2 threads
hash_len=32,
salt_len=16
)
# Store this in the database
api_key_hash = ph.hash("sk-prod-xK9mN2pQr7vL")
# Verify: Argon2 extracts salt, parameters from the hash string automatically
is_valid = ph.verify(api_key_hash, "sk-prod-xK9mN2pQr7vL")
Better still for API keys: Hash them with HMAC-SHA256 using a server-side secret:
import hmac, hashlib, os, secrets
# Generate API key
raw_key = secrets.token_urlsafe(32) # Show to user once
# Store HMAC (keyed hash) in DB requires knowledge of SERVER_SECRET to crack
server_secret = os.environ['API_KEY_SECRET']
stored_hash = hmac.new(server_secret.encode(), raw_key.encode(), hashlib.sha256).hexdigest()
This uses a server-side key, meaning an attacker who steals the database cannot brute-force without also stealing the server secret.
Question 2
You are performing a TLS audit and run nmap --script ssl-enum-ciphers -p 443 target.com. The output includes the following cipher suites marked as grade C:
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_3DES_EDE_CBC_SHA
TLS_ECDHE_RSA_WITH_RC4_128_SHA
For each cipher suite, identify the specific weakness and explain the attack it enables.
Reveal Answer & Explanation
Answer: Each suite has a distinct cryptographic weakness.
Explanation:
Suite 1: TLS_RSA_WITH_AES_256_CBC_SHA
Weakness: No forward secrecy (static RSA key exchange) + CBC mode (padding oracle vulnerability)
TLS_RSA_prefix means the key exchange uses the server's static RSA key directly. If the server's RSA private key is ever compromised (breach, subpoena, leaked backup), every past session recorded can be decrypted.- CBC mode without authenticated encryption is vulnerable to BEAST (TLS 1.0) and padding oracle attacks (POODLE variant). If the TLS implementation doesn't verify padding in constant time, an attacker can decrypt individual bytes.
Suite 2: TLS_RSA_WITH_3DES_EDE_CBC_SHA
Weakness: 3DES is deprecated (Sweet32 attack) + no forward secrecy
- 3DES has a 64-bit block size. In CBC mode, after ~2^32 blocks (~32GB of data), birthday-bound collisions in block values occur. The Sweet32 attack (CVE-2016-2183) exploits these collisions to recover plaintext practical against long-lived sessions (HTTPS with keep-alive, VPN).
- 3DES effective security is only 112-bit due to meet-in-the-middle attacks on the three-key variant.
- NIST deprecated 3DES in 2017.
Suite 3: TLS_ECDHE_RSA_WITH_RC4_128_SHA
Weakness: RC4 is broken multiple plaintext recovery attacks
- RC4 has statistical biases in its keystream. Attacks like RC4 NOMORE (2015) recover cookies in HTTP in under 75 hours with ~2^26 sessions.
- The initial bytes of the RC4 keystream are heavily biased known-plaintext attacks (cookie values at known offsets) enable recovery.
- Note:
ECDHE_RSA_prefix means this suite does have forward secrecy but RC4 makes the encryption itself insecure regardless. - RFC 7465 (2015) prohibits RC4 in TLS.
Correct suites (TLS 1.2):
# Only these cipher suites should remain enabled for TLS 1.2:
ECDHE-ECDSA-AES256-GCM-SHA384
ECDHE-RSA-AES256-GCM-SHA384
ECDHE-ECDSA-CHACHA20-POLY1305
ECDHE-RSA-CHACHA20-POLY1305
ECDHE-ECDSA-AES128-GCM-SHA256
ECDHE-RSA-AES128-GCM-SHA256
# Requirements: ECDHE (forward secrecy) + GCM or POLY1305 (AEAD, no padding oracle)
Question 3
A security team discovers that their web application's JWT tokens are signed with "alg": "HS256" and the signing key is the application's database password. The tokens are validated with the following Python code:
import jwt
def validate_token(token):
decoded = jwt.decode(token, options={"verify_signature": False})
return decoded
Identify all cryptographic and implementation vulnerabilities in this setup.
Reveal Answer & Explanation
Answer: Three critical vulnerabilities signature verification disabled, weak/guessable key, and susceptibility to alg:none attack.
Explanation:
Vulnerability 1 Signature not verified (verify_signature: False)
This is the most critical issue. options={"verify_signature": False} tells the JWT library to skip all signature verification. Any attacker can craft any JWT with any claims and the server will accept it.
# Attacker crafts admin token zero cryptography needed
import base64, json
header = base64.urlsafe_b64encode(json.dumps({"alg":"HS256","typ":"JWT"}).encode()).rstrip(b'=')
payload = base64.urlsafe_b64encode(json.dumps({"sub":"attacker","role":"admin","uid":1}).encode()).rstrip(b'=')
forged_token = f"{header.decode()}.{payload.decode()}.FAKESIGNATURE"
# Server accepts this because verify_signature=False
Vulnerability 2 alg:none attack
Even if signature verification was enabled, if the server accepts whatever algorithm the JWT header specifies, an attacker can set "alg": "none" and omit the signature entirely. Some JWT libraries historically treated alg:none as valid.
# Forge token with alg:none
import base64, json
header = base64.urlsafe_b64encode(json.dumps({"alg":"none","typ":"JWT"}).encode()).rstrip(b'=')
payload = base64.urlsafe_b64encode(json.dumps({"sub":"admin","role":"superuser"}).encode()).rstrip(b'=')
token = f"{header.decode()}.{payload.decode()}." # No signature
Vulnerability 3 Database password as signing key
HS256 is HMAC-SHA256 the signing key must be kept secret, but:
- Database passwords appear in connection strings, config files, logs, environment variables, backups
- If the DB password is ever rotated (good practice), it invalidates all tokens
- Passwords chosen by humans are low-entropy brute-forceable
An attacker with a valid JWT signed with a weak key can crack it offline:
# Brute force HS256 JWT signing key with hashcat
hashcat -a 0 -m 16500 eyJhbGci...token.jwt rockyou.txt
# jwt_tool for JWT attacks
python3 jwt_tool.py eyJhbGci...token.jwt -C -d rockyou.txt # Crack key
python3 jwt_tool.py eyJhbGci...token.jwt -X a # alg:none attack
python3 jwt_tool.py eyJhbGci...token.jwt -I -pc role -pv admin # Inject claim
Correct implementation:
import jwt, os
from cryptography.hazmat.primitives.asymmetric import rsa
# Option 1: HS256 with proper random key
SECRET_KEY = os.environ['JWT_SECRET'] # Must be random, 256-bit, from secrets.token_bytes(32)
def validate_token_correct(token):
try:
decoded = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"], # Explicitly whitelist never accept 'none'
options={"require": ["exp", "iat", "sub"]} # Require standard claims
)
return decoded
except jwt.ExpiredSignatureError:
raise ValueError("Token expired")
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {e}")
# Option 2: RS256 with proper key pair (better for microservices public key can verify)
# Sign with private key, verify with public key only
Question 4
Explain what forward secrecy (perfect forward secrecy) means, why a server using only TLS_RSA_WITH_AES_256_GCM_SHA384 does NOT have forward secrecy, and demonstrate with a practical attack scenario where the lack of forward secrecy is exploited.
Reveal Answer & Explanation
Answer: Forward secrecy means past sessions remain secure even if the long-term private key is later compromised. Static RSA key exchange lacks this because the session key is protected only by the server's long-term RSA private key.
Explanation:
What forward secrecy means:
In a protocol with PFS, each session uses a unique ephemeral key that is destroyed after the session ends. Compromise of the long-term identity key after the fact cannot decrypt past sessions because the session key no longer exists.
Why TLS_RSA_WITH_AES_256_GCM_SHA384 lacks PFS:
In static RSA key exchange (the TLS_RSA_ prefix):
Client Server
────── ──────
Generate random pre-master secret (PMS)
Encrypt PMS with server's RSA public key ──────────────────────→
Decrypt PMS with RSA private key
Both derive session keys from PMS
The entire session's security depends on the secrecy of the server's static RSA private key. If an attacker:
- Records all TLS traffic today (passive collection)
- Compromises the server's RSA private key in the future (data breach, court order, insider)
- They can decrypt ALL past recorded sessions
Practical attack scenario the NSA/Nation-State Model:
2020: Attacker taps fiber link, records all encrypted traffic to bank.com
[bank.com uses TLS_RSA_WITH_AES_256_GCM_SHA384]
Attacker stores: ClientHello, ServerHello, EncryptedPreMasterSecret, all ciphertext
2024: Attacker breaches bank.com server (or buys the private key on dark web)
Obtains: bank.com RSA private key
Attack:
# Attacker uses the private key to decrypt the 2020 recorded sessions
# Using Wireshark with the private key:
# Edit → Preferences → Protocols → TLS → RSA keys list
# Add: bank.com IP, port 443, server.key
# Or using ssldump:
ssldump -r 2020_capture.pcap -k bank_private.key -d
# Output: all HTTP requests/responses from 2020 in plaintext
With ECDHE (PFS enabled):
Client Server
────── ──────
Generate ephemeral ECDH key pair Generate ephemeral ECDH key pair
(a_priv, a_pub) (b_priv, b_pub)
Send a_pub ──────────────────────────→ Receive a_pub
Receive b_pub ←────────────────────── Send b_pub (signed with RSA private key)
Compute: shared = ECDH(a_priv, b_pub) Compute: shared = ECDH(b_priv, a_pub)
Derive session keys from shared Derive session keys from shared
[After session ends: a_priv and b_priv are deleted]
Now if the RSA private key is compromised, the attacker can only verify the server's identity they cannot compute a_priv or b_priv (ephemeral, deleted) so they cannot derive the session keys.
Verification:
# Check if a server uses ephemeral key exchange (PFS)
openssl s_client -connect target.com:443 2>/dev/null | grep "Server Temp Key"
# With PFS: "Server Temp Key: X25519, 253 bits"
# Without PFS: [no Server Temp Key line using static RSA]
# Enforce PFS in cipher suite selection (remove all TLS_RSA_ suites)
# Any suite starting with ECDHE_ or DHE_ has PFS
openssl ciphers -v 'ECDHE+AESGCM:ECDHE+CHACHA20' | column -t