mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -05:00
fix(ext/node): implement hkdf-expand (#18612)
Towards https://github.com/denoland/deno/issues/18455
This commit is contained in:
parent
2d0a9ffbcc
commit
df72420d72
8 changed files with 287 additions and 8 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1108,6 +1108,7 @@ dependencies = [
|
||||||
"digest 0.10.6",
|
"digest 0.10.6",
|
||||||
"ecb",
|
"ecb",
|
||||||
"hex",
|
"hex",
|
||||||
|
"hkdf",
|
||||||
"idna 0.3.0",
|
"idna 0.3.0",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"libz-sys",
|
"libz-sys",
|
||||||
|
|
|
@ -138,6 +138,7 @@ zstd = "=0.11.2"
|
||||||
|
|
||||||
# crypto
|
# crypto
|
||||||
rsa = { version = "0.7.0", default-features = false, features = ["std", "pem", "hazmat"] } # hazmat needed for PrehashSigner in ext/node
|
rsa = { version = "0.7.0", default-features = false, features = ["std", "pem", "hazmat"] } # hazmat needed for PrehashSigner in ext/node
|
||||||
|
hkdf = "0.12.3"
|
||||||
|
|
||||||
# macros
|
# macros
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
|
|
|
@ -228,6 +228,7 @@
|
||||||
"test-console-sync-write-error.js",
|
"test-console-sync-write-error.js",
|
||||||
"test-console-table.js",
|
"test-console-table.js",
|
||||||
"test-console-tty-colors.js",
|
"test-console-tty-colors.js",
|
||||||
|
"test-crypto-hkdf.js",
|
||||||
"test-crypto-hmac.js",
|
"test-crypto-hmac.js",
|
||||||
"test-crypto-prime.js",
|
"test-crypto-prime.js",
|
||||||
"test-crypto-secret-keygen.js",
|
"test-crypto-secret-keygen.js",
|
||||||
|
|
203
cli/tests/node_compat/test/parallel/test-crypto-hkdf.js
Normal file
203
cli/tests/node_compat/test/parallel/test-crypto-hkdf.js
Normal file
|
@ -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);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
|
@ -20,6 +20,7 @@ deno_core.workspace = true
|
||||||
digest = { version = "0.10.5", features = ["core-api", "std"] }
|
digest = { version = "0.10.5", features = ["core-api", "std"] }
|
||||||
ecb.workspace = true
|
ecb.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
|
hkdf.workspace = true
|
||||||
idna = "0.3.0"
|
idna = "0.3.0"
|
||||||
indexmap.workspace = true
|
indexmap.workspace = true
|
||||||
libz-sys = { version = "1.1.8", features = ["static"] }
|
libz-sys = { version = "1.1.8", features = ["static"] }
|
||||||
|
|
|
@ -7,6 +7,7 @@ use deno_core::OpState;
|
||||||
use deno_core::ResourceId;
|
use deno_core::ResourceId;
|
||||||
use deno_core::StringOrBuffer;
|
use deno_core::StringOrBuffer;
|
||||||
use deno_core::ZeroCopyBuf;
|
use deno_core::ZeroCopyBuf;
|
||||||
|
use hkdf::Hkdf;
|
||||||
use num_bigint::BigInt;
|
use num_bigint::BigInt;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
@ -419,3 +420,60 @@ pub async fn op_node_generate_secret_async(len: i32) -> ZeroCopyBuf {
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.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<ZeroCopyBuf, AnyError> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let mut okm = vec![0u8; okm_len];
|
||||||
|
hkdf_sync(&hash, &ikm, &salt, &info, &mut okm)?;
|
||||||
|
Ok(okm.into())
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
|
@ -189,6 +189,8 @@ deno_core::extension!(deno_node,
|
||||||
crypto::op_node_check_prime_bytes_async,
|
crypto::op_node_check_prime_bytes_async,
|
||||||
crypto::op_node_pbkdf2,
|
crypto::op_node_pbkdf2,
|
||||||
crypto::op_node_pbkdf2_async,
|
crypto::op_node_pbkdf2_async,
|
||||||
|
crypto::op_node_hkdf,
|
||||||
|
crypto::op_node_hkdf_async,
|
||||||
crypto::op_node_generate_secret,
|
crypto::op_node_generate_secret,
|
||||||
crypto::op_node_generate_secret_async,
|
crypto::op_node_generate_secret_async,
|
||||||
crypto::op_node_sign,
|
crypto::op_node_sign,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
validateString,
|
validateString,
|
||||||
} from "ext:deno_node/internal/validators.mjs";
|
} from "ext:deno_node/internal/validators.mjs";
|
||||||
import {
|
import {
|
||||||
|
ERR_CRYPTO_INVALID_DIGEST,
|
||||||
ERR_INVALID_ARG_TYPE,
|
ERR_INVALID_ARG_TYPE,
|
||||||
ERR_OUT_OF_RANGE,
|
ERR_OUT_OF_RANGE,
|
||||||
hideStackFrames,
|
hideStackFrames,
|
||||||
|
@ -26,17 +27,19 @@ import {
|
||||||
isAnyArrayBuffer,
|
isAnyArrayBuffer,
|
||||||
isArrayBufferView,
|
isArrayBufferView,
|
||||||
} from "ext:deno_node/internal/util/types.ts";
|
} from "ext:deno_node/internal/util/types.ts";
|
||||||
import { notImplemented } from "ext:deno_node/_utils.ts";
|
|
||||||
|
const { core } = globalThis.__bootstrap;
|
||||||
|
const { ops } = core;
|
||||||
|
|
||||||
const validateParameters = hideStackFrames((hash, key, salt, info, length) => {
|
const validateParameters = hideStackFrames((hash, key, salt, info, length) => {
|
||||||
key = prepareKey(key);
|
|
||||||
salt = toBuf(salt);
|
|
||||||
info = toBuf(info);
|
|
||||||
|
|
||||||
validateString(hash, "digest");
|
validateString(hash, "digest");
|
||||||
|
key = new Uint8Array(prepareKey(key));
|
||||||
validateByteSource(salt, "salt");
|
validateByteSource(salt, "salt");
|
||||||
validateByteSource(info, "info");
|
validateByteSource(info, "info");
|
||||||
|
|
||||||
|
salt = new Uint8Array(toBuf(salt));
|
||||||
|
info = new Uint8Array(toBuf(info));
|
||||||
|
|
||||||
validateInteger(length, "length", 0, kMaxLength);
|
validateInteger(length, "length", 0, kMaxLength);
|
||||||
|
|
||||||
if (info.byteLength > 1024) {
|
if (info.byteLength > 1024) {
|
||||||
|
@ -91,7 +94,7 @@ export function hkdf(
|
||||||
salt: BinaryLike,
|
salt: BinaryLike,
|
||||||
info: BinaryLike,
|
info: BinaryLike,
|
||||||
length: number,
|
length: number,
|
||||||
callback: (err: Error | null, derivedKey: ArrayBuffer) => void,
|
callback: (err: Error | null, derivedKey: ArrayBuffer | undefined) => void,
|
||||||
) {
|
) {
|
||||||
({ hash, key, salt, info, length } = validateParameters(
|
({ hash, key, salt, info, length } = validateParameters(
|
||||||
hash,
|
hash,
|
||||||
|
@ -103,7 +106,9 @@ export function hkdf(
|
||||||
|
|
||||||
validateFunction(callback, "callback");
|
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(
|
export function hkdfSync(
|
||||||
|
@ -121,7 +126,14 @@ export function hkdfSync(
|
||||||
length,
|
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 {
|
export default {
|
||||||
|
|
Loading…
Add table
Reference in a new issue