Critical severity9.1OSV Advisory· Published Nov 21, 2025· Updated Apr 15, 2026
CVE-2025-64767
CVE-2025-64767
Description
hpke-js is a Hybrid Public Key Encryption (HPKE) module built on top of Web Cryptography API. Prior to version 1.7.5, the public SenderContext Seal() API has a race condition which allows for the same AEAD nonce to be re-used for multiple Seal() calls. This can lead to complete loss of Confidentiality and Integrity of the produced messages. This issue has been patched in version 1.7.5.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@hpke/corenpm | < 1.7.5 | 1.7.5 |
Affected products
1Patches
194a767c9b9f3Merge branch 'advisory-fix-1'
4 files changed · +148 −0
packages/core/src/mutex.ts+14 −0 added@@ -0,0 +1,14 @@ +export class Mutex { + #locked: Promise<void> = Promise.resolve(); + + async lock(): Promise<() => void> { + let releaseLock!: () => void; + const nextLock = new Promise<void>((resolve) => { + releaseLock = resolve; + }); + const previousLock = this.#locked; + this.#locked = nextLock; + await previousLock; + return releaseLock; + } +}
packages/core/src/recipientContext.ts+7 −0 modified@@ -1,17 +1,24 @@ import { EMPTY, OpenError } from "@hpke/common"; import { EncryptionContextImpl } from "./encryptionContext.ts"; +import { Mutex } from "./mutex.ts"; export class RecipientContextImpl extends EncryptionContextImpl { + #mutex?: Mutex; + override async open( data: ArrayBuffer, aad: ArrayBuffer = EMPTY.buffer as ArrayBuffer, ): Promise<ArrayBuffer> { + this.#mutex ??= new Mutex(); + const release = await this.#mutex.lock(); let pt: ArrayBuffer; try { pt = await this._ctx.key.open(this.computeNonce(this._ctx), data, aad); } catch (e: unknown) { throw new OpenError(e); + } finally { + release(); } this.incrementSeq(this._ctx); return pt;
packages/core/src/senderContext.ts+6 −0 modified@@ -4,10 +4,12 @@ import { EMPTY, SealError } from "@hpke/common"; import type { AeadParams } from "./interfaces/aeadParams.ts"; import type { Encapsulator } from "./interfaces/encapsulator.ts"; import { EncryptionContextImpl } from "./encryptionContext.ts"; +import { Mutex } from "./mutex.ts"; export class SenderContextImpl extends EncryptionContextImpl implements Encapsulator { public readonly enc: ArrayBuffer; + #mutex?: Mutex; constructor( api: SubtleCrypto, @@ -23,11 +25,15 @@ export class SenderContextImpl extends EncryptionContextImpl data: ArrayBuffer, aad: ArrayBuffer = EMPTY.buffer as ArrayBuffer, ): Promise<ArrayBuffer> { + this.#mutex ??= new Mutex(); + const release = await this.#mutex.lock(); let ct: ArrayBuffer; try { ct = await this._ctx.key.seal(this.computeNonce(this._ctx), data, aad); } catch (e: unknown) { throw new SealError(e); + } finally { + release(); } this.incrementSeq(this._ctx); return ct;
packages/core/test/sample.test.ts+121 −0 modified@@ -949,6 +949,127 @@ describe("README examples", () => { }); }); + describe("Nonce reuse", () => { + it("should not allow nonce reuse", async () => { + const suite = new CipherSuite({ + kem: new DhkemP256HkdfSha256(), + kdf: new HkdfSha256(), + aead: new Aes128Gcm(), + }); + + const keypair = await suite.kem.generateKeyPair(); + const skR = keypair.privateKey; + const pkR = keypair.publicKey; + + const sender = await suite.createSenderContext({ + recipientPublicKey: pkR, + }); + + const [message0, message1] = await Promise.all([ + sender.seal( + new TextEncoder().encode("Secret message 1: Attack at dawn").buffer, + ), + sender.seal( + new TextEncoder().encode("Secret message 2: Withdraw troops").buffer, + ), + ]); + + const recipient = await suite.createRecipientContext({ + recipientKey: skR, + enc: sender.enc, + }); + + const plaintext0 = await recipient.open(message0); + console.log( + "✓ Decrypted message seq=0:", + new TextDecoder().decode(plaintext0), + ); + + try { + console.log( + "✓ Decrypted message seq=1:", + new TextDecoder().decode(await recipient.open(message1)), + ); + console.log( + "\n✓ nonce-reuse reproduction completed, code is NOT vulnerable", + ); + } catch (_err) { + // re-sequence the recipient to verify same nonce was used for two messages + (recipient as unknown as { _ctx: { seq: number } })._ctx.seq = 0; + console.log( + "❌ Decrypted a different message with seq=0", + new TextDecoder().decode(await recipient.open(message1)), + ); + + console.log( + "\n✓ nonce-reuse reproduction completed, code is vulnerable, nonces are reused when concurrent calls to .seal() are used", + ); + } + + // Test that failed Open() doesn't increment sequence + const recipient2 = await suite.createRecipientContext({ + recipientKey: skR, + enc: sender.enc, + }); + + const invalidMessage = new Uint8Array(message0.byteLength); + invalidMessage.set(new Uint8Array(message0)); + invalidMessage[0] ^= 0xff; // Corrupt the first byte + + try { + await recipient2.open(invalidMessage.buffer); + } catch (_err: unknown) { + // ignore + } + + // Now try to open the first valid message - should still work with seq=0 + try { + await recipient2.open(message0); + console.log( + "✓ Successfully decrypted message with seq=0 after failed open()", + ); + console.log("✓ Failed open() did NOT increment sequence"); + } catch (err: unknown) { + console.log("❌ Failed to decrypt message:", err); + } + + // Test that same message produces same ciphertext due to nonce reuse + const sender2 = await suite.createSenderContext({ + recipientPublicKey: pkR, + }); + + const sameMessage = new TextEncoder().encode("Identical message").buffer; + const [cipher0, cipher1] = await Promise.all([ + sender2.seal(sameMessage), + sender2.seal(sameMessage), + ]); + + const cipher0Array = new Uint8Array(cipher0); + const cipher1Array = new Uint8Array(cipher1); + + let identical = true; + if (cipher0Array.length !== cipher1Array.length) { + identical = false; + } else { + for (let i = 0; i < cipher0Array.length; i++) { + if (cipher0Array[i] !== cipher1Array[i]) { + identical = false; + break; + } + } + } + assertEquals(identical, false); + // if (identical) { + // console.log( + // "\n❌ Same message produced IDENTICAL ciphertext (nonce reuse confirmed)", + // ); + // } else { + // console.log( + // "\n✓ Same message produced different ciphertext (nonces are unique)", + // ); + // } + }); + }); // describe("Oblivious HTTP with DhkemP256HkdfSha256/HkdfSha256/Aes128Gcm", () => { // it("should work normally", async () => { // const te = new TextEncoder();
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-73g8-5h73-26h4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-64767ghsaADVISORY
- github.com/dajiaji/hpke-js/blob/b7fd3592c7c08660c98289d67c6bb7f891af75c4/packages/core/src/senderContext.tsnvdWEB
- github.com/dajiaji/hpke-js/commit/94a767c9b9f37ce48d5cd86f7017d8cacd294aafnvdWEB
- github.com/dajiaji/hpke-js/security/advisories/GHSA-73g8-5h73-26h4nvdWEB
News mentions
0No linked articles in our index yet.