From 9d0161356b4c38bbc40254ab80d228e6b5515e1d Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Tue, 14 Mar 2023 15:59:23 +0900 Subject: [PATCH] fix(ext/node): add crypto.createCipheriv (#18091) --- Cargo.lock | 2 + Cargo.toml | 2 + cli/tests/unit_node/crypto_cipher_test.ts | 24 ++++ ext/crypto/Cargo.toml | 4 +- ext/node/Cargo.toml | 2 + ext/node/crypto/cipher.rs | 108 ++++++++++++++++++ ext/node/crypto/mod.rs | 44 ++++++++ ext/node/lib.rs | 3 + ext/node/polyfills/internal/crypto/cipher.ts | 113 ++++++++++--------- 9 files changed, 247 insertions(+), 55 deletions(-) create mode 100644 ext/node/crypto/cipher.rs diff --git a/Cargo.lock b/Cargo.lock index 59cfe5611e..83681a9f4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1244,6 +1244,8 @@ dependencies = [ name = "deno_node" version = "0.29.0" dependencies = [ + "aes", + "cbc", "deno_core", "digest 0.10.6", "hex", diff --git a/Cargo.toml b/Cargo.toml index 32823b0b96..62051cf239 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ deno_websocket = { version = "0.97.0", path = "./ext/websocket" } deno_webstorage = { version = "0.87.0", path = "./ext/webstorage" } deno_napi = { version = "0.22.0", path = "./ext/napi" } +aes = "=0.8.2" anyhow = "1.0.57" async-trait = "0.1.51" atty = "=0.2.14" @@ -84,6 +85,7 @@ base64 = "=0.13.1" bencher = "0.1" bytes = "1.4.0" cache_control = "=0.2.0" +cbc = { version = "=0.1.2", features = ["alloc"] } console_static_text = "=0.7.1" data-url = "=0.2.0" dlopen = "0.1.8" diff --git a/cli/tests/unit_node/crypto_cipher_test.ts b/cli/tests/unit_node/crypto_cipher_test.ts index e3bd7ca262..5fbaa548d0 100644 --- a/cli/tests/unit_node/crypto_cipher_test.ts +++ b/cli/tests/unit_node/crypto_cipher_test.ts @@ -48,3 +48,27 @@ Deno.test({ ); }, }); + +Deno.test({ + name: "createCipheriv - basic", + fn() { + const cipher = crypto.createCipheriv( + "aes-128-cbc", + new Uint8Array(16), + new Uint8Array(16), + ); + assertEquals( + cipher.update(new Uint8Array(16), undefined, "hex"), + "66e94bd4ef8a2c3b884cfa59ca342b2e", + ); + assertEquals( + cipher.update(new Uint8Array(19), undefined, "hex"), + "f795bd4a52e29ed713d313fa20e98dbc", + ); + assertEquals( + cipher.update(new Uint8Array(55), undefined, "hex"), + "a10cf66d0fddf3405370b4bf8df5bfb347c78395e0d8ae2194da0a90abc9888a94ee48f6c78fcd518a941c3896102cb1", + ); + assertEquals(cipher.final("hex"), "e11901dde4a2f99fe4efc707e48c6aed"); + }, +}); diff --git a/ext/crypto/Cargo.toml b/ext/crypto/Cargo.toml index 45ef6c7a93..3e9dd91351 100644 --- a/ext/crypto/Cargo.toml +++ b/ext/crypto/Cargo.toml @@ -14,12 +14,12 @@ description = "Web Cryptography API implementation for Deno" path = "lib.rs" [dependencies] -aes = "0.8.1" +aes.workspace = true aes-gcm = "0.10" aes-kw = { version = "0.2.1", features = ["alloc"] } base64.workspace = true block-modes = "0.9.1" -cbc = { version = "0.1.2", features = ["alloc"] } +cbc.workspace = true const-oid = "0.9.0" ctr = "0.9.1" # https://github.com/dalek-cryptography/curve25519-dalek/pull/397 diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 9afd1452cb..c6ffcac482 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -14,6 +14,8 @@ description = "Node compatibility for Deno" path = "lib.rs" [dependencies] +aes.workspace = true +cbc.workspace = true deno_core.workspace = true digest = { version = "0.10.5", features = ["core-api", "std"] } hex = "0.4.3" diff --git a/ext/node/crypto/cipher.rs b/ext/node/crypto/cipher.rs new file mode 100644 index 0000000000..0833cb5918 --- /dev/null +++ b/ext/node/crypto/cipher.rs @@ -0,0 +1,108 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use aes::cipher::block_padding::Pkcs7; +use aes::cipher::BlockEncryptMut; +use aes::cipher::KeyIvInit; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::Resource; + +use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +enum Cipher { + Aes128Cbc(Box>), + // TODO(kt3k): add more algorithms Aes192Cbc, Aes256Cbc, Aes128ECB, Aes128GCM, etc. +} + +enum Decipher { + // TODO(kt3k): implement Deciphers + // Aes128Cbc(Box>), +} + +pub struct CipherContext { + cipher: Rc>, +} + +pub struct DecipherContext { + _decipher: Rc>, +} + +impl CipherContext { + pub fn new(algorithm: &str, key: &[u8], iv: &[u8]) -> Result { + Ok(Self { + cipher: Rc::new(RefCell::new(Cipher::new(algorithm, key, iv)?)), + }) + } + + pub fn encrypt(&self, input: &[u8], output: &mut [u8]) { + self.cipher.borrow_mut().encrypt(input, output); + } + + pub fn r#final( + self, + input: &[u8], + output: &mut [u8], + ) -> Result<(), AnyError> { + Rc::try_unwrap(self.cipher) + .map_err(|_| type_error("Cipher context is already in use"))? + .into_inner() + .r#final(input, output) + } +} + +impl Resource for CipherContext { + fn name(&self) -> Cow { + "cryptoCipher".into() + } +} + +impl Resource for DecipherContext { + fn name(&self) -> Cow { + "cryptoDecipher".into() + } +} + +impl Cipher { + fn new( + algorithm_name: &str, + key: &[u8], + iv: &[u8], + ) -> Result { + use Cipher::*; + Ok(match algorithm_name { + "aes-128-cbc" => { + Aes128Cbc(Box::new(cbc::Encryptor::new(key.into(), iv.into()))) + } + _ => return Err(type_error(format!("Unknown cipher {algorithm_name}"))), + }) + } + + /// encrypt encrypts the data in the middle of the input. + fn encrypt(&mut self, input: &[u8], output: &mut [u8]) { + use Cipher::*; + match self { + Aes128Cbc(encryptor) => { + assert!(input.len() % 16 == 0); + for (input, output) in input.chunks(16).zip(output.chunks_mut(16)) { + encryptor.encrypt_block_b2b_mut(input.into(), output.into()); + } + } + } + } + + /// r#final encrypts the last block of the input data. + fn r#final(self, input: &[u8], output: &mut [u8]) -> Result<(), AnyError> { + assert!(input.len() < 16); + use Cipher::*; + match self { + Aes128Cbc(encryptor) => { + let _ = (*encryptor) + .encrypt_padded_b2b_mut::(input, output) + .map_err(|_| type_error("Cannot pad the input data"))?; + Ok(()) + } + } + } +} diff --git a/ext/node/crypto/mod.rs b/ext/node/crypto/mod.rs index f9a61b4561..3e6af9b4b3 100644 --- a/ext/node/crypto/mod.rs +++ b/ext/node/crypto/mod.rs @@ -15,6 +15,7 @@ use rsa::PublicKey; use rsa::RsaPrivateKey; use rsa::RsaPublicKey; +mod cipher; mod digest; #[op(fast)] @@ -153,3 +154,46 @@ pub fn op_node_public_encrypt( _ => Err(type_error("Unknown padding")), } } + +#[op(fast)] +pub fn op_node_create_cipheriv( + state: &mut OpState, + algorithm: &str, + key: &[u8], + iv: &[u8], +) -> u32 { + state.resource_table.add( + match cipher::CipherContext::new(algorithm, key, iv) { + Ok(context) => context, + Err(_) => return 0, + }, + ) +} + +#[op(fast)] +pub fn op_node_cipheriv_encrypt( + 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.encrypt(input, output); + true +} + +#[op] +pub fn op_node_cipheriv_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 1c9d9e0aac..318667d59b 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -102,6 +102,9 @@ fn ext_polyfill() -> ExtensionBuilder { fn ops_polyfill(ext: &mut ExtensionBuilder) -> &mut ExtensionBuilder { ext.ops(vec![ + crypto::op_node_cipheriv_encrypt::decl(), + crypto::op_node_cipheriv_final::decl(), + crypto::op_node_create_cipheriv::decl(), crypto::op_node_create_hash::decl(), crypto::op_node_hash_update::decl(), crypto::op_node_hash_update_str::decl(), diff --git a/ext/node/polyfills/internal/crypto/cipher.ts b/ext/node/polyfills/internal/crypto/cipher.ts index 34776e3abe..b2d24947ac 100644 --- a/ext/node/polyfills/internal/crypto/cipher.ts +++ b/ext/node/polyfills/internal/crypto/cipher.ts @@ -10,12 +10,13 @@ import { Buffer } from "ext:deno_node/buffer.ts"; import { notImplemented } from "ext:deno_node/_utils.ts"; import type { TransformOptions } from "ext:deno_node/_stream.d.ts"; import { Transform } from "ext:deno_node/_stream.mjs"; -import { KeyObject } from "ext:deno_node/internal/crypto/keys.ts"; +import { KeyObject } from "./keys.ts"; import type { BufferEncoding } from "ext:deno_node/_global.d.ts"; import type { BinaryLike, Encoding, } from "ext:deno_node/internal/crypto/types.ts"; +import { getDefaultEncoding } from "ext:deno_node/internal/crypto/util.ts"; const { ops } = globalThis.__bootstrap.core; @@ -42,21 +43,13 @@ export interface CipherOCBOptions extends TransformOptions { } export interface Cipher extends ReturnType { - update(data: BinaryLike): Buffer; - update(data: string, inputEncoding: Encoding): Buffer; - update( - data: ArrayBufferView, - inputEncoding: undefined, - outputEncoding: Encoding, - ): string; update( data: string, - inputEncoding: Encoding | undefined, - outputEncoding: Encoding, + inputEncoding?: Encoding, + outputEncoding?: Encoding, ): string; - final(): Buffer; - final(outputEncoding: BufferEncoding): string; + final(outputEncoding?: BufferEncoding): string; setAutoPadding(autoPadding?: boolean): this; } @@ -124,21 +117,27 @@ export interface DecipherOCB extends Decipher { } export class Cipheriv extends Transform implements Cipher { - constructor( - _cipher: string, - _key: CipherKey, - _iv: BinaryLike | null, - _options?: TransformOptions, - ) { - super(); + /** CipherContext resource id */ + #context: number; - notImplemented("crypto.Cipheriv"); + /** plaintext data cache */ + #cache: BlockModeCache; + + constructor( + cipher: string, + key: CipherKey, + iv: BinaryLike | null, + options?: TransformOptions, + ) { + super(options); + this.#cache = new BlockModeCache(); + this.#context = ops.op_node_create_cipheriv(cipher, key, iv); } - final(): Buffer; - final(outputEncoding: BufferEncoding): string; - final(_outputEncoding?: string): Buffer | string { - notImplemented("crypto.Cipheriv.prototype.final"); + final(encoding: string = getDefaultEncoding()): Buffer | string { + const buf = new Buffer(16); + ops.op_node_cipheriv_final(this.#context, this.#cache.cache, buf); + return encoding === "buffer" ? buf : buf.toString(encoding); } getAuthTag(): Buffer { @@ -152,30 +151,52 @@ export class Cipheriv extends Transform implements Cipher { }, ): this { notImplemented("crypto.Cipheriv.prototype.setAAD"); + return this; } setAutoPadding(_autoPadding?: boolean): this { notImplemented("crypto.Cipheriv.prototype.setAutoPadding"); + return this; } - update(data: BinaryLike): Buffer; - update(data: string, inputEncoding: Encoding): Buffer; update( - data: ArrayBufferView, - inputEncoding: undefined, - outputEncoding: Encoding, - ): string; - update( - data: string, - inputEncoding: Encoding | undefined, - outputEncoding: Encoding, - ): string; - update( - _data: string | BinaryLike | ArrayBufferView, + data: string | Buffer | ArrayBufferView, + // TODO(kt3k): Handle inputEncoding _inputEncoding?: Encoding, - _outputEncoding?: Encoding, + outputEncoding: Encoding = getDefaultEncoding(), ): Buffer | string { - notImplemented("crypto.Cipheriv.prototype.update"); + this.#cache.add(data); + const input = this.#cache.get(); + const output = new Buffer(input.length); + ops.op_node_cipheriv_encrypt(this.#context, input, output); + return outputEncoding === "buffer" + ? output + : output.toString(outputEncoding); + } +} + +/** Caches data and output the chunk of multiple of 16. + * Used by CBC, ECB modes of block ciphers */ +class BlockModeCache { + constructor() { + this.cache = new Uint8Array(0); + } + + add(data: Uint8Array) { + const cache = this.cache; + this.cache = new Uint8Array(cache.length + data.length); + this.cache.set(cache); + this.cache.set(data, cache.length); + } + + get(): Uint8Array { + if (this.cache.length < 16) { + return null; + } + const len = Math.floor(this.cache.length / 16) * 16; + const out = this.cache.subarray(0, len); + this.cache = this.cache.subarray(len); + return out; } } @@ -191,8 +212,6 @@ export class Decipheriv extends Transform implements Cipher { notImplemented("crypto.Decipheriv"); } - final(): Buffer; - final(outputEncoding: BufferEncoding): string; final(_outputEncoding?: string): Buffer | string { notImplemented("crypto.Decipheriv.prototype.final"); } @@ -214,18 +233,6 @@ export class Decipheriv extends Transform implements Cipher { notImplemented("crypto.Decipheriv.prototype.setAutoPadding"); } - update(data: BinaryLike): Buffer; - update(data: string, inputEncoding: Encoding): Buffer; - update( - data: ArrayBufferView, - inputEncoding: undefined, - outputEncoding: Encoding, - ): string; - update( - data: string, - inputEncoding: Encoding | undefined, - outputEncoding: Encoding, - ): string; update( _data: string | BinaryLike | ArrayBufferView, _inputEncoding?: Encoding,