diff --git a/Cargo.lock b/Cargo.lock index 2a21a541aed5b9..bd938b1a75803d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1108,6 +1108,7 @@ dependencies = [ "digest 0.10.6", "ecb", "hex", + "hkdf", "idna 0.3.0", "indexmap", "libz-sys", diff --git a/Cargo.toml b/Cargo.toml index c12b14af1b1afa..2cdf4b375e5319 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ zstd = "=0.11.2" # crypto rsa = { version = "0.7.0", default-features = false, features = ["std", "pem", "hazmat"] } # hazmat needed for PrehashSigner in ext/node +hkdf = "0.12.3" # macros proc-macro2 = "1" diff --git a/cli/tests/node_compat/config.json b/cli/tests/node_compat/config.json index e314c19581fd65..fc9a8d1318a8e3 100644 --- a/cli/tests/node_compat/config.json +++ b/cli/tests/node_compat/config.json @@ -228,6 +228,7 @@ "test-console-sync-write-error.js", "test-console-table.js", "test-console-tty-colors.js", + "test-crypto-hkdf.js", "test-crypto-hmac.js", "test-crypto-prime.js", "test-crypto-secret-keygen.js", diff --git a/cli/tests/node_compat/test/parallel/test-crypto-hkdf.js b/cli/tests/node_compat/test/parallel/test-crypto-hkdf.js new file mode 100644 index 00000000000000..b5b35e3ce52d5d --- /dev/null +++ b/cli/tests/node_compat/test/parallel/test-crypto-hkdf.js @@ -0,0 +1,203 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { kMaxLength } = require('buffer'); +const assert = require('assert'); +const { + createSecretKey, + hkdf, + hkdfSync +} = require('crypto'); + +{ + assert.throws(() => hkdf(), { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "digest" argument must be of type string/ + }); + + [1, {}, [], false, Infinity].forEach((i) => { + assert.throws(() => hkdf(i, 'a'), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "digest" argument must be of type string/ + }); + assert.throws(() => hkdfSync(i, 'a'), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "digest" argument must be of type string/ + }); + }); + + [1, {}, [], false, Infinity].forEach((i) => { + assert.throws(() => hkdf('sha256', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "ikm" argument must be / + }); + assert.throws(() => hkdfSync('sha256', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "ikm" argument must be / + }); + }); + + [1, {}, [], false, Infinity].forEach((i) => { + assert.throws(() => hkdf('sha256', 'secret', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "salt" argument must be / + }); + assert.throws(() => hkdfSync('sha256', 'secret', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "salt" argument must be / + }); + }); + + [1, {}, [], false, Infinity].forEach((i) => { + assert.throws(() => hkdf('sha256', 'secret', 'salt', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "info" argument must be / + }); + assert.throws(() => hkdfSync('sha256', 'secret', 'salt', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "info" argument must be / + }); + }); + + ['test', {}, [], false].forEach((i) => { + assert.throws(() => hkdf('sha256', 'secret', 'salt', 'info', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "length" argument must be of type number/ + }); + assert.throws(() => hkdfSync('sha256', 'secret', 'salt', 'info', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "length" argument must be of type number/ + }); + }); + + assert.throws(() => hkdf('sha256', 'secret', 'salt', 'info', -1), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws(() => hkdfSync('sha256', 'secret', 'salt', 'info', -1), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws(() => hkdf('sha256', 'secret', 'salt', 'info', + kMaxLength + 1), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws(() => hkdfSync('sha256', 'secret', 'salt', 'info', + kMaxLength + 1), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws(() => hkdfSync('unknown', 'a', '', '', 10), { + code: 'ERR_CRYPTO_INVALID_DIGEST' + }); + + assert.throws(() => hkdf('unknown', 'a', '', Buffer.alloc(1025), 10, + common.mustNotCall()), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws(() => hkdfSync('unknown', 'a', '', Buffer.alloc(1025), 10), { + code: 'ERR_OUT_OF_RANGE' + }); +} + +const algorithms = [ + ['sha256', 'secret', 'salt', 'info', 10], + ['sha256', '', '', '', 10], + ['sha256', '', 'salt', '', 10], + ['sha512', 'secret', 'salt', '', 15], +]; + +algorithms.forEach(([ hash, secret, salt, info, length ]) => { + { + const syncResult = hkdfSync(hash, secret, salt, info, length); + assert(syncResult instanceof ArrayBuffer); + let is_async = false; + hkdf(hash, secret, salt, info, length, + common.mustSucceed((asyncResult) => { + assert(is_async); + assert(asyncResult instanceof ArrayBuffer); + assert.deepStrictEqual(syncResult, asyncResult); + })); + // Keep this after the hkdf call above. This verifies + // that the callback is invoked asynchronously. + is_async = true; + } + + { + const buf_secret = Buffer.from(secret); + const buf_salt = Buffer.from(salt); + const buf_info = Buffer.from(info); + + const syncResult = hkdfSync(hash, buf_secret, buf_salt, buf_info, length); + hkdf(hash, buf_secret, buf_salt, buf_info, length, + common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(syncResult, asyncResult); + })); + } + + { + const key_secret = createSecretKey(Buffer.from(secret)); + const buf_salt = Buffer.from(salt); + const buf_info = Buffer.from(info); + + const syncResult = hkdfSync(hash, key_secret, buf_salt, buf_info, length); + hkdf(hash, key_secret, buf_salt, buf_info, length, + common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(syncResult, asyncResult); + })); + } + + { + const ta_secret = new Uint8Array(Buffer.from(secret)); + const ta_salt = new Uint16Array(Buffer.from(salt)); + const ta_info = new Uint32Array(Buffer.from(info)); + + const syncResult = hkdfSync(hash, ta_secret, ta_salt, ta_info, length); + hkdf(hash, ta_secret, ta_salt, ta_info, length, + common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(syncResult, asyncResult); + })); + } + + { + const ta_secret = new Uint8Array(Buffer.from(secret)); + const ta_salt = new Uint16Array(Buffer.from(salt)); + const ta_info = new Uint32Array(Buffer.from(info)); + + const syncResult = hkdfSync( + hash, + ta_secret.buffer, + ta_salt.buffer, + ta_info.buffer, + length); + hkdf(hash, ta_secret, ta_salt, ta_info, length, + common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(syncResult, asyncResult); + })); + } + + { + const ta_secret = new Uint8Array(Buffer.from(secret)); + const sa_salt = new ArrayBuffer(0); + const sa_info = new ArrayBuffer(1); + + const syncResult = hkdfSync( + hash, + ta_secret.buffer, + sa_salt, + sa_info, + length); + hkdf(hash, ta_secret, sa_salt, sa_info, length, + common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(syncResult, asyncResult); + })); + } +}); diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 4dbc79b9e19b99..e74cf380535352 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -20,6 +20,7 @@ deno_core.workspace = true digest = { version = "0.10.5", features = ["core-api", "std"] } ecb.workspace = true hex.workspace = true +hkdf.workspace = true idna = "0.3.0" indexmap.workspace = true libz-sys = { version = "1.1.8", features = ["static"] } diff --git a/ext/node/crypto/mod.rs b/ext/node/crypto/mod.rs index 499e99fea8eec4..adacdf6d68e976 100644 --- a/ext/node/crypto/mod.rs +++ b/ext/node/crypto/mod.rs @@ -7,6 +7,7 @@ use deno_core::OpState; use deno_core::ResourceId; use deno_core::StringOrBuffer; use deno_core::ZeroCopyBuf; +use hkdf::Hkdf; use num_bigint::BigInt; use rand::Rng; use std::future::Future; @@ -419,3 +420,60 @@ pub async fn op_node_generate_secret_async(len: i32) -> ZeroCopyBuf { .await .unwrap() } + +fn hkdf_sync( + hash: &str, + ikm: &[u8], + salt: &[u8], + info: &[u8], + okm: &mut [u8], +) -> Result<(), AnyError> { + macro_rules! hkdf { + ($hash:ty) => {{ + let hk = Hkdf::<$hash>::new(Some(salt), ikm); + hk.expand(info, okm) + .map_err(|_| type_error("HKDF-Expand failed"))?; + }}; + } + + match hash { + "md4" => hkdf!(md4::Md4), + "md5" => hkdf!(md5::Md5), + "ripemd160" => hkdf!(ripemd::Ripemd160), + "sha1" => hkdf!(sha1::Sha1), + "sha224" => hkdf!(sha2::Sha224), + "sha256" => hkdf!(sha2::Sha256), + "sha384" => hkdf!(sha2::Sha384), + "sha512" => hkdf!(sha2::Sha512), + _ => return Err(type_error("Unknown digest")), + } + + Ok(()) +} + +#[op] +pub fn op_node_hkdf( + hash: &str, + ikm: &[u8], + salt: &[u8], + info: &[u8], + okm: &mut [u8], +) -> Result<(), AnyError> { + hkdf_sync(hash, ikm, salt, info, okm) +} + +#[op] +pub async fn op_node_hkdf_async( + hash: String, + ikm: ZeroCopyBuf, + salt: ZeroCopyBuf, + info: ZeroCopyBuf, + okm_len: usize, +) -> Result { + tokio::task::spawn_blocking(move || { + let mut okm = vec![0u8; okm_len]; + hkdf_sync(&hash, &ikm, &salt, &info, &mut okm)?; + Ok(okm.into()) + }) + .await? +} diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 478efaf27898ce..bf947f5e8dcf30 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -189,6 +189,8 @@ deno_core::extension!(deno_node, crypto::op_node_check_prime_bytes_async, crypto::op_node_pbkdf2, crypto::op_node_pbkdf2_async, + crypto::op_node_hkdf, + crypto::op_node_hkdf_async, crypto::op_node_generate_secret, crypto::op_node_generate_secret_async, crypto::op_node_sign, diff --git a/ext/node/polyfills/internal/crypto/hkdf.ts b/ext/node/polyfills/internal/crypto/hkdf.ts index deeba102f5f48f..fb26053df7ac22 100644 --- a/ext/node/polyfills/internal/crypto/hkdf.ts +++ b/ext/node/polyfills/internal/crypto/hkdf.ts @@ -7,6 +7,7 @@ import { validateString, } from "ext:deno_node/internal/validators.mjs"; import { + ERR_CRYPTO_INVALID_DIGEST, ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE, hideStackFrames, @@ -26,17 +27,19 @@ import { isAnyArrayBuffer, isArrayBufferView, } from "ext:deno_node/internal/util/types.ts"; -import { notImplemented } from "ext:deno_node/_utils.ts"; -const validateParameters = hideStackFrames((hash, key, salt, info, length) => { - key = prepareKey(key); - salt = toBuf(salt); - info = toBuf(info); +const { core } = globalThis.__bootstrap; +const { ops } = core; +const validateParameters = hideStackFrames((hash, key, salt, info, length) => { validateString(hash, "digest"); + key = new Uint8Array(prepareKey(key)); validateByteSource(salt, "salt"); validateByteSource(info, "info"); + salt = new Uint8Array(toBuf(salt)); + info = new Uint8Array(toBuf(info)); + validateInteger(length, "length", 0, kMaxLength); if (info.byteLength > 1024) { @@ -91,7 +94,7 @@ export function hkdf( salt: BinaryLike, info: BinaryLike, length: number, - callback: (err: Error | null, derivedKey: ArrayBuffer) => void, + callback: (err: Error | null, derivedKey: ArrayBuffer | undefined) => void, ) { ({ hash, key, salt, info, length } = validateParameters( hash, @@ -103,7 +106,9 @@ export function hkdf( validateFunction(callback, "callback"); - notImplemented("crypto.hkdf"); + core.opAsync("op_node_hkdf_async", hash, key, salt, info, length) + .then((okm) => callback(null, okm.buffer)) + .catch((err) => callback(new ERR_CRYPTO_INVALID_DIGEST(err), undefined)); } export function hkdfSync( @@ -121,7 +126,14 @@ export function hkdfSync( length, )); - notImplemented("crypto.hkdfSync"); + const okm = new Uint8Array(length); + try { + ops.op_node_hkdf(hash, key, salt, info, okm); + } catch (e) { + throw new ERR_CRYPTO_INVALID_DIGEST(e); + } + + return okm.buffer; } export default {