From d25f4051dea1f93902e99c78b4aa75838ad985c3 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 23 Apr 2022 19:42:45 +0200 Subject: [PATCH] crypto: validate `this` in all webcrypto methods and getters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/42815 Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell Reviewed-By: Tobias Nießen --- lib/internal/crypto/webcrypto.js | 58 +++++-- test/parallel/test-webcrypto-constructors.js | 159 +++++++++++++++++++ 2 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 test/parallel/test-webcrypto-constructors.js diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index cf440ebf8ff39d..3b7e4d13d8b1c0 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -6,6 +6,7 @@ const { JSONStringify, ObjectDefineProperties, ReflectApply, + ReflectConstruct, SafeSet, SymbolToStringTag, StringPrototypeRepeat, @@ -31,6 +32,7 @@ const { TextDecoder, TextEncoder } = require('internal/encoding'); const { codes: { + ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, ERR_INVALID_THIS, } @@ -70,12 +72,21 @@ const { randomUUID: _randomUUID, } = require('internal/crypto/random'); -const randomUUID = () => _randomUUID(); +async function digest(algorithm, data) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); + return ReflectApply(asyncDigest, this, arguments); +} + +function randomUUID() { + if (this !== crypto) throw new ERR_INVALID_THIS('Crypto'); + return _randomUUID(); +} async function generateKey( algorithm, extractable, keyUsages) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); algorithm = normalizeAlgorithm(algorithm); validateBoolean(extractable, 'extractable'); validateArray(keyUsages, 'keyUsages'); @@ -123,6 +134,7 @@ async function generateKey( } async function deriveBits(algorithm, baseKey, length) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); algorithm = normalizeAlgorithm(algorithm); if (!isCryptoKey(baseKey)) throw new ERR_INVALID_ARG_TYPE('baseKey', 'CryptoKey', baseKey); @@ -194,6 +206,7 @@ async function deriveKey( derivedKeyAlgorithm, extractable, keyUsages) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); algorithm = normalizeAlgorithm(algorithm); derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm); if (!isCryptoKey(baseKey)) @@ -238,7 +251,11 @@ async function deriveKey( throw lazyDOMException('Unrecognized name.'); } - return importKey('raw', bits, derivedKeyAlgorithm, extractable, keyUsages); + return ReflectApply( + importKey, + this, + ['raw', bits, derivedKeyAlgorithm, extractable, keyUsages], + ); } async function exportKeySpki(key) { @@ -415,6 +432,7 @@ async function exportKeyJWK(key) { } async function exportKey(format, key) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); validateString(format, 'format'); validateOneOf(format, 'format', kExportFormats); if (!isCryptoKey(key)) @@ -496,6 +514,7 @@ async function importKey( algorithm, extractable, keyUsages) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); validateString(format, 'format'); validateOneOf(format, 'format', kExportFormats); if (format !== 'node.keyObject' && format !== 'jwk') @@ -557,8 +576,9 @@ async function importKey( // subtle.wrapKey() is essentially a subtle.exportKey() followed // by a subtle.encrypt(). async function wrapKey(format, key, wrappingKey, algorithm) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); algorithm = normalizeAlgorithm(algorithm); - let keyData = await exportKey(format, key); + let keyData = await ReflectApply(exportKey, this, [format, key]); if (format === 'jwk') { if (keyData == null || typeof keyData !== 'object') @@ -586,6 +606,7 @@ async function unwrapKey( unwrappedKeyAlgo, extractable, keyUsages) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); wrappedKey = getArrayBufferOrView(wrappedKey, 'wrappedKey'); unwrapAlgo = normalizeAlgorithm(unwrapAlgo); let keyData = await cipherOrWrap( @@ -607,7 +628,11 @@ async function unwrapKey( } } - return importKey(format, keyData, unwrappedKeyAlgo, extractable, keyUsages); + return ReflectApply( + importKey, + this, + [format, keyData, unwrappedKeyAlgo, extractable, keyUsages], + ); } function signVerify(algorithm, key, data, signature) { @@ -654,10 +679,12 @@ function signVerify(algorithm, key, data, signature) { } async function sign(algorithm, key, data) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); return signVerify(algorithm, key, data); } async function verify(algorithm, key, signature, data) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); return signVerify(algorithm, key, data, signature); } @@ -707,30 +734,39 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { } async function encrypt(algorithm, key, data) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); return cipherOrWrap(kWebCryptoCipherEncrypt, algorithm, key, data, 'encrypt'); } async function decrypt(algorithm, key, data) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); return cipherOrWrap(kWebCryptoCipherDecrypt, algorithm, key, data, 'decrypt'); } // The SubtleCrypto and Crypto classes are defined as part of the // Web Crypto API standard: https://www.w3.org/TR/WebCryptoAPI/ -class SubtleCrypto {} -const subtle = new SubtleCrypto(); +class SubtleCrypto { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } +} +const subtle = ReflectConstruct(function() {}, [], SubtleCrypto); class Crypto { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } + get subtle() { + if (this !== crypto) throw new ERR_INVALID_THIS('Crypto'); return subtle; } } -const crypto = new Crypto(); +const crypto = ReflectConstruct(function() {}, [], Crypto); function getRandomValues(array) { - if (!(this instanceof Crypto)) { - throw new ERR_INVALID_THIS('Crypto'); - } + if (this !== crypto) throw new ERR_INVALID_THIS('Crypto'); return ReflectApply(_getRandomValues, this, arguments); } @@ -799,7 +835,7 @@ ObjectDefineProperties( enumerable: true, configurable: true, writable: true, - value: asyncDigest, + value: digest, }, generateKey: { enumerable: true, diff --git a/test/parallel/test-webcrypto-constructors.js b/test/parallel/test-webcrypto-constructors.js new file mode 100644 index 00000000000000..ddc4e8cb0796b1 --- /dev/null +++ b/test/parallel/test-webcrypto-constructors.js @@ -0,0 +1,159 @@ +// Flags: --experimental-global-webcrypto +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +// Test CryptoKey constructor +{ + assert.throws(() => new CryptoKey(), { + name: 'TypeError', message: 'Illegal constructor', code: 'ERR_ILLEGAL_CONSTRUCTOR' + }); +} + +// Test SubtleCrypto constructor +{ + assert.throws(() => new SubtleCrypto(), { + name: 'TypeError', message: 'Illegal constructor', code: 'ERR_ILLEGAL_CONSTRUCTOR' + }); +} + +// Test Crypto constructor +{ + assert.throws(() => new Crypto(), { + name: 'TypeError', message: 'Illegal constructor', code: 'ERR_ILLEGAL_CONSTRUCTOR' + }); +} + +const notCrypto = Reflect.construct(function() {}, [], Crypto); +const notSubtle = Reflect.construct(function() {}, [], SubtleCrypto); + +// Test Crypto.prototype.subtle +{ + assert.throws(() => notCrypto.subtle, { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }); +} + +// Test Crypto.prototype.randomUUID +{ + assert.throws(() => notCrypto.randomUUID(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }); +} + +// Test Crypto.prototype.getRandomValues +{ + assert.throws(() => notCrypto.getRandomValues(new Uint8Array(12)), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }); +} + +// Test SubtleCrypto.prototype.encrypt +{ + assert.rejects(() => notSubtle.encrypt(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.decrypt +{ + assert.rejects(() => notSubtle.decrypt(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.sign +{ + assert.rejects(() => notSubtle.sign(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.verify +{ + assert.rejects(() => notSubtle.verify(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.digest +{ + assert.rejects(() => notSubtle.digest(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.generateKey +{ + assert.rejects(() => notSubtle.generateKey(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.deriveKey +{ + assert.rejects(() => notSubtle.deriveKey(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.deriveBits +{ + assert.rejects(() => notSubtle.deriveBits(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.importKey +{ + assert.rejects(() => notSubtle.importKey(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.exportKey +{ + assert.rejects(() => notSubtle.exportKey(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.wrapKey +{ + assert.rejects(() => notSubtle.wrapKey(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +// Test SubtleCrypto.prototype.unwrapKey +{ + assert.rejects(() => notSubtle.unwrapKey(), { + name: 'TypeError', code: 'ERR_INVALID_THIS', + }).then(common.mustCall()); +} + +{ + globalThis.crypto.subtle.importKey( + 'raw', + globalThis.crypto.getRandomValues(new Uint8Array(4)), + 'PBKDF2', + false, + ['deriveKey'], + ).then((key) => { + globalThis.crypto.subtle.importKey = common.mustNotCall(); + return globalThis.crypto.subtle.deriveKey({ + name: 'PBKDF2', + hash: 'SHA-512', + salt: globalThis.crypto.getRandomValues(new Uint8Array()), + iterations: 5, + }, key, { + name: 'AES-GCM', + length: 256 + }, true, ['encrypt', 'decrypt']); + }).then(common.mustCall()); +}