diff options
Diffstat (limited to 'lib/internal/crypto')
-rw-r--r-- | lib/internal/crypto/cipher.js | 33 | ||||
-rw-r--r-- | lib/internal/crypto/hash.js | 9 | ||||
-rw-r--r-- | lib/internal/crypto/keygen.js | 135 | ||||
-rw-r--r-- | lib/internal/crypto/keys.js | 337 | ||||
-rw-r--r-- | lib/internal/crypto/sig.js | 31 |
5 files changed, 426 insertions, 119 deletions
diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index 1e5dc91c8d..0e8e5c4cf8 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -13,6 +13,11 @@ const { const { validateString } = require('internal/validators'); const { + preparePrivateKey, + preparePublicOrPrivateKey, + prepareSecretKey +} = require('internal/crypto/keys'); +const { getDefaultEncoding, kHandle, legacyNativeHandle, @@ -37,19 +42,25 @@ const { deprecate, normalizeEncoding } = require('internal/util'); // Lazy loaded for startup performance. let StringDecoder; -function rsaFunctionFor(method, defaultPadding) { +function rsaFunctionFor(method, defaultPadding, keyType) { return (options, buffer) => { - const key = options.key || options; + const { format, type, data, passphrase } = + keyType === 'private' ? + preparePrivateKey(options) : + preparePublicOrPrivateKey(options); const padding = options.padding || defaultPadding; - const passphrase = options.passphrase || null; - return method(toBuf(key), buffer, padding, passphrase); + return method(data, format, type, passphrase, buffer, padding); }; } -const publicEncrypt = rsaFunctionFor(_publicEncrypt, RSA_PKCS1_OAEP_PADDING); -const publicDecrypt = rsaFunctionFor(_publicDecrypt, RSA_PKCS1_PADDING); -const privateEncrypt = rsaFunctionFor(_privateEncrypt, RSA_PKCS1_PADDING); -const privateDecrypt = rsaFunctionFor(_privateDecrypt, RSA_PKCS1_OAEP_PADDING); +const publicEncrypt = rsaFunctionFor(_publicEncrypt, RSA_PKCS1_OAEP_PADDING, + 'public'); +const publicDecrypt = rsaFunctionFor(_publicDecrypt, RSA_PKCS1_PADDING, + 'private'); +const privateEncrypt = rsaFunctionFor(_privateEncrypt, RSA_PKCS1_PADDING, + 'private'); +const privateDecrypt = rsaFunctionFor(_privateDecrypt, RSA_PKCS1_OAEP_PADDING, + 'public'); function getDecoder(decoder, encoding) { encoding = normalizeEncoding(encoding); @@ -104,11 +115,7 @@ function createCipher(cipher, password, options, decipher) { function createCipherWithIV(cipher, key, options, decipher, iv) { validateString(cipher, 'cipher'); - key = toBuf(key); - if (!isArrayBufferView(key)) { - throw invalidArrayBufferView('key', key); - } - + key = prepareSecretKey(key); iv = toBuf(iv); if (iv !== null && !isArrayBufferView(iv)) { throw invalidArrayBufferView('iv', iv); diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index f289d11cf8..713ded3d18 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -12,6 +12,10 @@ const { toBuf } = require('internal/crypto/util'); +const { + prepareSecretKey +} = require('internal/crypto/keys'); + const { Buffer } = require('buffer'); const { @@ -88,10 +92,7 @@ function Hmac(hmac, key, options) { if (!(this instanceof Hmac)) return new Hmac(hmac, key, options); validateString(hmac, 'hmac'); - if (typeof key !== 'string' && !isArrayBufferView(key)) { - throw new ERR_INVALID_ARG_TYPE('key', - ['string', 'TypedArray', 'DataView'], key); - } + key = prepareSecretKey(key); this[kHandle] = new _Hmac(); this[kHandle].init(hmac, toBuf(key)); this[kState] = { diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 7222d301f0..7c0c411043 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -6,24 +6,32 @@ const { generateKeyPairDSA, generateKeyPairEC, OPENSSL_EC_NAMED_CURVE, - OPENSSL_EC_EXPLICIT_CURVE, - PK_ENCODING_PKCS1, - PK_ENCODING_PKCS8, - PK_ENCODING_SPKI, - PK_ENCODING_SEC1, - PK_FORMAT_DER, - PK_FORMAT_PEM + OPENSSL_EC_EXPLICIT_CURVE } = internalBinding('crypto'); +const { + parsePublicKeyEncoding, + parsePrivateKeyEncoding, + + PublicKeyObject, + PrivateKeyObject +} = require('internal/crypto/keys'); const { customPromisifyArgs } = require('internal/util'); const { isUint32, validateString } = require('internal/validators'); const { - ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_CALLBACK, ERR_INVALID_OPT_VALUE } = require('internal/errors').codes; +const { isArrayBufferView } = require('internal/util/types'); + +function wrapKey(key, ctor) { + if (typeof key === 'string' || isArrayBufferView(key)) + return key; + return new ctor(key); +} + function generateKeyPair(type, options, callback) { if (typeof options === 'function') { callback = options; @@ -38,6 +46,9 @@ function generateKeyPair(type, options, callback) { const wrap = new AsyncWrap(Providers.KEYPAIRGENREQUEST); wrap.ondone = (ex, pubkey, privkey) => { if (ex) return callback.call(wrap, ex); + // If no encoding was chosen, return key objects instead. + pubkey = wrapKey(pubkey, PublicKeyObject); + privkey = wrapKey(privkey, PrivateKeyObject); callback.call(wrap, null, pubkey, privkey); }; @@ -69,86 +80,32 @@ function handleError(impl, wrap) { function parseKeyEncoding(keyType, options) { const { publicKeyEncoding, privateKeyEncoding } = options; - if (publicKeyEncoding == null || typeof publicKeyEncoding !== 'object') - throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding', publicKeyEncoding); - - const { format: strPublicFormat, type: strPublicType } = publicKeyEncoding; - - let publicType; - if (strPublicType === 'pkcs1') { - if (keyType !== 'rsa') { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - strPublicType, 'can only be used for RSA keys'); - } - publicType = PK_ENCODING_PKCS1; - } else if (strPublicType === 'spki') { - publicType = PK_ENCODING_SPKI; + let publicFormat, publicType; + if (publicKeyEncoding == null) { + publicFormat = publicType = undefined; + } else if (typeof publicKeyEncoding === 'object') { + ({ + format: publicFormat, + type: publicType + } = parsePublicKeyEncoding(publicKeyEncoding, keyType, + 'publicKeyEncoding')); } else { - throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.type', strPublicType); + throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding', publicKeyEncoding); } - let publicFormat; - if (strPublicFormat === 'der') { - publicFormat = PK_FORMAT_DER; - } else if (strPublicFormat === 'pem') { - publicFormat = PK_FORMAT_PEM; + let privateFormat, privateType, cipher, passphrase; + if (privateKeyEncoding == null) { + privateFormat = privateType = undefined; + } else if (typeof privateKeyEncoding === 'object') { + ({ + format: privateFormat, + type: privateType, + cipher, + passphrase + } = parsePrivateKeyEncoding(privateKeyEncoding, keyType, + 'privateKeyEncoding')); } else { - throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.format', - strPublicFormat); - } - - if (privateKeyEncoding == null || typeof privateKeyEncoding !== 'object') throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding', privateKeyEncoding); - - const { - cipher, - passphrase, - format: strPrivateFormat, - type: strPrivateType - } = privateKeyEncoding; - - let privateType; - if (strPrivateType === 'pkcs1') { - if (keyType !== 'rsa') { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - strPrivateType, 'can only be used for RSA keys'); - } - privateType = PK_ENCODING_PKCS1; - } else if (strPrivateType === 'pkcs8') { - privateType = PK_ENCODING_PKCS8; - } else if (strPrivateType === 'sec1') { - if (keyType !== 'ec') { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - strPrivateType, 'can only be used for EC keys'); - } - privateType = PK_ENCODING_SEC1; - } else { - throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.type', strPrivateType); - } - - let privateFormat; - if (strPrivateFormat === 'der') { - privateFormat = PK_FORMAT_DER; - } else if (strPrivateFormat === 'pem') { - privateFormat = PK_FORMAT_PEM; - } else { - throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.format', - strPrivateFormat); - } - - if (cipher != null) { - if (typeof cipher !== 'string') - throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.cipher', cipher); - if (privateFormat === PK_FORMAT_DER && - (privateType === PK_ENCODING_PKCS1 || - privateType === PK_ENCODING_SEC1)) { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - strPrivateType, 'does not support encryption'); - } - if (typeof passphrase !== 'string') { - throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.passphrase', - passphrase); - } } return { @@ -181,8 +138,8 @@ function check(type, options, callback) { } impl = (wrap) => generateKeyPairRSA(modulusLength, publicExponent, - publicType, publicFormat, - privateType, privateFormat, + publicFormat, publicType, + privateFormat, privateType, cipher, passphrase, wrap); } break; @@ -200,8 +157,8 @@ function check(type, options, callback) { } impl = (wrap) => generateKeyPairDSA(modulusLength, divisorLength, - publicType, publicFormat, - privateType, privateFormat, + publicFormat, publicType, + privateFormat, privateType, cipher, passphrase, wrap); } break; @@ -219,8 +176,8 @@ function check(type, options, callback) { throw new ERR_INVALID_OPT_VALUE('paramEncoding', paramEncoding); impl = (wrap) => generateKeyPairEC(namedCurve, paramEncoding, - publicType, publicFormat, - privateType, privateFormat, + publicFormat, publicType, + privateFormat, privateType, cipher, passphrase, wrap); } break; diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js new file mode 100644 index 0000000000..ad82835080 --- /dev/null +++ b/lib/internal/crypto/keys.js @@ -0,0 +1,337 @@ +'use strict'; + +const { + KeyObject: KeyObjectHandle, + kKeyTypeSecret, + kKeyTypePublic, + kKeyTypePrivate, + kKeyFormatPEM, + kKeyFormatDER, + kKeyEncodingPKCS1, + kKeyEncodingPKCS8, + kKeyEncodingSPKI, + kKeyEncodingSEC1 +} = internalBinding('crypto'); +const { + ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, + ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_OPT_VALUE, + ERR_OUT_OF_RANGE +} = require('internal/errors').codes; +const { kHandle } = require('internal/crypto/util'); + +const { isArrayBufferView } = require('internal/util/types'); + +const kKeyType = Symbol('kKeyType'); + +const encodingNames = []; +for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'], + [kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']]) + encodingNames[m[0]] = m[1]; + +class KeyObject { + constructor(type, handle) { + if (type !== 'secret' && type !== 'public' && type !== 'private') + throw new ERR_INVALID_ARG_VALUE('type', type); + if (typeof handle !== 'object') + throw new ERR_INVALID_ARG_TYPE('handle', 'string', handle); + + this[kKeyType] = type; + + Object.defineProperty(this, kHandle, { + value: handle, + enumerable: false, + configurable: false, + writable: false + }); + } + + get type() { + return this[kKeyType]; + } +} + +class SecretKeyObject extends KeyObject { + constructor(handle) { + super('secret', handle); + } + + get symmetricKeySize() { + return this[kHandle].getSymmetricKeySize(); + } + + export() { + return this[kHandle].export(); + } +} + +const kAsymmetricKeyType = Symbol('kAsymmetricKeyType'); + +class AsymmetricKeyObject extends KeyObject { + get asymmetricKeyType() { + return this[kAsymmetricKeyType] || + (this[kAsymmetricKeyType] = this[kHandle].getAsymmetricKeyType()); + } +} + +class PublicKeyObject extends AsymmetricKeyObject { + constructor(handle) { + super('public', handle); + } + + export(encoding) { + const { + format, + type + } = parsePublicKeyEncoding(encoding, this.asymmetricKeyType); + return this[kHandle].export(format, type); + } +} + +class PrivateKeyObject extends AsymmetricKeyObject { + constructor(handle) { + super('private', handle); + } + + export(encoding) { + const { + format, + type, + cipher, + passphrase + } = parsePrivateKeyEncoding(encoding, this.asymmetricKeyType); + return this[kHandle].export(format, type, cipher, passphrase); + } +} + +function parseKeyFormat(formatStr, defaultFormat, optionName) { + if (formatStr === undefined && defaultFormat !== undefined) + return defaultFormat; + else if (formatStr === 'pem') + return kKeyFormatPEM; + else if (formatStr === 'der') + return kKeyFormatDER; + throw new ERR_INVALID_OPT_VALUE(optionName, formatStr); +} + +function parseKeyType(typeStr, required, keyType, isPublic, optionName) { + if (typeStr === undefined && !required) { + return undefined; + } else if (typeStr === 'pkcs1') { + if (keyType !== undefined && keyType !== 'rsa') { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + typeStr, 'can only be used for RSA keys'); + } + return kKeyEncodingPKCS1; + } else if (typeStr === 'spki' && isPublic !== false) { + return kKeyEncodingSPKI; + } else if (typeStr === 'pkcs8' && isPublic !== true) { + return kKeyEncodingPKCS8; + } else if (typeStr === 'sec1' && isPublic !== true) { + if (keyType !== undefined && keyType !== 'ec') { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + typeStr, 'can only be used for EC keys'); + } + return kKeyEncodingSEC1; + } + + throw new ERR_INVALID_OPT_VALUE(optionName, typeStr); +} + +function option(name, objName) { + return objName === undefined ? name : `${objName}.${name}`; +} + +function parseKeyFormatAndType(enc, keyType, isPublic, objName) { + const { format: formatStr, type: typeStr } = enc; + + const isInput = keyType === undefined; + const format = parseKeyFormat(formatStr, + isInput ? kKeyFormatPEM : undefined, + option('format', objName)); + + const type = parseKeyType(typeStr, + !isInput || format === kKeyFormatDER, + keyType, + isPublic, + option('type', objName)); + + return { format, type }; +} + +function isStringOrBuffer(val) { + return typeof val === 'string' || isArrayBufferView(val); +} + +function parseKeyEncoding(enc, keyType, isPublic, objName) { + const isInput = keyType === undefined; + + const { + format, + type + } = parseKeyFormatAndType(enc, keyType, isPublic, objName); + + let cipher, passphrase; + if (isPublic !== true) { + ({ cipher, passphrase } = enc); + + if (!isInput && cipher != null) { + if (typeof cipher !== 'string') + throw new ERR_INVALID_OPT_VALUE(option('cipher', objName), cipher); + if (format === kKeyFormatDER && + (type === kKeyEncodingPKCS1 || + type === kKeyEncodingSEC1)) { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + encodingNames[type], 'does not support encryption'); + } + } + + if ((isInput && passphrase !== undefined && + !isStringOrBuffer(passphrase)) || + (!isInput && cipher != null && !isStringOrBuffer(passphrase))) { + throw new ERR_INVALID_OPT_VALUE(option('passphrase', objName), + passphrase); + } + } + + return { format, type, cipher, passphrase }; +} + +// Parses the public key encoding based on an object. keyType must be undefined +// when this is used to parse an input encoding and must be a valid key type if +// used to parse an output encoding. +function parsePublicKeyEncoding(enc, keyType, objName) { + return parseKeyFormatAndType(enc, keyType, true, objName); +} + +// Parses the private key encoding based on an object. keyType must be undefined +// when this is used to parse an input encoding and must be a valid key type if +// used to parse an output encoding. +function parsePrivateKeyEncoding(enc, keyType, objName) { + return parseKeyEncoding(enc, keyType, false, objName); +} + +function getKeyObjectHandle(key, isPublic, allowKeyObject) { + if (!allowKeyObject) { + return new ERR_INVALID_ARG_TYPE( + 'key', + ['string', 'Buffer', 'TypedArray', 'DataView'], + key + ); + } + if (isPublic != null) { + const expectedType = isPublic ? 'public' : 'private'; + if (key.type !== expectedType) + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, expectedType); + } + return key[kHandle]; +} + +function prepareAsymmetricKey(key, isPublic, allowKeyObject = true) { + if (isKeyObject(key)) { + // Best case: A key object, as simple as that. + return { data: getKeyObjectHandle(key, isPublic, allowKeyObject) }; + } else if (typeof key === 'string' || isArrayBufferView(key)) { + // Expect PEM by default, mostly for backward compatibility. + return { format: kKeyFormatPEM, data: key }; + } else if (typeof key === 'object') { + const data = key.key; + // The 'key' property can be a KeyObject as well to allow specifying + // additional options such as padding along with the key. + if (isKeyObject(data)) + return { data: getKeyObjectHandle(data, isPublic, allowKeyObject) }; + // Either PEM or DER using PKCS#1 or SPKI. + if (!isStringOrBuffer(data)) { + throw new ERR_INVALID_ARG_TYPE( + 'key', + ['string', 'Buffer', 'TypedArray', 'DataView', + ...(allowKeyObject ? ['KeyObject'] : [])], + key); + } + return { data, ...parseKeyEncoding(key, undefined, isPublic) }; + } else { + throw new ERR_INVALID_ARG_TYPE( + 'key', + ['string', 'Buffer', 'TypedArray', 'DataView', + ...(allowKeyObject ? ['KeyObject'] : [])], + key + ); + } +} + +function preparePublicKey(key, allowKeyObject) { + return prepareAsymmetricKey(key, true, allowKeyObject); +} + +function preparePrivateKey(key, allowKeyObject) { + return prepareAsymmetricKey(key, false, allowKeyObject); +} + +function preparePublicOrPrivateKey(key, allowKeyObject) { + return prepareAsymmetricKey(key, undefined, allowKeyObject); +} + +function prepareSecretKey(key, bufferOnly = false) { + if (!isArrayBufferView(key) && (bufferOnly || typeof key !== 'string')) { + if (isKeyObject(key) && !bufferOnly) { + if (key.type !== 'secret') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret'); + return key[kHandle]; + } else { + throw new ERR_INVALID_ARG_TYPE( + 'key', + ['Buffer', 'TypedArray', 'DataView', + ...(bufferOnly ? [] : ['string', 'KeyObject'])], + key); + } + } + return key; +} + +function createSecretKey(key) { + key = prepareSecretKey(key, true); + if (key.byteLength === 0) + throw new ERR_OUT_OF_RANGE('key.byteLength', '> 0', key.byteLength); + const handle = new KeyObjectHandle(kKeyTypeSecret); + handle.init(key); + return new SecretKeyObject(handle); +} + +function createPublicKey(key) { + const { format, type, data } = preparePublicKey(key, false); + const handle = new KeyObjectHandle(kKeyTypePublic); + handle.init(data, format, type); + return new PublicKeyObject(handle); +} + +function createPrivateKey(key) { + const { format, type, data, passphrase } = preparePrivateKey(key, false); + const handle = new KeyObjectHandle(kKeyTypePrivate); + handle.init(data, format, type, passphrase); + return new PrivateKeyObject(handle); +} + +function isKeyObject(key) { + return key instanceof KeyObject; +} + +module.exports = { + // Public API. + createSecretKey, + createPublicKey, + createPrivateKey, + + // These are designed for internal use only and should not be exposed. + parsePublicKeyEncoding, + parsePrivateKeyEncoding, + preparePublicKey, + preparePrivateKey, + preparePublicOrPrivateKey, + prepareSecretKey, + SecretKeyObject, + PublicKeyObject, + PrivateKeyObject, + isKeyObject +}; diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js index fa2d4998b6..32f7c37ec2 100644 --- a/lib/internal/crypto/sig.js +++ b/lib/internal/crypto/sig.js @@ -17,6 +17,10 @@ const { toBuf, validateArrayBufferView, } = require('internal/crypto/util'); +const { + preparePrivateKey, + preparePublicKey +} = require('internal/crypto/keys'); const { Writable } = require('stream'); function Sign(algorithm, options) { @@ -71,21 +75,18 @@ Sign.prototype.sign = function sign(options, encoding) { if (!options) throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); - var key = options.key || options; - var passphrase = options.passphrase || null; + const { data, format, type, passphrase } = preparePrivateKey(options, true); // Options specific to RSA - var rsaPadding = getPadding(options); - - var pssSaltLength = getSaltLength(options); + const rsaPadding = getPadding(options); + const pssSaltLength = getSaltLength(options); - key = validateArrayBufferView(key, 'key'); - - var ret = this[kHandle].sign(key, passphrase, rsaPadding, pssSaltLength); + const ret = this[kHandle].sign(data, format, type, passphrase, rsaPadding, + pssSaltLength); encoding = encoding || getDefaultEncoding(); if (encoding && encoding !== 'buffer') - ret = ret.toString(encoding); + return ret.toString(encoding); return ret; }; @@ -108,7 +109,12 @@ Verify.prototype._write = Sign.prototype._write; Verify.prototype.update = Sign.prototype.update; Verify.prototype.verify = function verify(options, signature, sigEncoding) { - var key = options.key || options; + const { + data, + format, + type + } = preparePublicKey(options, true); + sigEncoding = sigEncoding || getDefaultEncoding(); // Options specific to RSA @@ -116,12 +122,11 @@ Verify.prototype.verify = function verify(options, signature, sigEncoding) { var pssSaltLength = getSaltLength(options); - key = validateArrayBufferView(key, 'key'); - signature = validateArrayBufferView(toBuf(signature, sigEncoding), 'signature'); - return this[kHandle].verify(key, signature, rsaPadding, pssSaltLength); + return this[kHandle].verify(data, format, type, signature, + rsaPadding, pssSaltLength); }; legacyNativeHandle(Verify); |