Description
Each encrypt() call in VaultCrypto generates a random 12-byte nonce via os.urandom(12). AES-GCM is catastrophically broken when a nonce is reused under the same key — the GHASH authentication key can be recovered, enabling universal ciphertext forgeries. With random 12-byte (96-bit) nonces, the birthday bound is ~2^48 encryptions. More critically, there is zero nonce tracking, collision detection, or counter-based generation. If the vault encrypts credentials across multiple deployments or long-lived instances, the risk of collision grows silently. A single collision silently corrupts the entire security model.
Affected Files
backend/secuscan/vault.py (line 51 — nonce generation)
Expected Behaviour
Nonce generation must guarantee uniqueness under a given key, e.g., via a monotonic counter or XChaCha20-Poly1305 (24-byte random nonce, no birthday problem at practical volumes). A collision should be detectable and preventable.
Actual Behaviour
Random nonces with no collision tracking or counter mechanism. A single collision silently corrupts the security model — the attacker can recover the GHASH authentication key and forge arbitrary ciphertexts, bypassing all encryption protections for stored credentials.
Proposed Fix
# vault.py — Replace random nonces with a monotonic counter + random padding
import struct
import threading
class VaultCrypto:
_NONCE_LEN = 12
def __init__(self, key: bytes):
# ... existing key setup ...
self._counter = 0
self._counter_lock = threading.Lock()
def _next_nonce(self) -> bytes:
"""Generate a unique nonce: 8-byte big-endian counter || 4-byte random."""
with self._counter_lock:
self._counter += 1
ctr_part = struct.pack(">Q", self._counter)
random_part = os.urandom(4)
return ctr_part + random_part
def encrypt(self, plaintext: str) -> str:
nonce = self._next_nonce()
ciphertext = self._aesgcm.encrypt(
nonce, plaintext.encode("utf-8"), None
)
blob = nonce + ciphertext
return base64.urlsafe_b64encode(blob).decode("ascii")
Alternatively, migrate to XChaCha20-Poly1305 which has a 24-byte nonce, making random nonce collisions practically impossible:
# vault.py — Alternative: use XChaCha20-Poly1305
from cryptography.hazmat.primitives.ciphers.aead import XChaCha20Poly1305
class VaultCrypto:
def __init__(self, key: bytes):
self._xchacha = XChaCha20Poly1305(key)
def encrypt(self, plaintext: str) -> str:
nonce = os.urandom(24) # 192-bit nonce — no birthday problem
ciphertext = self._xchacha.encrypt(
nonce, plaintext.encode("utf-8"), None
)
blob = nonce + ciphertext
return base64.urlsafe_b64encode(blob).decode("ascii")
def decrypt(self, token: str) -> str:
blob = base64.urlsafe_b64decode(token)
nonce = blob[:24]
ciphertext = blob[24:]
plaintext = self._xchacha.decrypt(nonce, ciphertext, None)
return plaintext.decode("utf-8")
Description
Each
encrypt()call inVaultCryptogenerates a random 12-byte nonce viaos.urandom(12). AES-GCM is catastrophically broken when a nonce is reused under the same key — the GHASH authentication key can be recovered, enabling universal ciphertext forgeries. With random 12-byte (96-bit) nonces, the birthday bound is ~2^48 encryptions. More critically, there is zero nonce tracking, collision detection, or counter-based generation. If the vault encrypts credentials across multiple deployments or long-lived instances, the risk of collision grows silently. A single collision silently corrupts the entire security model.Affected Files
backend/secuscan/vault.py(line 51 — nonce generation)Expected Behaviour
Nonce generation must guarantee uniqueness under a given key, e.g., via a monotonic counter or XChaCha20-Poly1305 (24-byte random nonce, no birthday problem at practical volumes). A collision should be detectable and preventable.
Actual Behaviour
Random nonces with no collision tracking or counter mechanism. A single collision silently corrupts the security model — the attacker can recover the GHASH authentication key and forge arbitrary ciphertexts, bypassing all encryption protections for stored credentials.
Proposed Fix
Alternatively, migrate to XChaCha20-Poly1305 which has a 24-byte nonce, making random nonce collisions practically impossible: