Reusing a Nonce, Key Pair in Encryption

Description

Reusing a Nonce, Key Pair in Encryption is a vulnerability that occurs when the same nonce (number used once) is used with the same key for multiple encryption operations. Nonces are designed to be unique for each encryption operation to ensure that even identical plaintexts produce different ciphertexts. When a nonce is reused with the same key, it can compromise the security of the encryption scheme, potentially allowing attackers to recover plaintext, forge messages, or perform replay attacks. This weakness is particularly critical in stream ciphers and authenticated encryption modes like AES-GCM where nonce reuse can be catastrophic.

Risk

Nonce reuse represents a critical cryptographic vulnerability with consequences varying by encryption mode. In stream ciphers and counter modes, reusing a nonce allows XOR of two ciphertexts to reveal XOR of plaintexts, enabling statistical analysis attacks. In AES-GCM, nonce reuse allows authentication tag forgery and can completely compromise the encryption scheme. Beyond cryptanalytic attacks, nonce reuse enables replay attacks where attackers can resend previously captured encrypted messages that will be accepted as valid. The risk is amplified in systems processing high volumes of messages where nonce collision probability increases. Real-world consequences have included complete compromise of encrypted communications and the ability to forge authenticated messages.

Solution

Ensure nonces are never reused with the same key through proper nonce generation and management. Use cryptographically secure random number generators for nonce generation when message volume is low enough that collision probability is negligible. Implement incrementing counters for nonces when message ordering is maintained, ensuring counters never wrap or reset. Use timestamp-based nonces combined with random components for distributed systems. Track used nonces to detect and reject duplicates when feasible. Consider nonce-misuse-resistant encryption schemes like AES-GCM-SIV for applications where nonce uniqueness cannot be guaranteed. Implement key rotation policies that limit the number of messages encrypted with any single key. Use separate keys for different contexts to isolate nonce spaces.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Nonce reuse can enable replay attacks where attackers resend previously captured encrypted data, impersonating legitimate users or repeating transactions.
Confidentiality, IntegrityScope: Confidentiality, Integrity

In many encryption modes, nonce reuse allows cryptanalytic attacks recovering plaintext or forging valid ciphertexts, compromising both confidentiality and message authenticity.

Example Code

Vulnerable Code (C/Python)

The following examples demonstrate nonce reuse vulnerabilities:

// Vulnerable: Hard-coded nonce reused for all encryptions
#include <openssl/evp.h>
#include <string.h>

// Vulnerable: Static nonce used for all encryptions
static const unsigned char NONCE[12] = {
    0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
    0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b
};

static const unsigned char KEY[32] = { /* ... */ };

int vulnerable_encrypt_password(const char *password,
                                unsigned char *ciphertext) {
    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();

    // Vulnerable: Same nonce used every time!
    EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, KEY, NONCE);

    int len;
    EVP_EncryptUpdate(ctx, ciphertext, &len,
                      (unsigned char*)password, strlen(password));

    EVP_EncryptFinal_ex(ctx, ciphertext + len, &len);

    EVP_CIPHER_CTX_free(ctx);

    // All passwords encrypted with same nonce - can be XORed to reveal data!
    return 0;
}

// Vulnerable: Nonce reused for remote commands
int vulnerable_send_command(const char *command,
                            unsigned char *encrypted_command) {

    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();

    // Vulnerable: Static nonce allows replay attacks
    EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, KEY, NONCE);

    int len;
    EVP_EncryptUpdate(ctx, encrypted_command, &len,
                      (unsigned char*)command, strlen(command));

    // Attacker can capture and replay any command!

    EVP_CIPHER_CTX_free(ctx);
    return len;
}
# Vulnerable: Nonce reuse in Python encryption
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

class VulnerableEncryption:
    def __init__(self, key):
        self.aesgcm = AESGCM(key)
        # Vulnerable: Same nonce for all operations
        self.nonce = b'\x00' * 12

    def encrypt(self, plaintext):
        # Vulnerable: Reusing nonce!
        return self.aesgcm.encrypt(self.nonce, plaintext, None)

    def decrypt(self, ciphertext):
        return self.aesgcm.decrypt(self.nonce, ciphertext, None)

# Vulnerable: Counter that wraps around
class VulnerableCounter:
    def __init__(self, key):
        self.aesgcm = AESGCM(key)
        self.counter = 0  # Will eventually overflow and wrap

    def encrypt(self, plaintext):
        # Vulnerable: Counter can wrap after 2^32 encryptions
        nonce = self.counter.to_bytes(12, 'big')
        self.counter = (self.counter + 1) % (2**32)  # Wraps!

        return self.aesgcm.encrypt(nonce, plaintext, None)

