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.
- Include a Timestamp: Add the current timestamp to your message. This helps detect replayed messages that are older than an acceptable window.
- Use UTC time for consistency.
- Choose an appropriate tolerance (e.g., 5 minutes). Messages older than this tolerance should be rejected.
- Construct the Message: Combine the nonce, timestamp, and your actual data into a single message before encryption. The order is important; define it consistently. For example:
nonce || timestamp || data(where ‘||’ represents concatenation). - Encrypt the Message: Encrypt the combined message using FIDO2’s HMAC secret with a suitable symmetric cipher like AES in GCM mode.
- Decrypt and Verify: When decrypting, extract the nonce and timestamp from the message before decrypting the data.
- 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.
- Nonce Storage (Important): Store used nonces for a reasonable period to prevent reuse. A simple in-memory list is sufficient for low-volume applications, but consider a database or other persistent storage for higher security and scalability.
- 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.
# 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}
# 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')}")
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.

