1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

fix(ext/node): implement hkdf-expand (#18612)

Towards https://github.com/denoland/deno/issues/18455
This commit is contained in:
Divy Srivastava 2023-04-06 22:26:56 +05:30 committed by GitHub
parent 2d0a9ffbcc
commit df72420d72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 287 additions and 8 deletions

1
Cargo.lock generated
View file

@ -1108,6 +1108,7 @@ dependencies = [
"digest 0.10.6",
"ecb",
"hex",
"hkdf",
"idna 0.3.0",
"indexmap",
"libz-sys",

View file

@ -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"

View file

@ -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",

View 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);
}));
}
});

View file

@ -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"] }

View file

@ -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<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?
}

View file

@ -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,

View file

@ -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 { core } = globalThis.__bootstrap;
const { ops } = core;
const validateParameters = hideStackFrames((hash, key, salt, info, length) => {
key = prepareKey(key);
salt = toBuf(salt);
info = toBuf(info);
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 {