fix: reject failed decompression with JWEInvalid error

This commit is contained in:
Filip Skokan
2026-03-18 22:16:09 +01:00
parent 1945e98934
commit d88fee07fc
4 changed files with 41 additions and 6 deletions
+4 -1
View File
@@ -252,7 +252,10 @@ export async function flattenedDecrypt(
) {
throw new TypeError('maxDecompressedLength must be 0, a positive safe integer, or Infinity')
}
result.plaintext = await decompress(plaintext, maxDecompressedLength)
result.plaintext = await decompress(plaintext, maxDecompressedLength).catch((cause) => {
if (cause instanceof JWEInvalid) throw cause
throw new JWEInvalid('Failed to decompress plaintext', { cause })
})
}
if (jwe.protected !== undefined) {
+3 -1
View File
@@ -266,7 +266,9 @@ export class FlattenedEncrypt {
let plaintext = this.#plaintext
if (joseHeader.zip === 'DEF') {
plaintext = await compress(plaintext)
plaintext = await compress(plaintext).catch((cause) => {
throw new JWEInvalid('Failed to compress plaintext', { cause })
})
}
const { ciphertext, tag, iv } = await encrypt(enc, plaintext, cek, this.#iv, additionalData)
+4 -4
View File
@@ -14,8 +14,8 @@ export async function compress(input: Uint8Array): Promise<Uint8Array> {
const cs = new CompressionStream('deflate-raw')
const writer = cs.writable.getWriter()
writer.write(input as Uint8Array<ArrayBuffer>)
writer.close()
writer.write(input as Uint8Array<ArrayBuffer>).catch(() => {})
writer.close().catch(() => {})
const chunks: Uint8Array[] = []
const reader = cs.readable.getReader()
@@ -33,8 +33,8 @@ export async function decompress(input: Uint8Array, maxLength: number): Promise<
const ds = new DecompressionStream('deflate-raw')
const writer = ds.writable.getWriter()
writer.write(input as Uint8Array<ArrayBuffer>)
writer.close()
writer.write(input as Uint8Array<ArrayBuffer>).catch(() => {})
writer.close().catch(() => {})
const chunks: Uint8Array[] = []
let length = 0
+30
View File
@@ -1,4 +1,5 @@
import test from 'ava'
import * as crypto from 'node:crypto'
import {
FlattenedEncrypt,
@@ -130,6 +131,35 @@ if (hasCompressionStreams) {
})
}
})
test('invalid deflate data is properly rejected', async (t) => {
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 })
const cek = crypto.randomBytes(32)
const iv = crypto.randomBytes(12)
const protectedHeader = { alg: 'RSA-OAEP-256', enc: 'A256GCM', zip: 'DEF' }
const protectedB64 = Buffer.from(JSON.stringify(protectedHeader)).toString('base64url')
const cipher = crypto.createCipheriv('aes-256-gcm', cek, iv, { authTagLength: 16 })
cipher.setAAD(Buffer.from(protectedB64))
const ciphertext = Buffer.concat([cipher.update(new Uint8Array([0x00])), cipher.final()])
const tag = cipher.getAuthTag()
const encryptedKey = crypto.publicEncrypt({ key: publicKey, oaepHash: 'sha256' }, cek)
const flattened = {
protected: protectedB64,
encrypted_key: Buffer.from(encryptedKey).toString('base64url'),
iv: Buffer.from(iv).toString('base64url'),
ciphertext: Buffer.from(ciphertext).toString('base64url'),
tag: Buffer.from(tag).toString('base64url'),
}
await t.throwsAsync(flattenedDecrypt(flattened, privateKey), {
code: 'ERR_JWE_INVALID',
message: 'Failed to decompress plaintext',
})
t.pass()
})
}
test('unsupported zip value on encrypt throws JOSENotSupported', async (t) => {