Blog | G5 Cyber Security

FIDO2 HMAC Secret: Preventing Replay Attacks

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:

  1. 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.
  • Include a Timestamp: Add the current timestamp to your message. This helps detect replayed messages that are older than an acceptable window.
  • 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.
  • # 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}
    
  • Decrypt and Verify: When decrypting, extract the nonce and timestamp from the message before decrypting the 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')}")
    
  • 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.
  • Important Considerations

    Exit mobile version