Skip to content

Commit

Permalink
crypto: add KeyObject.prototype.toCryptoKey
Browse files Browse the repository at this point in the history
PR-URL: nodejs#55262
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Yagiz Nizipli <[email protected]>
Reviewed-By: Tobias Nießen <[email protected]>
  • Loading branch information
panva authored and tpoisseau committed Nov 21, 2024
1 parent 2d437c7 commit 1866692
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 59 deletions.
19 changes: 19 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,24 @@ added: v11.6.0
For secret keys, this property represents the size of the key in bytes. This
property is `undefined` for asymmetric keys.

### `keyObject.toCryptoKey(algorithm, extractable, keyUsages)`

<!-- YAML
added: REPLACEME
-->

<!--lint disable maximum-line-length remark-lint-->

* `algorithm`: {AlgorithmIdentifier|RsaHashedImportParams|EcKeyImportParams|HmacImportParams}

<!--lint enable maximum-line-length remark-lint-->

* `extractable`: {boolean}
* `keyUsages`: {string\[]} See [Key usages][].
* Returns: {CryptoKey}

Converts a `KeyObject` instance to a `CryptoKey`.

### `keyObject.type`

<!-- YAML
Expand Down Expand Up @@ -6087,6 +6105,7 @@ See the [list of SSL OP Flags][] for details.
[FIPS provider from OpenSSL 3]: https://www.openssl.org/docs/man3.0/man7/crypto.html#FIPS-provider
[HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed
[JWK]: https://tools.ietf.org/html/rfc7517
[Key usages]: webcrypto.md#cryptokeyusages
[NIST SP 800-131A]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar2.pdf
[NIST SP 800-132]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
Expand Down
7 changes: 6 additions & 1 deletion lib/internal/crypto/aes.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ async function aesGenerateKey(algorithm, extractable, keyUsages) {
extractable);
}

