Skip to content

Commit

Permalink
fix(ext/node): add symmetric keygen (#18609)
Browse files Browse the repository at this point in the history
Towards #18455
  • Loading branch information
littledivy authored Apr 6, 2023
1 parent 339165b commit 3b62a58
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 16 deletions.
1 change: 1 addition & 0 deletions cli/tests/node_compat/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"test-console-tty-colors.js",
"test-crypto-hmac.js",
"test-crypto-prime.js",
"test-crypto-secret-keygen.js",
"test-dgram-close-during-bind.js",
"test-dgram-close-signal.js",
"test-diagnostics-channel-has-subscribers.js",
Expand Down
137 changes: 137 additions & 0 deletions cli/tests/node_compat/test/parallel/test-crypto-secret-keygen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');

const {
generateKey,
generateKeySync
} = require('crypto');

[1, true, [], {}, Infinity, null, undefined].forEach((i) => {
assert.throws(() => generateKey(i, 1, common.mustNotCall()), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "type" argument must be /
});
assert.throws(() => generateKeySync(i, 1), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "type" argument must be /
});
});

['', true, [], null, undefined].forEach((i) => {
assert.throws(() => generateKey('aes', i, common.mustNotCall()), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options" argument must be /
});
assert.throws(() => generateKeySync('aes', i), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options" argument must be /
});
});

['', true, {}, [], null, undefined].forEach((length) => {
assert.throws(() => generateKey('hmac', { length }, common.mustNotCall()), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.length" property must be /
});
assert.throws(() => generateKeySync('hmac', { length }), {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.length" property must be /
});
});

assert.throws(() => generateKey('aes', { length: 256 }), {
code: 'ERR_INVALID_ARG_TYPE'
});

assert.throws(() => generateKey('hmac', { length: -1 }, common.mustNotCall()), {
code: 'ERR_OUT_OF_RANGE'
});

assert.throws(() => generateKey('hmac', { length: 4 }, common.mustNotCall()), {
code: 'ERR_OUT_OF_RANGE'
});

assert.throws(() => generateKey('hmac', { length: 7 }, common.mustNotCall()), {
code: 'ERR_OUT_OF_RANGE'
});

assert.throws(
() => generateKey('hmac', { length: 2 ** 31 }, common.mustNotCall()), {
code: 'ERR_OUT_OF_RANGE'
});

assert.throws(() => generateKeySync('hmac', { length: -1 }), {
code: 'ERR_OUT_OF_RANGE'
});

assert.throws(() => generateKeySync('hmac', { length: 4 }), {
code: 'ERR_OUT_OF_RANGE'
});

assert.throws(() => generateKeySync('hmac', { length: 7 }), {
code: 'ERR_OUT_OF_RANGE'
});

assert.throws(
() => generateKeySync('hmac', { length: 2 ** 31 }), {
code: 'ERR_OUT_OF_RANGE'
});

assert.throws(() => generateKeySync('aes', { length: 123 }), {
code: 'ERR_INVALID_ARG_VALUE',
message: /The property 'options\.length' must be one of: 128, 192, 256/
});

{
const key = generateKeySync('aes', { length: 128 });
assert(key);
const keybuf = key.export();
assert.strictEqual(keybuf.byteLength, 128 / 8);

generateKey('aes', { length: 128 }, common.mustSucceed((key) => {
assert(key);
const keybuf = key.export();
assert.strictEqual(keybuf.byteLength, 128 / 8);
}));
}

{
const key = generateKeySync('aes', { length: 256 });
assert(key);
const keybuf = key.export();
assert.strictEqual(keybuf.byteLength, 256 / 8);

generateKey('aes', { length: 256 }, common.mustSucceed((key) => {
assert(key);
const keybuf = key.export();
assert.strictEqual(keybuf.byteLength, 256 / 8);
}));
}

{
const key = generateKeySync('hmac', { length: 123 });
assert(key);
const keybuf = key.export();
assert.strictEqual(keybuf.byteLength, Math.floor(123 / 8));

generateKey('hmac', { length: 123 }, common.mustSucceed((key) => {
assert(key);
const keybuf = key.export();
assert.strictEqual(keybuf.byteLength, Math.floor(123 / 8));
}));
}

assert.throws(
() => generateKey('unknown', { length: 123 }, common.mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
message: /The argument 'type' must be a supported key type/
});

assert.throws(() => generateKeySync('unknown', { length: 123 }), {
code: 'ERR_INVALID_ARG_VALUE',
message: /The argument 'type' must be a supported key type/
});
17 changes: 17 additions & 0 deletions ext/node/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use deno_core::ResourceId;
use deno_core::StringOrBuffer;
use deno_core::ZeroCopyBuf;
use num_bigint::BigInt;
use rand::Rng;
use std::future::Future;
use std::rc::Rc;

Expand Down Expand Up @@ -402,3 +403,19 @@ pub async fn op_node_pbkdf2_async(
})
.await?
}