# Vulnerable: Predictable nonce based on timestamp
import time

class VulnerablePredictableNonce:
    def __init__(self, key):
        self.aesgcm = AESGCM(key)

    def encrypt(self, plaintext):
        # Vulnerable: Timestamp-only nonce can collide
        # Multiple encryptions in same second reuse nonce!
        nonce = int(time.time()).to_bytes(12, 'big')

        return self.aesgcm.encrypt(nonce, plaintext, None)
// Vulnerable: Java nonce reuse
import javax.crypto.*;
import javax.crypto.spec.*;
import java.util.Arrays;

public class VulnerableEncryption {

    private final SecretKey key;
    // Vulnerable: Static nonce
    private static final byte[] STATIC_NONCE = new byte[12];

    public VulnerableEncryption(SecretKey key) {
        this.key = key;
    }

    // Vulnerable: Same nonce for every encryption
    public byte[] encrypt(byte[] plaintext) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec parameterSpec = new GCMParameterSpec(128, STATIC_NONCE);

        cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
        return cipher.doFinal(plaintext);
    }

    // Vulnerable: Counter initialized to same value on restart
    private int counter = 0;

    public byte[] encryptWithCounter(byte[] plaintext) throws Exception {
        // Vulnerable: Counter resets on application restart
        byte[] nonce = new byte[12];
        nonce[0] = (byte) (counter >> 24);
        nonce[1] = (byte) (counter >> 16);
        nonce[2] = (byte) (counter >> 8);
        nonce[3] = (byte) counter;
        counter++;

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, nonce));
        return cipher.doFinal(plaintext);
        // After restart, counter starts at 0 again - nonce reuse!
    }
}

Fixed Code (C/Python)

// Fixed: Unique nonce generation
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <string.h>
#include <stdint.h>

typedef struct {
    unsigned char key[32];
    uint64_t counter;
    unsigned char instance_id[4];  // Random per-instance identifier
} SecureContext;

int init_secure_context(SecureContext *ctx, const unsigned char *key) {
    memcpy(ctx->key, key, 32);
    ctx->counter = 0;

    // Fixed: Generate random instance ID to prevent cross-instance collision
    if (RAND_bytes(ctx->instance_id, 4) != 1) {
        return -1;
    }

    return 0;
}

int secure_encrypt(SecureContext *ctx, const unsigned char *plaintext,
                   size_t plaintext_len, unsigned char *ciphertext) {

    unsigned char nonce[12];

    // Fixed: Construct unique nonce from instance_id + counter
    memcpy(nonce, ctx->instance_id, 4);
    nonce[4] = (ctx->counter >> 56) & 0xFF;
    nonce[5] = (ctx->counter >> 48) & 0xFF;
    nonce[6] = (ctx->counter >> 40) & 0xFF;
    nonce[7] = (ctx->counter >> 32) & 0xFF;
    nonce[8] = (ctx->counter >> 24) & 0xFF;
    nonce[9] = (ctx->counter >> 16) & 0xFF;
    nonce[10] = (ctx->counter >> 8) & 0xFF;
    nonce[11] = ctx->counter & 0xFF;

    // Fixed: Increment counter for next use
    ctx->counter++;

    // Fixed: Check for counter overflow - time to rotate key!
    if (ctx->counter == 0) {
        return -1;  // Counter wrapped - need new key
    }

    EVP_CIPHER_CTX *cipher_ctx = EVP_CIPHER_CTX_new();
    EVP_EncryptInit_ex(cipher_ctx, EVP_aes_256_gcm(), NULL, ctx->key, nonce);

    // Prepend nonce to ciphertext
    memcpy(ciphertext, nonce, 12);

    int len;
    EVP_EncryptUpdate(cipher_ctx, ciphertext + 12, &len,
                      plaintext, plaintext_len);

    int ciphertext_len = len;
    EVP_EncryptFinal_ex(cipher_ctx, ciphertext + 12 + len, &len);
    ciphertext_len += len;

    // Get authentication tag
    unsigned char tag[16];
    EVP_CIPHER_CTX_ctrl(cipher_ctx, EVP_CTRL_GCM_GET_TAG, 16, tag);
    memcpy(ciphertext + 12 + ciphertext_len, tag, 16);

    EVP_CIPHER_CTX_free(cipher_ctx);

    return 12 + ciphertext_len + 16;  // nonce + ciphertext + tag
}
# Fixed: Unique nonce generation in Python
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
import struct
import threading

