Skip to content

Commit

Permalink
Implement generateKey()/generateKeySync() for aes/hmac keys
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed Feb 8, 2025
1 parent f685d0c commit b231446
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 13 deletions.
90 changes: 77 additions & 13 deletions src/node/internal/crypto_keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ import {
} from 'node-internal:internal_errors';

import {
validateInteger,
validateObject,
validateOneOf,
validateString,
} from 'node-internal:validators';

import { inspect } from 'node-internal:internal_inspect';
import { randomBytes } from 'node-internal:crypto_random';
const kInspect = inspect.custom;

// Key input contexts.
Expand Down Expand Up @@ -515,7 +517,7 @@ export function createPublicKey(

export type PublicKeyResult = KeyExportResult | PublicKeyObject;
export type PrivateKeyResult = KeyExportResult | PrivateKeyObject;
export type GenerateKeyCallback = (err?: any, key?: KeyObject) => void;
export type GenerateKeyCallback = (err?: any, key?: SecretKeyObject) => void;
export type GenerateKeyPairCallback = (
err?: any,
publicKey?: PublicKeyResult,
Expand All @@ -532,25 +534,87 @@ export function generateKey(
_options: GenerateKeyOptions,
callback: GenerateKeyCallback
) {
// This API is not implemented yet.
callback(new ERR_METHOD_NOT_IMPLEMENTED('crypto.generateKeySync'));
}

export function generateKeySync(
_type: SecretKeyType,
_options: GenerateKeyOptions
) {
// This API is not implemented yet.
throw new ERR_METHOD_NOT_IMPLEMENTED('crypto.generateKeySync');
try {
// Unlike Node.js, which implements async crypto functions using the
// libuv thread pool, we don't actually perform async crypto operations.
// Here we just defer to the sync version of the function and then "fake"
// async by using queueMicrotask to call the callback.
const result = generateKeySync(_type, _options);
queueMicrotask(() => {
try {
callback(null, result);
} catch (err) {
reportError(err);
}
});
} catch (err) {
queueMicrotask(() => {
try {
callback(err);
} catch (otherErr) {
reportError(otherErr);
}
});
}
}

export function generateKeyPair(
_type: AsymmetricKeyType,
_options: GenerateKeyPairOptions,
callback: GenerateKeyPairCallback
) {
// This API is not implemented yet.
callback(new ERR_METHOD_NOT_IMPLEMENTED('crypto.generateKeyPair'));
try {
// Unlike Node.js, which implements async crypto functions using the
// libuv thread pool, we don't actually perform async crypto operations.
// Here we just defer to the sync version of the function and then "fake"
// async by using queueMicrotask to call the callback.
const { publicKey, privateKey } = generateKeyPairSync(_type, _options);
queueMicrotask(() => {
try {
callback(null, publicKey, privateKey);
} catch (err) {
reportError(err);
}
});
} catch (err) {
queueMicrotask(() => {
try {
callback(err);
} catch (otherErr) {
reportError(otherErr);
}
});
}
}

export function generateKeySync(
type: SecretKeyType,
options: GenerateKeyOptions
): SecretKeyObject {
validateOneOf(type, 'type', ['hmac', 'aes']);
validateObject(options, 'options');
const { length } = options;

switch (type) {
case 'hmac': {
// The minimum is 8, and the maximum length is 65,536. If the length is
// not a multiple of 8, the generated key will be truncated to
// Math.floor(length / 8).
// Note that the upper bound of 65536 is intentionally more limited than
// what Node.js allows. This puts the maximum size limit on generated
// secret keys to 8192 bytes. We can adjust this up if necessary but
// it's a good starting point.
validateInteger(length, 'options.length', 8, 65536);
const buf = randomBytes(Math.floor(length / 8));
return createSecretKey(buf);
}
case 'aes': {
// The length must be one of 128, 192, or 256.
validateOneOf(length, 'options.length', [128, 192, 256]);
const buf = randomBytes(length / 8);
return createSecretKey(buf);
}
}
}

export function generateKeyPairSync(
Expand Down
63 changes: 63 additions & 0 deletions src/workerd/api/node/tests/crypto_keys-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
createPrivateKey,
createPublicKey,
createHmac,
generateKey,
generateKeySync,
} from 'node:crypto';
import { Buffer } from 'node:buffer';

Expand Down Expand Up @@ -1567,3 +1569,64 @@ export const ed_public_key_jwk_import = {
);
},
};

export const generate_hmac_secret_key = {
async test() {
// Length is intentionally not a multiple of 8
const key = generateKeySync('hmac', { length: 33 });

Check failure

Code scanning / CodeQL

Use of a weak cryptographic key High test

Creation of an symmetric key uses 33 bits, which is below 128 and considered breakable.
strictEqual(key.type, 'secret');
strictEqual(key.symmetricKeySize, 4);

const { promise, resolve, reject } = Promise.withResolvers();
generateKey('hmac', { length: 33 }, (err, key) => {
if (err) {
reject(err);
return;
}
strictEqual(key.type, 'secret');
strictEqual(key.symmetricKeySize, 4);
resolve();
});

Check failure

Code scanning / CodeQL

Use of a weak cryptographic key High test

Creation of an symmetric key uses 33 bits, which is below 128 and considered breakable.
await promise;

throws(() => generateKeySync('hmac', { length: 0 }), {

Check failure

Code scanning / CodeQL

Use of a weak cryptographic key High test

Creation of an symmetric key uses 0 bits, which is below 128 and considered breakable.
message:
'The value of "options.length" is out of range. It must ' +
'be >= 8 && <= 65536. Received 0',
});
throws(() => generateKeySync('hmac', { length: 65537 }), {
message:
'The value of "options.length" is out of range. It must ' +
'be >= 8 && <= 65536. Received 65537',
});

const h = createHmac('sha256', key);
ok(h.update('test').digest());
},
};

export const generate_aes_secret_key = {
async test() {
const key = generateKeySync('aes', { length: 128 });
strictEqual(key.type, 'secret');
strictEqual(key.symmetricKeySize, 16);

const { promise, resolve, reject } = Promise.withResolvers();
generateKey('aes', { length: 128 }, (err, key) => {
if (err) {
reject(err);
return;
}
strictEqual(key.type, 'secret');
strictEqual(key.symmetricKeySize, 16);
resolve();
});
await promise;

throws(() => generateKeySync('aes', { length: 0 }), {

Check failure

Code scanning / CodeQL

Use of a weak cryptographic key High test

Creation of an symmetric AES key uses 0 bits, which is below 128 and considered breakable.
message:
"The property 'options.length' must be one of: 128, 192, " +
'256. Received 0',
});
},
};

0 comments on commit b231446

Please sign in to comment.