TL;DR
Using FIDO2’s HMAC secret for data encryption is efficient but vulnerable to replay attacks. This guide shows how to add a nonce (a unique, random number) and timestamp to your encrypted messages to prevent attackers from reusing old ciphertexts.
Understanding the Problem
FIDO2 authenticators generate an HMAC secret shared with the relying party. You might use this secret for symmetric encryption (e.g., using AES). However, if you simply encrypt data with the same key repeatedly, an attacker could record a valid ciphertext and replay it later to gain unauthorized access or perform unintended actions. This is a replay attack.
Solution: Add Nonces and Timestamps
To prevent replay attacks, include both a nonce and a timestamp in your encryption process. Here’s how:
- Generate a Unique Nonce: A nonce (Number used Once) is a random or pseudo-random value that should be unique for each encryption operation using the same key.
- Use a cryptographically secure random number generator (CSPRNG).
- The nonce size depends on your chosen cipher, but 128 bits is common and generally sufficient.
- Use UTC time for consistency.
- Choose an appropriate tolerance (e.g., 5 minutes). Messages older than this tolerance should be rejected.
nonce || timestamp || data (where ‘||’ represents concatenation).# Example using Python and cryptography library
from cryptography.fernet import Fernet
import time
import os
key = b'YOUR_HMAC_SECRET'
f = Fernet(key)
nonce = os.urandom(16) # 128-bit nonce
timestamp = int(time.time())
data = b'Sensitive data to encrypt'
message = nonce + str(timestamp).encode('utf-8') + data
ciphertext = f.encrypt(message)
print(f"Ciphertext: {ciphertext}
- Verify that the timestamp is within your acceptable tolerance window.
- Check if the nonce has been used previously (store a list of recently used nonces). This prevents an attacker from re-using a valid, but old, nonce with new data.
# Example decryption and verification in Python
ciphertext = b'YOUR_CIPHERTEXT'
decrypted_message = f.decrypt(ciphertext)
nonce, timestamp_str, data = decrypted_message.split(b'|', 2) # Assuming '|' as separator
timestamp = int(timestamp_str.decode('utf-8'))
current_time = int(time.time())
tolerance = 300 # 5 minutes in seconds
if current_time - timestamp > tolerance:
print("Message is too old, rejecting.")
else:
print(f"Decrypted data: {data.decode('utf-8')}")
- The length of the nonce storage depends on your application’s throughput and acceptable risk. If you generate nonces quickly, you’ll need to store more recent ones.
Important Considerations
- Clock Synchronization: Ensure that the clocks of the relying party and any other involved systems are synchronized (e.g., using NTP). Significant clock skew can lead to false positives in timestamp verification.
- Cipher Mode: Use an authenticated encryption mode like AES-GCM, which provides both confidentiality and integrity protection. This helps detect tampering with the ciphertext.
- Key Rotation: Regularly rotate your FIDO2 HMAC secret to limit the impact of a potential key compromise.