encrypt() reuses one AES-GCM nonce for up to ivInterval (32 000) messages under the same key.
Measured on 0.0.2, four consecutive encrypt() calls:
iv 1: 7ae680c83d569711275044e8
iv 2: 7ae680c83d569711275044e8
iv 3: 7ae680c83d569711275044e8
iv 4: 7ae680c83d569711275044e8
src/aes-gcm-pbkdf2.js derives the nonce inside deriveEncryptionKey, which runs only when there is no key yet or when count % ivInterval === 0. Every message in between is encrypted under the same (key, nonce) pair.
Impact
A repeated nonce in AES-GCM is not a tunable cost. Two messages under one (key, nonce) disclose the XOR of their plaintexts, and the repeat allows the GHASH authentication subkey to be recovered — so ciphertexts become forgeable as well as readable. For an OrbitDB database this means an attacker holding the log can both read entries and mint entries that authenticate correctly.
The 32 000-message rotation does not bound this: one repeat is already sufficient.
Second defect, in the same path
decrypt() caches the derived key against the nonce:
if (!decryptionKey || nonce.toString() !== decryptionNonce?.toString()) {
The derived key depends on the salt, not the nonce. This is currently harmless only because the nonce never changes; with a per-message nonce it would re-run PBKDF2 (32 767 iterations) for every entry.
Fix
A fresh nonce per encrypt(), with the PBKDF2 key and salt still cached — the derivation is the expensive part, the nonce is not — and the decrypt cache keyed on the salt. PR follows.
test/iv.test.js asserts rotation at the interval; it needs to assert nonce uniqueness per message instead.
encrypt()reuses one AES-GCM nonce for up toivInterval(32 000) messages under the same key.Measured on 0.0.2, four consecutive
encrypt()calls:src/aes-gcm-pbkdf2.jsderives the nonce insidederiveEncryptionKey, which runs only when there is no key yet or whencount % ivInterval === 0. Every message in between is encrypted under the same(key, nonce)pair.Impact
A repeated nonce in AES-GCM is not a tunable cost. Two messages under one
(key, nonce)disclose the XOR of their plaintexts, and the repeat allows the GHASH authentication subkey to be recovered — so ciphertexts become forgeable as well as readable. For an OrbitDB database this means an attacker holding the log can both read entries and mint entries that authenticate correctly.The 32 000-message rotation does not bound this: one repeat is already sufficient.
Second defect, in the same path
decrypt()caches the derived key against the nonce:The derived key depends on the salt, not the nonce. This is currently harmless only because the nonce never changes; with a per-message nonce it would re-run PBKDF2 (32 767 iterations) for every entry.
Fix
A fresh nonce per
encrypt(), with the PBKDF2 key and salt still cached — the derivation is the expensive part, the nonce is not — and the decrypt cache keyed on the salt. PR follows.test/iv.test.jsasserts rotation at the interval; it needs to assert nonce uniqueness per message instead.