async function aesImportKey(
function aesImportKey(
algorithm,
format,
keyData,
Expand All @@ -266,6 +266,11 @@ async function aesImportKey(
let keyObject;
let length;
switch (format) {
case 'KeyObject': {
validateKeyLength(keyData.symmetricKeySize * 8);
keyObject = keyData;
break;
}
case 'raw': {
validateKeyLength(keyData.byteLength * 8);
keyObject = createSecretKey(keyData);
Expand Down
7 changes: 6 additions & 1 deletion lib/internal/crypto/cfrg.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ function cfrgExportKey(key, format) {
key[kKeyObject][kHandle]));
}

async function cfrgImportKey(
function cfrgImportKey(
format,
keyData,
algorithm,
Expand All @@ -208,6 +208,11 @@ async function cfrgImportKey(
let keyObject;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableCfrgKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
break;
}
case 'spki': {
verifyAcceptableCfrgKeyUse(name, true, usagesSet);
try {
Expand Down
7 changes: 6 additions & 1 deletion lib/internal/crypto/ec.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ function ecExportKey(key, format) {
key[kKeyObject][kHandle]));
}

async function ecImportKey(
function ecImportKey(
format,
keyData,
algorithm,
Expand All @@ -167,6 +167,11 @@ async function ecImportKey(
let keyObject;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableEcKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
break;
}
case 'spki': {
verifyAcceptableEcKeyUse(name, true, usagesSet);
try {
Expand Down
159 changes: 159 additions & 0 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
ObjectDefineProperties,
ObjectDefineProperty,
ObjectSetPrototypeOf,
SafeSet,
Symbol,
SymbolToStringTag,
Uint8Array,
Expand Down Expand Up @@ -49,6 +50,8 @@ const {
kKeyObject,
getArrayBufferOrView,
bigIntArrayToUnsignedBigInt,
normalizeAlgorithm,
hasAnyNotIn,
} = require('internal/crypto/util');

const {
Expand All @@ -65,6 +68,7 @@ const {
const {
customInspectSymbol: kInspect,
kEnumerableProperty,
lazyDOMException,
} = require('internal/util');

const { inspect } = require('internal/util/inspect');
Expand Down Expand Up @@ -148,6 +152,8 @@ const {
},
});

let webidl;

class SecretKeyObject extends KeyObject {
constructor(handle) {
super('secret', handle);
Expand All @@ -168,6 +174,51 @@ const {
}
return this[kHandle].export();
}

toCryptoKey(algorithm, extractable, keyUsages) {
webidl ??= require('internal/crypto/webidl');
algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey');
extractable = webidl.converters.boolean(extractable);
keyUsages = webidl.converters['sequence<KeyUsage>'](keyUsages);

let result;
switch (algorithm.name) {
case 'HMAC':
result = require('internal/crypto/mac')
.hmacImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
case 'AES-CTR':
// Fall through
case 'AES-CBC':
// Fall through
case 'AES-GCM':
// Fall through
case 'AES-KW':
result = require('internal/crypto/aes')
.aesImportKey(algorithm, 'KeyObject', this, extractable, keyUsages);
break;
case 'HKDF':
// Fall through
case 'PBKDF2':
result = importGenericSecretKey(
algorithm,
'KeyObject',
this,
extractable,
keyUsages);
break;
default:
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}

if (result.usages.length === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
'SyntaxError');
}

return result;
}
}

const kAsymmetricKeyType = Symbol('kAsymmetricKeyType');
Expand Down Expand Up @@ -209,6 +260,51 @@ const {
return {};
}
}

toCryptoKey(algorithm, extractable, keyUsages) {
webidl ??= require('internal/crypto/webidl');
algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey');
extractable = webidl.converters.boolean(extractable);
keyUsages = webidl.converters['sequence<KeyUsage>'](keyUsages);

let result;
switch (algorithm.name) {
case 'RSASSA-PKCS1-v1_5':
// Fall through
case 'RSA-PSS':
// Fall through
case 'RSA-OAEP':
result = require('internal/crypto/rsa')
.rsaImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
case 'ECDSA':
// Fall through
case 'ECDH':
result = require('internal/crypto/ec')
.ecImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
case 'Ed25519':
// Fall through
case 'Ed448':
// Fall through
case 'X25519':
// Fall through
case 'X448':
result = require('internal/crypto/cfrg')
.cfrgImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
default:
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}

if (result.type === 'private' && result.usages.length === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
'SyntaxError');
}

return result;
}
}

class PublicKeyObject extends AsymmetricKeyObject {
Expand Down Expand Up @@ -801,6 +897,68 @@ function isCryptoKey(obj) {
return obj != null && obj[kKeyObject] !== undefined;
}

function importGenericSecretKey(
{ name, length },
format,
keyData,
extractable,
keyUsages) {
const usagesSet = new SafeSet(keyUsages);
if (extractable)
throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');

if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}

switch (format) {
case 'KeyObject': {
if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}

const checkLength = keyData.symmetricKeySize * 8;

// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (length !== undefined && length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
return new InternalCryptoKey(keyData, { name }, keyUsages, false);
}
case 'raw': {
if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}

const checkLength = keyData.byteLength * 8;

// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (length !== undefined && length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}

const keyObject = createSecretKey(keyData);
return new InternalCryptoKey(keyObject, { name }, keyUsages, false);
}
}

throw lazyDOMException(
`Unable to import ${name} key with format ${format}`,
'NotSupportedError');
}

module.exports = {
// Public API.
createSecretKey,
Expand All @@ -822,4 +980,5 @@ module.exports = {
PrivateKeyObject,
isKeyObject,
isCryptoKey,
importGenericSecretKey,
};
20 changes: 19 additions & 1 deletion lib/internal/crypto/mac.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function getAlgorithmName(hash) {
}
}

async function hmacImportKey(
function hmacImportKey(
format,
keyData,
algorithm,
Expand All @@ -96,6 +96,24 @@ async function hmacImportKey(
}
let keyObject;
switch (format) {
case 'KeyObject': {
const checkLength = keyData.symmetricKeySize * 8;

if (checkLength === 0 || algorithm.length === 0)
throw lazyDOMException('Zero-length key is not supported', 'DataError');

// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (algorithm.length !== undefined &&
algorithm.length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}

keyObject = keyData;
break;
}
case 'raw': {
const checkLength = keyData.byteLength * 8;

Expand Down
7 changes: 6 additions & 1 deletion lib/internal/crypto/rsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ function rsaExportKey(key, format) {
kRsaVariants[key.algorithm.name]));
}

async function rsaImportKey(
function rsaImportKey(
format,
keyData,
algorithm,
Expand All @@ -209,6 +209,11 @@ async function rsaImportKey(
const usagesSet = new SafeSet(keyUsages);
let keyObject;
switch (format) {
case 'KeyObject': {
verifyAcceptableRsaKeyUse(algorithm.name, keyData.type === 'public', usagesSet);
keyObject = keyData;
break;
}
case 'spki': {
verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet);
try {
Expand Down
Loading

0 comments on commit 1866692

Please sign in to comment.