#[op]
pub fn op_node_generate_secret(buf: &mut [u8]) {
rand::thread_rng().fill(buf);
}

#[op]
pub async fn op_node_generate_secret_async(len: i32) -> ZeroCopyBuf {
tokio::task::spawn_blocking(move || {
let mut buf = vec![0u8; len as usize];
rand::thread_rng().fill(&mut buf[..]);
buf.into()
})
.await
.unwrap()
}
2 changes: 2 additions & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,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_generate_secret,
crypto::op_node_generate_secret_async,
crypto::op_node_sign,
winerror::op_node_sys_to_uv_error,
v8::op_v8_cached_data_version_tag,
Expand Down
79 changes: 66 additions & 13 deletions ext/node/polyfills/internal/crypto/keygen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,80 @@
// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license.

import { KeyObject } from "ext:deno_node/internal/crypto/keys.ts";
import { kAesKeyLengths } from "ext:deno_node/internal/crypto/util.ts";
import {
SecretKeyObject,
setOwnedKey,
} from "ext:deno_node/internal/crypto/keys.ts";
import { notImplemented } from "ext:deno_node/_utils.ts";
import { ERR_INVALID_ARG_VALUE } from "ext:deno_node/internal/errors.ts";
import {
validateFunction,
validateInteger,
validateObject,
validateOneOf,
validateString,
} from "ext:deno_node/internal/validators.mjs";
import { Buffer } from "ext:deno_node/buffer.ts";
import { KeyFormat, KeyType } from "ext:deno_node/internal/crypto/types.ts";

const { core } = globalThis.__bootstrap;
const { ops } = core;

function validateGenerateKey(
type: "hmac" | "aes",
options: { length: number },
) {
validateString(type, "type");
validateObject(options, "options");
const { length } = options;
switch (type) {
case "hmac":
validateInteger(length, "options.length", 8, 2 ** 31 - 1);
break;
case "aes":
validateOneOf(length, "options.length", kAesKeyLengths);
break;
default:
throw new ERR_INVALID_ARG_VALUE(
"type",
type,
"must be a supported key type",
);
}
}

export function generateKeySync(
type: "hmac" | "aes",
options: {
length: number;
},
): KeyObject {
validateGenerateKey(type, options);
const { length } = options;

const key = new Uint8Array(Math.floor(length / 8));
ops.op_node_generate_secret(key);

return new SecretKeyObject(setOwnedKey(key));
}

export function generateKey(
_type: "hmac" | "aes",
_options: {
type: "hmac" | "aes",
options: {
length: number;
},
_callback: (err: Error | null, key: KeyObject) => void,
callback: (err: Error | null, key: KeyObject) => void,
) {
notImplemented("crypto.generateKey");
validateGenerateKey(type, options);
validateFunction(callback, "callback");
const { length } = options;

core.opAsync("op_node_generate_secret_async", Math.floor(length / 8)).then(
(key) => {
callback(null, new SecretKeyObject(setOwnedKey(key)));
},
);
}

export interface BasePrivateKeyEncodingOptions<T extends KeyFormat> {
Expand Down Expand Up @@ -662,15 +724,6 @@ export function generateKeyPairSync(
notImplemented("crypto.generateKeyPairSync");
}

export function generateKeySync(
_type: "hmac" | "aes",
_options: {
length: number;
},
): KeyObject {
notImplemented("crypto.generateKeySync");
}

export default {
generateKey,
generateKeySync,
Expand Down
6 changes: 4 additions & 2 deletions ext/node/polyfills/internal/crypto/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export function prepareSecretKey(
return getArrayBufferOrView(key, "key", encoding);
}

class SecretKeyObject extends KeyObject {
export class SecretKeyObject extends KeyObject {
constructor(handle: unknown) {
super("secret", handle);
}
Expand Down Expand Up @@ -313,7 +313,7 @@ class SecretKeyObject extends KeyObject {
}
}

function setOwnedKey(key: Uint8Array): unknown {
export function setOwnedKey(key: Uint8Array): unknown {
const handle = {};
KEY_STORE.set(handle, key);
return handle;
Expand Down Expand Up @@ -345,4 +345,6 @@ export default {
isCryptoKey,
KeyObject,
prepareSecretKey,
setOwnedKey,
SecretKeyObject,
};
5 changes: 4 additions & 1 deletion ext/node/polyfills/internal/crypto/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ export function setEngine(_engine: string, _flags: typeof constants) {
notImplemented("crypto.setEngine");
}

export { kHandle, kKeyObject };
const kAesKeyLengths = [128, 192, 256];

export { kAesKeyLengths, kHandle, kKeyObject };

export default {
getDefaultEncoding,
Expand All @@ -147,4 +149,5 @@ export default {
toBuf,
kHandle,
kKeyObject,
kAesKeyLengths,
};

0 comments on commit 3b62a58

Please sign in to comment.