diff --git a/cli/tests/node_compat/config.jsonc b/cli/tests/node_compat/config.jsonc index 2146daf926..8631efcad1 100644 --- a/cli/tests/node_compat/config.jsonc +++ b/cli/tests/node_compat/config.jsonc @@ -45,6 +45,7 @@ "test-child-process-stdout-flush-exit.js", "test-child-process-stdout-flush.js", "test-console-instance.js", + "test-crypto-dh.js", "test-crypto-hkdf.js", "test-crypto-hmac.js", "test-crypto-prime.js", @@ -239,6 +240,7 @@ "test-console-sync-write-error.js", "test-console-table.js", "test-console-tty-colors.js", + "test-crypto-dh.js", "test-crypto-hkdf.js", "test-crypto-hmac.js", "test-crypto-prime.js", diff --git a/cli/tests/node_compat/test/parallel/test-crypto-dh.js b/cli/tests/node_compat/test/parallel/test-crypto-dh.js new file mode 100644 index 0000000000..b436207ac4 --- /dev/null +++ b/cli/tests/node_compat/test/parallel/test-crypto-dh.js @@ -0,0 +1,214 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 18.12.1 +// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +const size = common.hasFipsCrypto || common.hasOpenSSL3 ? 1024 : 256; +const dh1 = crypto.createDiffieHellman(size); +const p1 = dh1.getPrime('buffer'); +const dh2 = crypto.createDiffieHellman(p1, 'buffer'); +const key1 = dh1.generateKeys(); +const key2 = dh2.generateKeys('hex'); +const secret1 = dh1.computeSecret(key2, 'hex', 'base64'); +const secret2 = dh2.computeSecret(key1, 'latin1', 'buffer'); + +// Test Diffie-Hellman with two parties sharing a secret, +// using various encodings as we go along +assert.strictEqual(secret2.toString('base64'), secret1); +assert.strictEqual(dh1.verifyError, 0); +assert.strictEqual(dh2.verifyError, 0); + +// https://github.com/nodejs/node/issues/32738 +// XXX(bnoordhuis) validateInt32() throwing ERR_OUT_OF_RANGE and RangeError +// instead of ERR_INVALID_ARG_TYPE and TypeError is questionable, IMO. +assert.throws(() => crypto.createDiffieHellman(13.37), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "sizeOrKey" is out of range. ' + + 'It must be an integer. Received 13.37', +}); + +assert.throws(() => crypto.createDiffieHellman('abcdef', 13.37), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "generator" is out of range. ' + + 'It must be an integer. Received 13.37', +}); + +for (const bits of [-1, 0, 1]) { + if (common.hasOpenSSL3) { + assert.throws(() => crypto.createDiffieHellman(bits), { + code: 'ERR_OSSL_DH_MODULUS_TOO_SMALL', + name: 'Error', + message: /modulus too small/, + }); + } else { + assert.throws(() => crypto.createDiffieHellman(bits), { + code: 'ERR_OSSL_BN_BITS_TOO_SMALL', + name: 'Error', + message: /bits too small/, + }); + } +} + +// Through a fluke of history, g=0 defaults to DH_GENERATOR (2). +{ + const g = 0; + crypto.createDiffieHellman('abcdef', g); + crypto.createDiffieHellman('abcdef', 'hex', g); +} + +for (const g of [-1, 1]) { + const ex = { + code: 'ERR_OSSL_DH_BAD_GENERATOR', + name: 'Error', + message: /bad generator/, + }; + assert.throws(() => crypto.createDiffieHellman('abcdef', g), ex); + assert.throws(() => crypto.createDiffieHellman('abcdef', 'hex', g), ex); +} + +crypto.createDiffieHellman('abcdef', Buffer.from([2])); // OK + +for (const g of [Buffer.from([]), + Buffer.from([0]), + Buffer.from([1])]) { + const ex = { + code: 'ERR_OSSL_DH_BAD_GENERATOR', + name: 'Error', + message: /bad generator/, + }; + assert.throws(() => crypto.createDiffieHellman('abcdef', g), ex); + assert.throws(() => crypto.createDiffieHellman('abcdef', 'hex', g), ex); +} + +[ + [0x1, 0x2], + () => { }, + /abc/, + {}, +].forEach((input) => { + assert.throws( + () => crypto.createDiffieHellman(input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); +}); + +// Create "another dh1" using generated keys from dh1, +// and compute secret again +const dh3 = crypto.createDiffieHellman(p1, 'buffer'); +const privkey1 = dh1.getPrivateKey(); +dh3.setPublicKey(key1); +dh3.setPrivateKey(privkey1); + +assert.deepStrictEqual(dh1.getPrime(), dh3.getPrime()); +assert.deepStrictEqual(dh1.getGenerator(), dh3.getGenerator()); +assert.deepStrictEqual(dh1.getPublicKey(), dh3.getPublicKey()); +assert.deepStrictEqual(dh1.getPrivateKey(), dh3.getPrivateKey()); +assert.strictEqual(dh3.verifyError, 0); + +const secret3 = dh3.computeSecret(key2, 'hex', 'base64'); + +assert.strictEqual(secret1, secret3); + +// computeSecret works without a public key set at all. +const dh4 = crypto.createDiffieHellman(p1, 'buffer'); +dh4.setPrivateKey(privkey1); + +assert.deepStrictEqual(dh1.getPrime(), dh4.getPrime()); +assert.deepStrictEqual(dh1.getGenerator(), dh4.getGenerator()); +assert.deepStrictEqual(dh1.getPrivateKey(), dh4.getPrivateKey()); +assert.strictEqual(dh4.verifyError, 0); + +const secret4 = dh4.computeSecret(key2, 'hex', 'base64'); + +assert.strictEqual(secret1, secret4); + + +if (false) { + let wrongBlockLength; + if (common.hasOpenSSL3) { + wrongBlockLength = { + message: 'error:1C80006B:Provider routines::wrong final block length', + code: 'ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH', + library: 'Provider routines', + reason: 'wrong final block length' + }; + } else { + wrongBlockLength = { + message: 'error:0606506D:digital envelope' + + ' routines:EVP_DecryptFinal_ex:wrong final block length', + code: 'ERR_OSSL_EVP_WRONG_FINAL_BLOCK_LENGTH', + library: 'digital envelope routines', + reason: 'wrong final block length' + }; + } + + // Run this one twice to make sure that the dh3 clears its error properly + { + const c = crypto.createDecipheriv('aes-128-ecb', crypto.randomBytes(16), ''); + assert.throws(() => { + c.final('utf8'); + }, wrongBlockLength); + } + + { + const c = crypto.createDecipheriv('aes-128-ecb', crypto.randomBytes(16), ''); + assert.throws(() => { + c.final('utf8'); + }, wrongBlockLength); + } + + assert.throws(() => { + dh3.computeSecret(''); + }, { message: common.hasOpenSSL3 ? + 'error:02800080:Diffie-Hellman routines::invalid secret' : + 'Supplied key is too small' }); + + // Invalid test: curve argument is undefined + assert.throws( + () => crypto.createECDH(), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "curve" argument must be of type string. ' + + 'Received undefined' + }); + + assert.throws( + function() { + crypto.getDiffieHellman('unknown-group'); + }, + { + name: 'Error', + code: 'ERR_CRYPTO_UNKNOWN_DH_GROUP', + message: 'Unknown DH group' + }, + 'crypto.getDiffieHellman(\'unknown-group\') ' + + 'failed to throw the expected error.' + ); +} + +assert.throws( + () => crypto.createDiffieHellman('', true), + { + code: 'ERR_INVALID_ARG_TYPE' + } +); +[true, Symbol(), {}, () => {}, []].forEach((generator) => assert.throws( + () => crypto.createDiffieHellman('', 'base64', generator), + { code: 'ERR_INVALID_ARG_TYPE' } +)); diff --git a/ext/node/lib.rs b/ext/node/lib.rs index e01954109a..aed325c93d 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -170,6 +170,8 @@ deno_core::extension!(deno_node, ops::crypto::op_node_dh_generate_group, ops::crypto::op_node_dh_generate_group_async, ops::crypto::op_node_dh_generate, + ops::crypto::op_node_dh_generate2, + ops::crypto::op_node_dh_compute_secret, ops::crypto::op_node_dh_generate_async, ops::crypto::op_node_verify, ops::crypto::op_node_random_int, diff --git a/ext/node/ops/crypto/dh.rs b/ext/node/ops/crypto/dh.rs index 4da9a01bf8..8b756d9a2d 100644 --- a/ext/node/ops/crypto/dh.rs +++ b/ext/node/ops/crypto/dh.rs @@ -63,7 +63,7 @@ impl DiffieHellman { } pub fn new(prime: Prime, generator: usize) -> Self { - let private_key = PrivateKey::new(32); + let private_key = PrivateKey::new(prime.bits()); let generator = BigUint::from_usize(generator).unwrap(); let public_key = private_key.compute_public_key(&generator, &prime); diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index 0f8feb2a92..05f2d34f7e 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -11,6 +11,7 @@ use deno_core::StringOrBuffer; use deno_core::ZeroCopyBuf; use hkdf::Hkdf; use num_bigint::BigInt; +use num_bigint_dig::BigUint; use num_traits::FromPrimitive; use rand::distributions::Distribution; use rand::distributions::Uniform; @@ -788,6 +789,30 @@ pub fn op_node_dh_generate( dh_generate(prime, prime_len, generator) } +// TODO(lev): This duplication should be avoided. +#[op] +pub fn op_node_dh_generate2( + prime: ZeroCopyBuf, + prime_len: usize, + generator: usize, +) -> Result<(ZeroCopyBuf, ZeroCopyBuf), AnyError> { + dh_generate(Some(prime).as_deref(), prime_len, generator) +} + +#[op] +pub fn op_node_dh_compute_secret( + prime: ZeroCopyBuf, + private_key: ZeroCopyBuf, + their_public_key: ZeroCopyBuf, +) -> Result { + let pubkey: BigUint = BigUint::from_bytes_be(their_public_key.as_ref()); + let privkey: BigUint = BigUint::from_bytes_be(private_key.as_ref()); + let primei: BigUint = BigUint::from_bytes_be(prime.as_ref()); + let shared_secret: BigUint = pubkey.modpow(&privkey, &primei); + + Ok(shared_secret.to_bytes_be().into()) +} + #[op] pub async fn op_node_dh_generate_async( prime: Option, diff --git a/ext/node/polyfills/internal/crypto/diffiehellman.ts b/ext/node/polyfills/internal/crypto/diffiehellman.ts index 62a802126f..2531c07c7a 100644 --- a/ext/node/polyfills/internal/crypto/diffiehellman.ts +++ b/ext/node/polyfills/internal/crypto/diffiehellman.ts @@ -6,7 +6,10 @@ import { isAnyArrayBuffer, isArrayBufferView, } from "ext:deno_node/internal/util/types.ts"; -import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; +import { + ERR_INVALID_ARG_TYPE, + NodeError, +} from "ext:deno_node/internal/errors.ts"; import { validateInt32, validateString, @@ -32,9 +35,14 @@ const DH_GENERATOR = 2; export class DiffieHellman { verifyError!: number; + #prime: Buffer; + #primeLength: number; + #generator: Buffer; + #privateKey: Buffer; + #publicKey: Buffer; constructor( - sizeOrKey: unknown, + sizeOrKey: number | string | ArrayBufferView, keyEncoding?: unknown, generator?: unknown, genEncoding?: unknown, @@ -71,24 +79,68 @@ export class DiffieHellman { genEncoding = genEncoding || encoding; if (typeof sizeOrKey !== "number") { - sizeOrKey = toBuf(sizeOrKey as string, keyEncoding as string); + this.#prime = toBuf(sizeOrKey as string, keyEncoding as string); + } else { + // The supplied parameter is our primeLength, generate a suitable prime. + this.#primeLength = sizeOrKey as number; + if (this.#primeLength < 2) { + throw new NodeError("ERR_OSSL_BN_BITS_TOO_SMALL", "bits too small"); + } + + this.#prime = Buffer.from( + ops.op_node_gen_prime(this.#primeLength).buffer, + ); } if (!generator) { - generator = DH_GENERATOR; + // While the commonly used cyclic group generators for DH are 2 and 5, we + // need this a buffer, because, well.. Node. + this.#generator = Buffer.alloc(4); + this.#generator.writeUint32BE(DH_GENERATOR); } else if (typeof generator === "number") { validateInt32(generator, "generator"); + this.#generator = Buffer.alloc(4); + if (generator <= 0 || generator >= 0x7fffffff) { + throw new NodeError("ERR_OSSL_DH_BAD_GENERATOR", "bad generator"); + } + this.#generator.writeUint32BE(generator); } else if (typeof generator === "string") { generator = toBuf(generator, genEncoding as string); + this.#generator = generator; } else if (!isArrayBufferView(generator) && !isAnyArrayBuffer(generator)) { throw new ERR_INVALID_ARG_TYPE( "generator", ["number", "string", "ArrayBuffer", "Buffer", "TypedArray", "DataView"], generator, ); + } else { + this.#generator = Buffer.from(generator); } - notImplemented("crypto.DiffieHellman"); + this.#checkGenerator(); + + // TODO(lev): actually implement this value + this.verifyError = 0; + } + + #checkGenerator(): number { + let generator: number; + + if (this.#generator.length == 0) { + throw new NodeError("ERR_OSSL_DH_BAD_GENERATOR", "bad generator"); + } else if (this.#generator.length == 1) { + generator = this.#generator.readUint8(); + } else if (this.#generator.length == 2) { + generator = this.#generator.readUint16BE(); + } else { + generator = this.#generator.readUint32BE(); + } + + if (generator != 2 && generator != 5) { + throw new NodeError("ERR_OSSL_DH_BAD_GENERATOR", "bad generator"); + } + + return generator; } computeSecret(otherPublicKey: ArrayBufferView): Buffer; @@ -106,59 +158,110 @@ export class DiffieHellman { outputEncoding: BinaryToTextEncoding, ): string; computeSecret( - _otherPublicKey: ArrayBufferView | string, - _inputEncoding?: BinaryToTextEncoding, - _outputEncoding?: BinaryToTextEncoding, + otherPublicKey: ArrayBufferView | string, + inputEncoding?: BinaryToTextEncoding, + outputEncoding?: BinaryToTextEncoding, ): Buffer | string { - notImplemented("crypto.DiffieHellman.prototype.computeSecret"); + let buf; + if (inputEncoding != undefined && inputEncoding != "buffer") { + buf = Buffer.from(otherPublicKey.buffer, inputEncoding); + } else { + buf = Buffer.from(otherPublicKey.buffer); + } + + const sharedSecret = ops.op_node_dh_compute_secret( + this.#prime, + this.#privateKey, + buf, + ); + + if (outputEncoding == undefined || outputEncoding == "buffer") { + return Buffer.from(sharedSecret.buffer); + } + + return Buffer.from(sharedSecret.buffer).toString(outputEncoding); } generateKeys(): Buffer; generateKeys(encoding: BinaryToTextEncoding): string; generateKeys(_encoding?: BinaryToTextEncoding): Buffer | string { - notImplemented("crypto.DiffieHellman.prototype.generateKeys"); + const generator = this.#checkGenerator(); + const [privateKey, publicKey] = ops.op_node_dh_generate2( + this.#prime, + this.#primeLength, + generator, + ); + + this.#privateKey = Buffer.from(privateKey.buffer); + this.#publicKey = Buffer.from(publicKey.buffer); + + return this.#publicKey; } getGenerator(): Buffer; getGenerator(encoding: BinaryToTextEncoding): string; - getGenerator(_encoding?: BinaryToTextEncoding): Buffer | string { - notImplemented("crypto.DiffieHellman.prototype.getGenerator"); + getGenerator(encoding?: BinaryToTextEncoding): Buffer | string { + if (encoding !== undefined && encoding != "buffer") { + return this.#generator.toString(encoding); + } + + return this.#generator; } getPrime(): Buffer; getPrime(encoding: BinaryToTextEncoding): string; - getPrime(_encoding?: BinaryToTextEncoding): Buffer | string { - notImplemented("crypto.DiffieHellman.prototype.getPrime"); + getPrime(encoding?: BinaryToTextEncoding): Buffer | string { + if (encoding !== undefined && encoding != "buffer") { + return this.#prime.toString(encoding); + } + + return this.#prime; } getPrivateKey(): Buffer; getPrivateKey(encoding: BinaryToTextEncoding): string; - getPrivateKey(_encoding?: BinaryToTextEncoding): Buffer | string { - notImplemented("crypto.DiffieHellman.prototype.getPrivateKey"); + getPrivateKey(encoding?: BinaryToTextEncoding): Buffer | string { + if (encoding !== undefined && encoding != "buffer") { + return this.#privateKey.toString(encoding); + } + + return this.#privateKey; } getPublicKey(): Buffer; getPublicKey(encoding: BinaryToTextEncoding): string; - getPublicKey(_encoding?: BinaryToTextEncoding): Buffer | string { - notImplemented("crypto.DiffieHellman.prototype.getPublicKey"); + getPublicKey(encoding?: BinaryToTextEncoding): Buffer | string { + if (encoding !== undefined && encoding != "buffer") { + return this.#publicKey.toString(encoding); + } + + return this.#publicKey; } setPrivateKey(privateKey: ArrayBufferView): void; setPrivateKey(privateKey: string, encoding: BufferEncoding): void; setPrivateKey( - _privateKey: ArrayBufferView | string, - _encoding?: BufferEncoding, + privateKey: ArrayBufferView | string, + encoding?: BufferEncoding, ) { - notImplemented("crypto.DiffieHellman.prototype.setPrivateKey"); + if (encoding == undefined || encoding == "buffer") { + this.#privateKey = Buffer.from(privateKey); + } else { + this.#privateKey = Buffer.from(privateKey, encoding); + } } setPublicKey(publicKey: ArrayBufferView): void; setPublicKey(publicKey: string, encoding: BufferEncoding): void; setPublicKey( - _publicKey: ArrayBufferView | string, - _encoding?: BufferEncoding, + publicKey: ArrayBufferView | string, + encoding?: BufferEncoding, ) { - notImplemented("crypto.DiffieHellman.prototype.setPublicKey"); + if (encoding == undefined || encoding == "buffer") { + this.#publicKey = Buffer.from(publicKey); + } else { + this.#publicKey = Buffer.from(publicKey, encoding); + } } } diff --git a/tools/node_compat/TODO.md b/tools/node_compat/TODO.md index 2ea6983f91..a94dbc090a 100644 --- a/tools/node_compat/TODO.md +++ b/tools/node_compat/TODO.md @@ -3,7 +3,7 @@ NOTE: This file should not be manually edited. Please edit 'cli/tests/node_compat/config.json' and run 'tools/node_compat/setup.ts' instead. -Total: 2935 +Total: 2934 - [abort/test-abort-backtrace.js](https://github.com/nodejs/node/tree/v18.12.1/test/abort/test-abort-backtrace.js) - [abort/test-abort-fatal-error.js](https://github.com/nodejs/node/tree/v18.12.1/test/abort/test-abort-fatal-error.js) @@ -476,7 +476,6 @@ Total: 2935 - [parallel/test-crypto-dh-padding.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-dh-padding.js) - [parallel/test-crypto-dh-shared.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-dh-shared.js) - [parallel/test-crypto-dh-stateless.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-dh-stateless.js) -- [parallel/test-crypto-dh.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-dh.js) - [parallel/test-crypto-domain.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-domain.js) - [parallel/test-crypto-domains.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-domains.js) - [parallel/test-crypto-ecb.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-ecb.js)