Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 205 additions & 57 deletions doc/api/crypto.md

Large diffs are not rendered by default.

34 changes: 29 additions & 5 deletions lib/internal/crypto/cfrg.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@

const {
SafeSet,
TypedArrayPrototypeGetBuffer,
} = primordials;

const { Buffer } = require('buffer');

const {
ECKeyExportJob,
KeyObjectHandle,
SignJob,
kCryptoJobAsync,
kKeyFormatDER,
kKeyTypePrivate,
kKeyTypePublic,
kSignJobModeSign,
kSignJobModeVerify,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatSPKI,
} = internalBinding('crypto');

const {
Expand Down Expand Up @@ -195,10 +199,30 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) {
}

function cfrgExportKey(key, format) {
return jobPromise(() => new ECKeyExportJob(
kCryptoJobAsync,
format,
key[kKeyObject][kHandle]));
try {
switch (format) {
case kWebCryptoKeyFormatRaw: {
if (key[kKeyType] === 'private') {
return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawPrivateKey());
}
return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawPublicKey());
}
case kWebCryptoKeyFormatSPKI: {
return TypedArrayPrototypeGetBuffer(
key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
}
case kWebCryptoKeyFormatPKCS8: {
return TypedArrayPrototypeGetBuffer(
key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
}
default:
return undefined;
}
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
}
}

function cfrgImportKey(
Expand Down
52 changes: 47 additions & 5 deletions lib/internal/crypto/ec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,30 @@

const {
SafeSet,
TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeGetByteLength,
} = primordials;

const {
ECKeyExportJob,
KeyObjectHandle,
SignJob,
kCryptoJobAsync,
kKeyFormatDER,
kKeyTypePrivate,
kSignJobModeSign,
kSignJobModeVerify,
kSigEncP1363,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatSPKI,
} = internalBinding('crypto');

const {
crypto: {
POINT_CONVERSION_UNCOMPRESSED,
},
} = internalBinding('constants');

const {
getUsagesUnion,
hasAnyNotIn,
Expand All @@ -41,6 +52,7 @@ const {
PublicKeyObject,
createPrivateKey,
createPublicKey,
kAlgorithm,
kKeyType,
} = require('internal/crypto/keys');

Expand Down Expand Up @@ -139,10 +151,40 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) {
}

function ecExportKey(key, format) {
return jobPromise(() => new ECKeyExportJob(
kCryptoJobAsync,
format,
key[kKeyObject][kHandle]));
try {
const handle = key[kKeyObject][kHandle];
switch (format) {
case kWebCryptoKeyFormatRaw: {
return TypedArrayPrototypeGetBuffer(
handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED));
}
case kWebCryptoKeyFormatSPKI: {
let spki = handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI);
// WebCrypto requires uncompressed point format for SPKI exports.
// This is a very rare edge case dependent on the imported key
// using compressed point format.
if (TypedArrayPrototypeGetByteLength(spki) !== {
'__proto__': null, 'P-256': 91, 'P-384': 120, 'P-521': 158,
}[key[kAlgorithm].namedCurve]) {
const raw = handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED);
const tmp = new KeyObjectHandle();
tmp.initECRaw(kNamedCurveAliases[key[kAlgorithm].namedCurve], raw);
spki = tmp.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI);
}
return TypedArrayPrototypeGetBuffer(spki);
}
case kWebCryptoKeyFormatPKCS8: {
return TypedArrayPrototypeGetBuffer(
handle.export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
}
default:
return undefined;
}
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
}
}

