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
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Nonce reuse can enable replay attacks where attackers resend previously captured encrypted data, impersonating legitimate users or repeating transactions. |
| Confidentiality, Integrity | Scope: 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
-
Cryptanalysis Tools — Tools for analyzing encryption implementations.
-
Custom Scripts — XOR analysis scripts for detecting nonce reuse.
CVE Examples
-
CVE-2017-13077 — WPA2 KRACK attack enabling nonce reuse.
-
CVE-2020-8597 — PPP daemon buffer overflow via EAP.
References
-
MITRE Corporation. "CWE-323: Reusing a Nonce, Key Pair in Encryption." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/323.html
-
Rogaway, P. "Nonce-Based Symmetric Encryption." https://web.cs.ucdavis.edu/~rogaway/papers/nonce.pdf
-
NIST. "Recommendation for Block Cipher Modes of Operation." SP 800-38D. https://csrc.nist.gov/publications/detail/sp/800-38d/final