class SecureEncryption:
    def __init__(self, key):
        self.aesgcm = AESGCM(key)
        self.lock = threading.Lock()
        self.counter = 0
        # Fixed: Random instance identifier
        self.instance_id = os.urandom(4)

    def encrypt(self, plaintext, associated_data=None):
        with self.lock:
            # Fixed: Construct unique nonce from instance_id + counter
            nonce = self.instance_id + struct.pack('>Q', self.counter)

            # Fixed: Increment counter
            self.counter += 1

            # Fixed: Check for counter overflow
            if self.counter >= 2**64:
                raise OverflowError("Counter overflow - rotate key!")

        ciphertext = self.aesgcm.encrypt(nonce, plaintext, associated_data)

        # Fixed: Prepend nonce to ciphertext for decryption
        return nonce + ciphertext

    def decrypt(self, nonce_and_ciphertext, associated_data=None):
        # Fixed: Extract nonce from message
        nonce = nonce_and_ciphertext[:12]
        ciphertext = nonce_and_ciphertext[12:]

        return self.aesgcm.decrypt(nonce, ciphertext, associated_data)

# Fixed: Random nonce generation for low-volume use
class SecureRandomNonce:
    def __init__(self, key):
        self.aesgcm = AESGCM(key)

    def encrypt(self, plaintext, associated_data=None):
        # Fixed: Random nonce for each encryption
        # Safe for < 2^32 messages with same key (birthday bound)
        nonce = os.urandom(12)

        ciphertext = self.aesgcm.encrypt(nonce, plaintext, associated_data)
        return nonce + ciphertext

    def decrypt(self, nonce_and_ciphertext, associated_data=None):
        nonce = nonce_and_ciphertext[:12]
        ciphertext = nonce_and_ciphertext[12:]
        return self.aesgcm.decrypt(nonce, ciphertext, associated_data)

# Fixed: Nonce-misuse resistant encryption
from cryptography.hazmat.primitives.ciphers.aead import AESGCMSIV

class MisuseResistantEncryption:
    def __init__(self, key):
        # Fixed: Use nonce-misuse resistant mode
        self.cipher = AESGCMSIV(key)

    def encrypt(self, plaintext, associated_data=None):
        # Still use unique nonces, but accidental reuse is less catastrophic
        nonce = os.urandom(12)
        ciphertext = self.cipher.encrypt(nonce, plaintext, associated_data)
        return nonce + ciphertext
// Fixed: Java secure nonce handling
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.util.concurrent.atomic.AtomicLong;

public class SecureEncryption {

    private final SecretKey key;
    private final AtomicLong counter;
    private final byte[] instanceId;

    public SecureEncryption(SecretKey key) throws Exception {
        this.key = key;
        this.counter = new AtomicLong(0);

        // Fixed: Random instance identifier
        this.instanceId = new byte[4];
        SecureRandom.getInstanceStrong().nextBytes(instanceId);
    }

    public byte[] encrypt(byte[] plaintext) throws Exception {
        // Fixed: Atomic counter increment
        long count = counter.getAndIncrement();

        // Fixed: Check for overflow
        if (count < 0) {
            throw new IllegalStateException("Counter overflow - rotate key");
        }

        // Fixed: Construct unique nonce
        byte[] nonce = new byte[12];
        System.arraycopy(instanceId, 0, nonce, 0, 4);
        for (int i = 0; i < 8; i++) {
            nonce[11 - i] = (byte) (count >> (i * 8));
        }

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, nonce));
        byte[] ciphertext = cipher.doFinal(plaintext);

        // Fixed: Prepend nonce to ciphertext
        byte[] result = new byte[12 + ciphertext.length];
        System.arraycopy(nonce, 0, result, 0, 12);
        System.arraycopy(ciphertext, 0, result, 12, ciphertext.length);

        return result;
    }
}

The fix ensures unique nonces through counters, random generation, or a combination of both.


Exploited in the Wild

WPA2 KRACK Attack (WiFi, 2017)

The KRACK (Key Reinstallation Attack) exploited nonce reuse in the WPA2 protocol's four-way handshake, allowing attackers to decrypt WiFi traffic.

Stream Cipher Nonce Reuse (Various, Historical)

Multiple implementations of stream ciphers have been compromised through nonce reuse, allowing XOR attacks to recover plaintext.


Tools to Test/Exploit


CVE Examples


References

  1. MITRE Corporation. "CWE-323: Reusing a Nonce, Key Pair in Encryption." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/323.html

  2. Rogaway, P. "Nonce-Based Symmetric Encryption." https://web.cs.ucdavis.edu/~rogaway/papers/nonce.pdf

  3. NIST. "Recommendation for Block Cipher Modes of Operation." SP 800-38D. https://csrc.nist.gov/publications/detail/sp/800-38d/final