function ecImportKey(
Expand Down
163 changes: 135 additions & 28 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ const {
kKeyEncodingSEC1,
} = internalBinding('crypto');

const {
crypto: {
POINT_CONVERSION_COMPRESSED,
POINT_CONVERSION_UNCOMPRESSED,
},
} = internalBinding('constants');

const {
validateObject,
validateOneOf,
Expand Down Expand Up @@ -82,6 +89,7 @@ const kKeyUsages = Symbol('kKeyUsages');
const kCachedAlgorithm = Symbol('kCachedAlgorithm');
const kCachedKeyUsages = Symbol('kCachedKeyUsages');


// Key input contexts.
const kConsumePublic = 0;
const kConsumePrivate = 1;
Expand Down Expand Up @@ -340,14 +348,27 @@ const {
}

export(options) {
if (options && options.format === 'jwk') {
return this[kHandle].exportJwk({}, false);
switch (options?.format) {
case 'jwk':
return this[kHandle].exportJwk({}, false);
case 'raw-public': {
if (this.asymmetricKeyType === 'ec') {
const { type = 'compressed' } = options;
validateOneOf(type, 'options.type', ['compressed', 'uncompressed']);
const form = type === 'compressed' ?
POINT_CONVERSION_COMPRESSED : POINT_CONVERSION_UNCOMPRESSED;
return this[kHandle].exportECPublicRaw(form);
}
return this[kHandle].rawPublicKey();
}
default: {
const {
format,
type,
} = parsePublicKeyEncoding(options, this.asymmetricKeyType);
return this[kHandle].export(format, type);
}
}
const {
format,
type,
} = parsePublicKeyEncoding(options, this.asymmetricKeyType);
return this[kHandle].export(format, type);
}
}

Expand All @@ -357,20 +378,32 @@ const {
}

export(options) {
if (options && options.format === 'jwk') {
if (options.passphrase !== undefined) {
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
'jwk', 'does not support encryption');
if (options?.passphrase !== undefined &&
options.format !== 'pem' && options.format !== 'der') {
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
options.format, 'does not support encryption');
}
switch (options?.format) {
case 'jwk':
return this[kHandle].exportJwk({}, false);
case 'raw-private': {
if (this.asymmetricKeyType === 'ec') {
return this[kHandle].exportECPrivateRaw();
}
return this[kHandle].rawPrivateKey();
}
case 'raw-seed':
return this[kHandle].rawSeed();
default: {
const {
format,
type,
cipher,
passphrase,
} = parsePrivateKeyEncoding(options, this.asymmetricKeyType);
return this[kHandle].export(format, type, cipher, passphrase);
}
return this[kHandle].exportJwk({}, false);
}
const {
format,
type,
cipher,
passphrase,
} = parsePrivateKeyEncoding(options, this.asymmetricKeyType);
return this[kHandle].export(format, type, cipher, passphrase);
}
}

Expand Down Expand Up @@ -549,13 +582,8 @@ function mlDsaPubLen(alg) {

function getKeyObjectHandleFromJwk(key, ctx) {
validateObject(key, 'key');
if (KeyObjectHandle.prototype.initPqcRaw) {
validateOneOf(
key.kty, 'key.kty', ['RSA', 'EC', 'OKP', 'AKP']);
} else {
validateOneOf(
key.kty, 'key.kty', ['RSA', 'EC', 'OKP']);
}
validateOneOf(
key.kty, 'key.kty', ['RSA', 'EC', 'OKP', 'AKP']);
const isPublic = ctx === kConsumePublic || ctx === kCreatePublic;

if (key.kty === 'AKP') {
Expand Down Expand Up @@ -691,6 +719,79 @@ function getKeyObjectHandleFromJwk(key, ctx) {
return handle;
}


function getKeyObjectHandleFromRaw(options, data, format) {
if (!isStringOrBuffer(data)) {
throw new ERR_INVALID_ARG_TYPE(
'key.key',
['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'],
data);
}

const keyData = getArrayBufferOrView(data, 'key.key');

validateString(options.asymmetricKeyType, 'key.asymmetricKeyType');
const asymmetricKeyType = options.asymmetricKeyType;

const handle = new KeyObjectHandle();

switch (asymmetricKeyType) {
case 'ec': {
validateString(options.namedCurve, 'key.namedCurve');
if (format === 'raw-public') {
if (!handle.initECRaw(options.namedCurve, keyData)) {
throw new ERR_INVALID_ARG_VALUE('key.key', keyData);
}
} else if (!handle.initECPrivateRaw(options.namedCurve, keyData)) {
throw new ERR_INVALID_ARG_VALUE('key.key', keyData);
}
return handle;
}
case 'ed25519':
case 'ed448':
case 'x25519':
case 'x448': {
const keyType = format === 'raw-public' ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initEDRaw(asymmetricKeyType, keyData, keyType)) {
throw new ERR_INVALID_ARG_VALUE('key.key', keyData);
}
return handle;
}
case 'rsa':
case 'rsa-pss':
case 'dsa':
case 'dh':
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
format, `is not supported for ${asymmetricKeyType} keys`);
case 'ml-dsa-44':
case 'ml-dsa-65':
case 'ml-dsa-87':
case 'ml-kem-512':
case 'ml-kem-768':
case 'ml-kem-1024':
case 'slh-dsa-sha2-128f':
case 'slh-dsa-sha2-128s':
case 'slh-dsa-sha2-192f':
case 'slh-dsa-sha2-192s':
case 'slh-dsa-sha2-256f':
case 'slh-dsa-sha2-256s':
case 'slh-dsa-shake-128f':
case 'slh-dsa-shake-128s':
case 'slh-dsa-shake-192f':
case 'slh-dsa-shake-192s':
case 'slh-dsa-shake-256f':
case 'slh-dsa-shake-256s': {
const keyType = format === 'raw-public' ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initPqcRaw(asymmetricKeyType, keyData, keyType)) {
throw new ERR_INVALID_ARG_VALUE('key.key', keyData);
}
return handle;
}
default:
throw new ERR_INVALID_ARG_VALUE('asymmetricKeyType', asymmetricKeyType);
}
}

function prepareAsymmetricKey(key, ctx) {
if (isKeyObject(key)) {
// Best case: A key object, as simple as that.
Expand All @@ -712,6 +813,12 @@ function prepareAsymmetricKey(key, ctx) {
else if (format === 'jwk') {
validateObject(data, 'key.key');
return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' };
} else if (format === 'raw-public' || format === 'raw-private' ||
format === 'raw-seed') {
return {
data: getKeyObjectHandleFromRaw(key, data, format),
format,
};
}

// Either PEM or DER using PKCS#1 or SPKI.
Expand Down Expand Up @@ -777,7 +884,7 @@ function createPublicKey(key) {
const { format, type, data, passphrase } =
prepareAsymmetricKey(key, kCreatePublic);
let handle;
if (format === 'jwk') {
if (format === 'jwk' || format === 'raw-public') {
handle = data;
} else {
handle = new KeyObjectHandle();
Expand All @@ -790,7 +897,7 @@ function createPrivateKey(key) {
const { format, type, data, passphrase } =
prepareAsymmetricKey(key, kCreatePrivate);
let handle;
if (format === 'jwk') {
if (format === 'jwk' || format === 'raw-private' || format === 'raw-seed') {
handle = data;
} else {
handle = new KeyObjectHandle();
Expand Down
Loading
Loading