diff --git a/cli/tests/unit_node/crypto_cipher_test.ts b/cli/tests/unit_node/crypto_cipher_test.ts index 5fbaa548d0..b14d149f5e 100644 --- a/cli/tests/unit_node/crypto_cipher_test.ts +++ b/cli/tests/unit_node/crypto_cipher_test.ts @@ -72,3 +72,41 @@ Deno.test({ assertEquals(cipher.final("hex"), "e11901dde4a2f99fe4efc707e48c6aed"); }, }); + +Deno.test({ + name: "createCipheriv - input encoding", + fn() { + const cipher = crypto.createCipheriv( + "aes-128-cbc", + new Uint8Array(16), + new Uint8Array(16), + ); + assertEquals( + cipher.update("hello, world! hello, world!", "utf-8", "hex"), + "ca7df4d74f51b77a7440ead38343ab0f", + ); + assertEquals(cipher.final("hex"), "d0da733dec1fa61125c80a6f97e6166e"); + }, +}); + +Deno.test({ + name: "createDecipheriv - basic", + fn() { + const decipher = crypto.createDecipheriv( + "aes-128-cbc", + new Uint8Array(16), + new Uint8Array(16), + ); + assertEquals( + decipher.update( + "66e94bd4ef8a2c3b884cfa59ca342b2ef795bd4a52e29ed713d313fa20e98dbca10cf66d0fddf3405370b4bf8df5bfb347c78395e0d8ae2194da0a90abc9888a94ee48f6c78fcd518a941c3896102cb1e11901dde4a2f99fe4efc707e48c6aed", + "hex", + ), + Buffer.alloc(80), + ); + assertEquals( + decipher.final(), + Buffer.alloc(10), // Checks the padding + ); + }, +}); diff --git a/ext/node/crypto/cipher.rs b/ext/node/crypto/cipher.rs index 0833cb5918..54cd611329 100644 --- a/ext/node/crypto/cipher.rs +++ b/ext/node/crypto/cipher.rs @@ -1,6 +1,7 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use aes::cipher::block_padding::Pkcs7; +use aes::cipher::BlockDecryptMut; use aes::cipher::BlockEncryptMut; use aes::cipher::KeyIvInit; use deno_core::error::type_error; @@ -17,8 +18,8 @@ enum Cipher { } enum Decipher { - // TODO(kt3k): implement Deciphers - // Aes128Cbc(Box>), + Aes128Cbc(Box>), + // TODO(kt3k): add more algorithms Aes192Cbc, Aes256Cbc, Aes128ECB, Aes128GCM, etc. } pub struct CipherContext { @@ -26,7 +27,7 @@ pub struct CipherContext { } pub struct DecipherContext { - _decipher: Rc>, + decipher: Rc>, } impl CipherContext { @@ -52,6 +53,29 @@ impl CipherContext { } } +impl DecipherContext { + pub fn new(algorithm: &str, key: &[u8], iv: &[u8]) -> Result { + Ok(Self { + decipher: Rc::new(RefCell::new(Decipher::new(algorithm, key, iv)?)), + }) + } + + pub fn decrypt(&self, input: &[u8], output: &mut [u8]) { + self.decipher.borrow_mut().decrypt(input, output); + } + + pub fn r#final( + self, + input: &[u8], + output: &mut [u8], + ) -> Result<(), AnyError> { + Rc::try_unwrap(self.decipher) + .map_err(|_| type_error("Decipher context is already in use"))? + .into_inner() + .r#final(input, output) + } +} + impl Resource for CipherContext { fn name(&self) -> Cow { "cryptoCipher".into() @@ -106,3 +130,46 @@ impl Cipher { } } } + +impl Decipher { + fn new( + algorithm_name: &str, + key: &[u8], + iv: &[u8], + ) -> Result { + use Decipher::*; + Ok(match algorithm_name { + "aes-128-cbc" => { + Aes128Cbc(Box::new(cbc::Decryptor::new(key.into(), iv.into()))) + } + _ => return Err(type_error(format!("Unknown cipher {algorithm_name}"))), + }) + } + + /// decrypt decrypts the data in the middle of the input. + fn decrypt(&mut self, input: &[u8], output: &mut [u8]) { + use Decipher::*; + match self { + Aes128Cbc(decryptor) => { + assert!(input.len() % 16 == 0); + for (input, output) in input.chunks(16).zip(output.chunks_mut(16)) { + decryptor.decrypt_block_b2b_mut(input.into(), output.into()); + } + } + } + } + + /// r#final decrypts the last block of the input data. + fn r#final(self, input: &[u8], output: &mut [u8]) -> Result<(), AnyError> { + assert!(input.len() == 16); + use Decipher::*; + match self { + Aes128Cbc(decryptor) => { + let _ = (*decryptor) + .decrypt_padded_b2b_mut::(input, output) + .map_err(|_| type_error("Cannot unpad the input data"))?; + Ok(()) + } + } + } +} diff --git a/ext/node/crypto/mod.rs b/ext/node/crypto/mod.rs index 3e6af9b4b3..53d064d863 100644 --- a/ext/node/crypto/mod.rs +++ b/ext/node/crypto/mod.rs @@ -197,3 +197,46 @@ pub fn op_node_cipheriv_final( .map_err(|_| type_error("Cipher context is already in use"))?; context.r#final(input, output) } + +#[op(fast)] +pub fn op_node_create_decipheriv( + state: &mut OpState, + algorithm: &str, + key: &[u8], + iv: &[u8], +) -> u32 { + state.resource_table.add( + match cipher::DecipherContext::new(algorithm, key, iv) { + Ok(context) => context, + Err(_) => return 0, + }, + ) +} + +#[op(fast)] +pub fn op_node_decipheriv_decrypt( + state: &mut OpState, + rid: u32, + input: &[u8], + output: &mut [u8], +) -> bool { + let context = match state.resource_table.get::(rid) { + Ok(context) => context, + Err(_) => return false, + }; + context.decrypt(input, output); + true +} + +#[op] +pub fn op_node_decipheriv_final( + state: &mut OpState, + rid: u32, + input: &[u8], + output: &mut [u8], +) -> Result<(), AnyError> { + let context = state.resource_table.take::(rid)?; + let context = Rc::try_unwrap(context) + .map_err(|_| type_error("Cipher context is already in use"))?; + context.r#final(input, output) +} diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 23a003bc83..d2d6b15c9d 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -96,10 +96,13 @@ fn op_node_build_os() -> String { deno_core::extension!(deno_node, deps = [ deno_io, deno_fs ], ops = [ + crypto::op_node_create_decipheriv, crypto::op_node_cipheriv_encrypt, crypto::op_node_cipheriv_final, crypto::op_node_create_cipheriv, crypto::op_node_create_hash, + crypto::op_node_decipheriv_decrypt, + crypto::op_node_decipheriv_final, crypto::op_node_hash_update, crypto::op_node_hash_update_str, crypto::op_node_hash_digest, diff --git a/ext/node/polyfills/internal/crypto/cipher.ts b/ext/node/polyfills/internal/crypto/cipher.ts index b2d24947ac..d40978cf6d 100644 --- a/ext/node/polyfills/internal/crypto/cipher.ts +++ b/ext/node/polyfills/internal/crypto/cipher.ts @@ -130,7 +130,7 @@ export class Cipheriv extends Transform implements Cipher { options?: TransformOptions, ) { super(options); - this.#cache = new BlockModeCache(); + this.#cache = new BlockModeCache(false); this.#context = ops.op_node_create_cipheriv(cipher, key, iv); } @@ -161,14 +161,23 @@ export class Cipheriv extends Transform implements Cipher { update( data: string | Buffer | ArrayBufferView, - // TODO(kt3k): Handle inputEncoding - _inputEncoding?: Encoding, + inputEncoding?: Encoding, outputEncoding: Encoding = getDefaultEncoding(), ): Buffer | string { - this.#cache.add(data); + // TODO(kt3k): throw ERR_INVALID_ARG_TYPE if data is not string, Buffer, or ArrayBufferView + if (typeof data === "string" && typeof inputEncoding === "string") { + this.#cache.add(Buffer.from(data, inputEncoding)); + } else { + this.#cache.add(data); + } const input = this.#cache.get(); - const output = new Buffer(input.length); - ops.op_node_cipheriv_encrypt(this.#context, input, output); + let output; + if (input === null) { + output = Buffer.alloc(0); + } else { + output = Buffer.allocUnsafe(input.length); + ops.op_node_cipheriv_encrypt(this.#context, input, output); + } return outputEncoding === "buffer" ? output : output.toString(outputEncoding); @@ -178,8 +187,13 @@ export class Cipheriv extends Transform implements Cipher { /** Caches data and output the chunk of multiple of 16. * Used by CBC, ECB modes of block ciphers */ class BlockModeCache { - constructor() { + cache: Uint8Array; + // The last chunk can be padded when decrypting. + #lastChunkIsNonZero: boolean; + + constructor(lastChunkIsNotZero = false) { this.cache = new Uint8Array(0); + this.#lastChunkIsNonZero = lastChunkIsNotZero; } add(data: Uint8Array) { @@ -189,11 +203,19 @@ class BlockModeCache { this.cache.set(data, cache.length); } - get(): Uint8Array { - if (this.cache.length < 16) { + /** Gets the chunk of the length of largest multiple of 16. + * Used for preparing data for encryption/decryption */ + get(): Uint8Array | null { + let len = this.cache.length; + if (this.#lastChunkIsNonZero) { + // Reduces the available chunk length by 1 to keep the last chunk + len -= 1; + } + if (len < 16) { return null; } - const len = Math.floor(this.cache.length / 16) * 16; + + len = Math.floor(len / 16) * 16; const out = this.cache.subarray(0, len); this.cache = this.cache.subarray(len); return out; @@ -201,19 +223,28 @@ class BlockModeCache { } export class Decipheriv extends Transform implements Cipher { - constructor( - _cipher: string, - _key: CipherKey, - _iv: BinaryLike | null, - _options?: TransformOptions, - ) { - super(); + /** DecipherContext resource id */ + #context: number; - notImplemented("crypto.Decipheriv"); + /** ciphertext data cache */ + #cache: BlockModeCache; + + constructor( + cipher: string, + key: CipherKey, + iv: BinaryLike | null, + options?: TransformOptions, + ) { + super(options); + this.#cache = new BlockModeCache(true); + this.#context = ops.op_node_create_decipheriv(cipher, key, iv); } - final(_outputEncoding?: string): Buffer | string { - notImplemented("crypto.Decipheriv.prototype.final"); + final(encoding: string = getDefaultEncoding()): Buffer | string { + let buf = new Buffer(16); + ops.op_node_decipheriv_final(this.#context, this.#cache.cache, buf); + buf = buf.subarray(0, 16 - buf.at(-1)); // Padded in Pkcs7 mode + return encoding === "buffer" ? buf : buf.toString(encoding); } setAAD( @@ -234,11 +265,27 @@ export class Decipheriv extends Transform implements Cipher { } update( - _data: string | BinaryLike | ArrayBufferView, - _inputEncoding?: Encoding, - _outputEncoding?: Encoding, + data: string | Buffer | ArrayBufferView, + inputEncoding?: Encoding, + outputEncoding: Encoding = getDefaultEncoding(), ): Buffer | string { - notImplemented("crypto.Decipheriv.prototype.update"); + // TODO(kt3k): throw ERR_INVALID_ARG_TYPE if data is not string, Buffer, or ArrayBufferView + if (typeof data === "string" && typeof inputEncoding === "string") { + this.#cache.add(Buffer.from(data, inputEncoding)); + } else { + this.#cache.add(data); + } + const input = this.#cache.get(); + let output; + if (input === null) { + output = Buffer.alloc(0); + } else { + output = new Buffer(input.length); + ops.op_node_decipheriv_decrypt(this.#context, input, output); + } + return outputEncoding === "buffer" + ? output + : output.toString(outputEncoding); } }