Skip to content

Commit 0bc213b

Browse files
authored
fix(ext/node): prevent cipher operations after finalize (#31533)
Throw a state error for operations if the cipher is already finalized.
1 parent 63fe089 commit 0bc213b

File tree

2 files changed

+87
-0
lines changed

2 files changed

+87
-0
lines changed

ext/node/polyfills/internal/crypto/cipher.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ export class Cipheriv extends Transform implements Cipher {
184184

185185
#autoPadding = true;
186186

187+
#finalized = false;
188+
187189
constructor(
188190
cipher: string,
189191
key: CipherKey,
@@ -220,6 +222,10 @@ export class Cipheriv extends Transform implements Cipher {
220222
}
221223

222224
final(encoding: string = getDefaultEncoding()): Buffer | string {
225+
if (this.#finalized) {
226+
throw new ERR_CRYPTO_INVALID_STATE("final");
227+
}
228+
223229
this.#lazyInitDecoder(encoding);
224230

225231
const buf = new FastBuffer(16);
@@ -229,6 +235,7 @@ export class Cipheriv extends Transform implements Cipher {
229235
if (hasNoBufferedData && !shouldPadEmptyBlock) {
230236
const maybeTag = op_node_cipheriv_take(this.#context);
231237
if (maybeTag) this.#authTag = Buffer.from(maybeTag);
238+
this.#finalized = true;
232239
return encoding === "buffer" ? Buffer.from([]) : "";
233240
}
234241

@@ -243,9 +250,11 @@ export class Cipheriv extends Transform implements Cipher {
243250
);
244251
if (maybeTag) {
245252
this.#authTag = Buffer.from(maybeTag);
253+
this.#finalized = true;
246254
return encoding === "buffer" ? Buffer.from([]) : "";
247255
}
248256

257+
this.#finalized = true;
249258
if (encoding !== "buffer") {
250259
return this.#decoder!.end(buf);
251260
}
@@ -280,6 +289,10 @@ export class Cipheriv extends Transform implements Cipher {
280289
inputEncoding?: Encoding,
281290
outputEncoding: Encoding = getDefaultEncoding(),
282291
): Buffer | string {
292+
if (this.#finalized) {
293+
throw new ERR_CRYPTO_INVALID_STATE("update");
294+
}
295+
283296
// TODO(kt3k): throw ERR_INVALID_ARG_TYPE if data is not string, Buffer, or ArrayBufferView
284297
let buf = data;
285298
if (typeof data === "string") {
@@ -401,6 +414,8 @@ export class Decipheriv extends Transform implements Cipher {
401414

402415
#authTag?: BinaryLike;
403416

417+
#finalized = false;
418+
404419
constructor(
405420
cipher: string,
406421
key: CipherKey,
@@ -437,6 +452,10 @@ export class Decipheriv extends Transform implements Cipher {
437452
}
438453

439454
final(encoding: string = getDefaultEncoding()): Buffer | string {
455+
if (this.#finalized) {
456+
throw new ERR_CRYPTO_INVALID_STATE("final");
457+
}
458+
440459
this.#lazyInitDecoder(encoding);
441460

442461
let buf = new FastBuffer(16);
@@ -449,13 +468,15 @@ export class Decipheriv extends Transform implements Cipher {
449468
);
450469

451470
if (!this.#needsBlockCache || this.#cache.cache.byteLength === 0) {
471+
this.#finalized = true;
452472
return encoding === "buffer" ? Buffer.from([]) : "";
453473
}
454474
if (this.#cache.cache.byteLength != 16) {
455475
throw new Error("Invalid final block size");
456476
}
457477

458478
buf = buf.subarray(0, 16 - buf.at(-1)); // Padded in Pkcs7 mode
479+
this.#finalized = true;
459480
if (encoding !== "buffer") {
460481
return this.#decoder!.end(buf);
461482
}
@@ -490,6 +511,10 @@ export class Decipheriv extends Transform implements Cipher {
490511
inputEncoding?: Encoding,
491512
outputEncoding: Encoding = getDefaultEncoding(),
492513
): Buffer | string {
514+
if (this.#finalized) {
515+
throw new ERR_CRYPTO_INVALID_STATE("update");
516+
}
517+
493518
// TODO(kt3k): throw ERR_INVALID_ARG_TYPE if data is not string, Buffer, or ArrayBufferView
494519
let buf = data;
495520
if (typeof data === "string") {

tests/unit_node/crypto/crypto_cipher_test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,65 @@ Deno.test({
592592
assertEquals(otherEncrypted.length, 0);
593593
},
594594
});
595+
596+
Deno.test({
597+
name: "createCipheriv - cipher lockdown after final()",
598+
fn() {
599+
const key = crypto.randomBytes(32);
600+
const iv = crypto.randomBytes(16);
601+
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
602+
603+
// Call final() to lock down the cipher
604+
cipher.final();
605+
606+
assertThrows(
607+
() => {
608+
cipher.update("test data");
609+
},
610+
Error,
611+
"Invalid state for operation update",
612+
);
613+
614+
assertThrows(
615+
() => {
616+
cipher.final();
617+
},
618+
Error,
619+
"Invalid state for operation final",
620+
);
621+
},
622+
});
623+
624+
Deno.test({
625+
name: "createDecipheriv - decipher lockdown after final()",
626+
fn() {
627+
const key = crypto.randomBytes(32);
628+
const iv = crypto.randomBytes(16);
629+
630+
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
631+
const encrypted = Buffer.concat([
632+
cipher.update("test data"),
633+
cipher.final(),
634+
]);
635+
636+
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
637+
decipher.update(encrypted);
638+
decipher.final();
639+
640+
assertThrows(
641+
() => {
642+
decipher.update(encrypted);
643+
},
644+
Error,
645+
"Invalid state for operation update",
646+
);
647+
648+
assertThrows(
649+
() => {
650+
decipher.final();
651+
},
652+
Error,
653+
"Invalid state for operation final",
654+
);
655+
},
656+
});

0 commit comments

Comments
 (0)