Skip to content

Commit f1adaf1

Browse files
committed
Implement generateKey()/generateKeySync() for aes/hmac keys
1 parent f685d0c commit f1adaf1

File tree

3 files changed

+143
-16
lines changed

3 files changed

+143
-16
lines changed

WORKSPACE

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ http_archive(
8282

8383
http_archive(
8484
name = "ncrypto",
85-
sha256 = "f0d4bdbe13990ec3343911312477c9b0cc30c5717024f2d81e82acd1df23ec58",
86-
strip_prefix = "ncrypto-next-round",
85+
sha256 = "c8284e4719130916cbce340989a59249e605dfbdecd496910d6ed2d1ecaf69ad",
86+
strip_prefix = "ncrypto-main",
8787
type = "tgz",
88-
url = "https://github.com/nodejs/ncrypto/archive/refs/heads/next-round.tar.gz",
88+
url = "https://github.com/nodejs/ncrypto/archive/refs/heads/main.tar.gz",
8989
)
9090

9191
http_archive(

src/node/internal/crypto_keys.ts

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,14 @@ import {
6767
} from 'node-internal:internal_errors';
6868

6969
import {
70+
validateInteger,
7071
validateObject,
7172
validateOneOf,
7273
validateString,
7374
} from 'node-internal:validators';
7475

7576
import { inspect } from 'node-internal:internal_inspect';
77+
import { randomBytes } from 'node-internal:crypto_random';
7678
const kInspect = inspect.custom;
7779

7880
// Key input contexts.
@@ -515,7 +517,7 @@ export function createPublicKey(
515517

516518
export type PublicKeyResult = KeyExportResult | PublicKeyObject;
517519
export type PrivateKeyResult = KeyExportResult | PrivateKeyObject;
518-
export type GenerateKeyCallback = (err?: any, key?: KeyObject) => void;
520+
export type GenerateKeyCallback = (err?: any, key?: SecretKeyObject) => void;
519521
export type GenerateKeyPairCallback = (
520522
err?: any,
521523
publicKey?: PublicKeyResult,
@@ -532,25 +534,87 @@ export function generateKey(
532534
_options: GenerateKeyOptions,
533535
callback: GenerateKeyCallback
534536
) {
535-
// This API is not implemented yet.
536-
callback(new ERR_METHOD_NOT_IMPLEMENTED('crypto.generateKeySync'));
537-
}
538-
539-
export function generateKeySync(
540-
_type: SecretKeyType,
541-
_options: GenerateKeyOptions
542-
) {
543-
// This API is not implemented yet.
544-
throw new ERR_METHOD_NOT_IMPLEMENTED('crypto.generateKeySync');
537+
try {
538+
// Unlike Node.js, which implements async crypto functions using the
539+
// libuv thread pool, we don't actually perform async crypto operations.
540+
// Here we just defer to the sync version of the function and then "fake"
541+
// async by using queueMicrotask to call the callback.
542+
const result = generateKeySync(_type, _options);
543+
queueMicrotask(() => {
544+
try {
545+
callback(null, result);
546+
} catch (err) {
547+
reportError(err);
548+
}
549+
});
550+
} catch (err) {
551+
queueMicrotask(() => {
552+
try {
553+
callback(err);
554+
} catch (otherErr) {
555+
reportError(otherErr);
556+
}
557+
});
558+
}
545559
}
546560

547561
export function generateKeyPair(
548562
_type: AsymmetricKeyType,
549563
_options: GenerateKeyPairOptions,
550564
callback: GenerateKeyPairCallback
551565
) {
552-
// This API is not implemented yet.
553-
callback(new ERR_METHOD_NOT_IMPLEMENTED('crypto.generateKeyPair'));
566+
try {
567+
// Unlike Node.js, which implements async crypto functions using the
568+
// libuv thread pool, we don't actually perform async crypto operations.
569+
// Here we just defer to the sync version of the function and then "fake"
570+
// async by using queueMicrotask to call the callback.
571+
const { publicKey, privateKey } = generateKeyPairSync(_type, _options);
572+
queueMicrotask(() => {
573+
try {
574+
callback(null, publicKey, privateKey);
575+
} catch (err) {
576+
reportError(err);
577+
}
578+
});
579+
} catch (err) {
580+
queueMicrotask(() => {
581+
try {
582+
callback(err);
583+
} catch (otherErr) {
584+
reportError(otherErr);
585+
}
586+
});
587+
}
588+
}
589+
590+
export function generateKeySync(
591+
type: SecretKeyType,
592+
options: GenerateKeyOptions
593+
): SecretKeyObject {
594+
validateOneOf(type, 'type', ['hmac', 'aes']);
595+
validateObject(options, 'options');
596+
const { length } = options;
597+
598+
switch (type) {
599+
case 'hmac': {
600+
// The minimum is 8, and the maximum length is 65,536. If the length is
601+
// not a multiple of 8, the generated key will be truncated to
602+
// Math.floor(length / 8).
603+
// Note that the upper bound of 65536 is intentionally more limited than
604+
// what Node.js allows. This puts the maximum size limit on generated
605+
// secret keys to 8192 bytes. We can adjust this up if necessary but
606+
// it's a good starting point.
607+
validateInteger(length, 'options.length', 8, 65536);
608+
const buf = randomBytes(Math.floor(length / 8));
609+
return createSecretKey(buf);
610+
}
611+
case 'aes': {
612+
// The length must be one of 128, 192, or 256.
613+
validateOneOf(length, 'options.length', [128, 192, 256]);
614+
const buf = randomBytes(length / 8);
615+
return createSecretKey(buf);
616+
}
617+
}
554618
}
555619

556620
export function generateKeyPairSync(

src/workerd/api/node/tests/crypto_keys-test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
createPrivateKey,
88
createPublicKey,
99
createHmac,
10+
generateKey,
11+
generateKeySync,
1012
} from 'node:crypto';
1113
import { Buffer } from 'node:buffer';
1214

@@ -1567,3 +1569,64 @@ export const ed_public_key_jwk_import = {
15671569
);
15681570
},
15691571
};
1572+
1573+
export const generate_hmac_secret_key = {
1574+
async test() {
1575+
// Length is intentionally not a multiple of 8
1576+
const key = generateKeySync('hmac', { length: 33 });
1577+
strictEqual(key.type, 'secret');
1578+
strictEqual(key.symmetricKeySize, 4);
1579+
1580+
const { promise, resolve, reject } = Promise.withResolvers();
1581+
generateKey('hmac', { length: 33 }, (err, key) => {
1582+
if (err) {
1583+
reject(err);
1584+
return;
1585+
}
1586+
strictEqual(key.type, 'secret');
1587+
strictEqual(key.symmetricKeySize, 4);
1588+
resolve();
1589+
});
1590+
await promise;
1591+
1592+
throws(() => generateKeySync('hmac', { length: 0 }), {
1593+
message:
1594+
'The value of "options.length" is out of range. It must ' +
1595+
'be >= 8 && <= 65536. Received 0',
1596+
});
1597+
throws(() => generateKeySync('hmac', { length: 65537 }), {
1598+
message:
1599+
'The value of "options.length" is out of range. It must ' +
1600+
'be >= 8 && <= 65536. Received 65537',
1601+
});
1602+
1603+
const h = createHmac('sha256', key);
1604+
ok(h.update('test').digest());
1605+
},
1606+
};
1607+
1608+
export const generate_aes_secret_key = {
1609+
async test() {
1610+
const key = generateKeySync('aes', { length: 128 });
1611+
strictEqual(key.type, 'secret');
1612+
strictEqual(key.symmetricKeySize, 16);
1613+
1614+
const { promise, resolve, reject } = Promise.withResolvers();
1615+
generateKey('aes', { length: 128 }, (err, key) => {
1616+
if (err) {
1617+
reject(err);
1618+
return;
1619+
}
1620+
strictEqual(key.type, 'secret');
1621+
strictEqual(key.symmetricKeySize, 16);
1622+
resolve();
1623+
});
1624+
await promise;
1625+
1626+
throws(() => generateKeySync('aes', { length: 0 }), {
1627+
message:
1628+
"The property 'options.length' must be one of: 128, 192, " +
1629+
'256. Received 0',
1630+
});
1631+
},
1632+
};

0 commit comments

Comments
 (0)