VYPR
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.

PackageAffected versionsPatched versions
@hpke/corenpm
< 1.7.51.7.5

Affected products

1

Patches

1
94a767c9b9f3

Merge branch 'advisory-fix-1'

https://github.com/dajiaji/hpke-jsAjitomi DaisukeNov 19, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.