From dae283d96fd31ad0f30840a7e55ac97294f505ac Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 25 Aug 2020 10:05:51 -0700 Subject: [PATCH] crypto: refactoring internals, add WebCrypto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/nodejs/node/issues/678 Refs: https://github.com/nodejs/node/issues/26854 Signed-off-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/35093 Reviewed-By: Matteo Collina Reviewed-By: Anna Henningsen Reviewed-By: MichaĆ«l Zasso Reviewed-By: Rich Trott --- .github/CODEOWNERS | 2 +- benchmark/crypto/hkdf.js | 44 + benchmark/crypto/keygen.js | 71 + benchmark/crypto/webcrypto-digest.js | 58 + doc/api/crypto.md | 559 +- doc/api/deprecations.md | 2 +- doc/api/errors.md | 120 + doc/api/index.md | 1 + doc/api/webcrypto.md | 1672 ++++ lib/crypto.js | 19 +- lib/internal/crypto/aes.js | 342 + lib/internal/crypto/certificate.js | 29 +- lib/internal/crypto/cipher.js | 107 +- lib/internal/crypto/diffiehellman.js | 406 +- lib/internal/crypto/dsa.js | 265 + lib/internal/crypto/ec.js | 304 + lib/internal/crypto/hash.js | 68 +- lib/internal/crypto/hashnames.js | 90 + lib/internal/crypto/hkdf.js | 183 + lib/internal/crypto/keygen.js | 436 +- lib/internal/crypto/keys.js | 227 +- lib/internal/crypto/mac.js | 211 + lib/internal/crypto/pbkdf2.js | 115 +- lib/internal/crypto/random.js | 138 +- lib/internal/crypto/rsa.js | 371 + lib/internal/crypto/scrypt.js | 124 +- lib/internal/crypto/sig.js | 47 +- lib/internal/crypto/util.js | 320 +- lib/internal/crypto/webcrypto.js | 760 ++ lib/internal/per_context/primordials.js | 1 + node.gyp | 54 +- src/async_wrap.h | 9 +- src/base64-inl.h | 20 +- src/base64.h | 34 +- src/base_object.h | 2 +- src/crypto/README.md | 349 + src/crypto/crypto_aes.cc | 597 ++ src/crypto/crypto_aes.h | 89 + src/crypto/crypto_bio.cc | 8 +- src/crypto/crypto_bio.h | 1 - src/crypto/crypto_cipher.cc | 848 ++ src/crypto/crypto_cipher.h | 278 + src/crypto/crypto_clienthello-inl.h | 1 - src/crypto/crypto_clienthello.cc | 1 - src/crypto/crypto_clienthello.h | 1 - src/crypto/crypto_common.cc | 2 +- src/crypto/crypto_common.h | 1 - src/crypto/crypto_context.cc | 1284 +++ src/crypto/crypto_context.h | 125 + src/crypto/crypto_dh.cc | 677 ++ src/crypto/crypto_dh.h | 157 + src/crypto/crypto_dsa.cc | 272 + src/crypto/crypto_dsa.h | 87 + src/crypto/crypto_ecdh.cc | 814 ++ src/crypto/crypto_ecdh.h | 170 + src/crypto/crypto_hash.cc | 314 + src/crypto/crypto_hash.h | 92 + src/crypto/crypto_hkdf.cc | 147 + src/crypto/crypto_hkdf.h | 66 + src/crypto/crypto_hmac.cc | 275 + src/crypto/crypto_hmac.h | 94 + src/crypto/crypto_keygen.cc | 107 + src/crypto/crypto_keygen.h | 292 + src/crypto/crypto_keys.cc | 1304 +++ src/crypto/crypto_keys.h | 401 + src/crypto/crypto_pbkdf2.cc | 149 + src/crypto/crypto_pbkdf2.h | 76 + src/crypto/crypto_random.cc | 73 + src/crypto/crypto_random.h | 57 + src/crypto/crypto_rsa.cc | 550 ++ src/crypto/crypto_rsa.h | 140 + src/crypto/crypto_scrypt.cc | 153 + src/crypto/crypto_scrypt.h | 87 + src/crypto/crypto_sig.cc | 881 +++ src/crypto/crypto_sig.h | 165 + src/crypto/crypto_spkac.cc | 128 + src/crypto/crypto_spkac.h | 20 + src/crypto/crypto_ssl.cc | 860 ++ src/crypto/crypto_ssl.h | 146 + src/crypto/crypto_timing.cc | 55 + src/crypto/crypto_timing.h | 20 + src/crypto/crypto_util.cc | 600 ++ src/crypto/crypto_util.h | 674 ++ src/env.h | 21 + src/inspector_io.cc | 3 +- src/node.h | 13 +- src/node_crypto.cc | 7027 +---------------- src/node_crypto.h | 862 +- src/node_errors.h | 34 + src/node_native_module.cc | 1 + src/node_task_queue.cc | 2 +- src/quic/node_quic.cc | 2 +- src/quic/node_quic_crypto.cc | 2 + src/string_bytes.cc | 20 + src/tls_wrap.cc | 6 +- src/tls_wrap.h | 1 + test/cctest/test_base64.cc | 15 + test/fixtures/crypto/aes_cbc.js | 144 + test/fixtures/crypto/aes_ctr.js | 99 + test/fixtures/crypto/aes_gcm.js | 134 + test/fixtures/crypto/dsa.js | 93 + test/fixtures/crypto/ecdsa.js | 113 + test/fixtures/crypto/hmac.js | 52 + test/fixtures/crypto/rsa.js | 330 + test/fixtures/crypto/rsa_pkcs.js | 138 + test/fixtures/crypto/rsa_pss.js | 149 + test/parallel/test-crypto-authenticated.js | 30 +- test/parallel/test-crypto-certificate.js | 52 +- test/parallel/test-crypto-cipher-decipher.js | 12 +- .../test-crypto-cipheriv-decipheriv.js | 12 +- test/parallel/test-crypto-dh.js | 7 +- test/parallel/test-crypto-ecdh-convert-key.js | 2 +- test/parallel/test-crypto-hash.js | 2 - test/parallel/test-crypto-hkdf.js | 220 + test/parallel/test-crypto-hmac.js | 4 +- test/parallel/test-crypto-key-objects.js | 16 +- test/parallel/test-crypto-keygen.js | 13 +- test/parallel/test-crypto-pbkdf2.js | 24 +- test/parallel/test-crypto-random.js | 40 +- test/parallel/test-crypto-rsa-dsa.js | 53 +- test/parallel/test-crypto-secret-keygen.js | 124 + test/parallel/test-crypto-sign-verify.js | 31 +- .../test-webcrypto-cryptokey-workers.js | 56 + .../test-webcrypto-derivebits-ecdh.js | 266 + .../test-webcrypto-derivebits-hkdf.js | 543 ++ .../test-webcrypto-derivebits-node-dh.js | 218 + .../test-webcrypto-derivebits-pbkdf2.js | 670 ++ test/parallel/test-webcrypto-derivebits.js | 131 + .../parallel/test-webcrypto-derivekey-ecdh.js | 246 + test/parallel/test-webcrypto-derivekey.js | 154 + test/parallel/test-webcrypto-digest.js | 170 + .../test-webcrypto-encrypt-decrypt-aes.js | 198 + .../test-webcrypto-encrypt-decrypt-rsa.js | 244 + .../test-webcrypto-encrypt-decrypt.js | 118 + .../test-webcrypto-export-import-dsa.js | 224 + .../test-webcrypto-export-import-ec.js | 234 + .../test-webcrypto-export-import-rsa.js | 480 ++ test/parallel/test-webcrypto-export-import.js | 207 + test/parallel/test-webcrypto-keygen.js | 647 ++ test/parallel/test-webcrypto-random.js | 66 + .../test-webcrypto-sign-verify-ecdsa.js | 226 + .../test-webcrypto-sign-verify-hmac.js | 179 + .../test-webcrypto-sign-verify-node-dsa.js | 220 + .../test-webcrypto-sign-verify-rsa.js | 226 + test/parallel/test-webcrypto-sign-verify.js | 106 + test/parallel/test-webcrypto-wrap-unwrap.js | 276 + test/sequential/test-async-wrap-getasyncid.js | 14 +- .../test-crypto-timing-safe-equal.js | 6 - tools/doc/type-parser.js | 38 + 149 files changed, 28390 insertions(+), 8575 deletions(-) create mode 100644 benchmark/crypto/hkdf.js create mode 100644 benchmark/crypto/keygen.js create mode 100644 benchmark/crypto/webcrypto-digest.js create mode 100644 doc/api/webcrypto.md create mode 100644 lib/internal/crypto/aes.js create mode 100644 lib/internal/crypto/dsa.js create mode 100644 lib/internal/crypto/ec.js create mode 100644 lib/internal/crypto/hashnames.js create mode 100644 lib/internal/crypto/hkdf.js create mode 100644 lib/internal/crypto/mac.js create mode 100644 lib/internal/crypto/rsa.js create mode 100644 lib/internal/crypto/webcrypto.js create mode 100644 src/crypto/README.md create mode 100644 src/crypto/crypto_aes.cc create mode 100644 src/crypto/crypto_aes.h create mode 100644 src/crypto/crypto_cipher.cc create mode 100644 src/crypto/crypto_cipher.h create mode 100644 src/crypto/crypto_context.cc create mode 100644 src/crypto/crypto_context.h create mode 100644 src/crypto/crypto_dh.cc create mode 100644 src/crypto/crypto_dh.h create mode 100644 src/crypto/crypto_dsa.cc create mode 100644 src/crypto/crypto_dsa.h create mode 100644 src/crypto/crypto_ecdh.cc create mode 100644 src/crypto/crypto_ecdh.h create mode 100644 src/crypto/crypto_hash.cc create mode 100644 src/crypto/crypto_hash.h create mode 100644 src/crypto/crypto_hkdf.cc create mode 100644 src/crypto/crypto_hkdf.h create mode 100644 src/crypto/crypto_hmac.cc create mode 100644 src/crypto/crypto_hmac.h create mode 100644 src/crypto/crypto_keygen.cc create mode 100644 src/crypto/crypto_keygen.h create mode 100644 src/crypto/crypto_keys.cc create mode 100644 src/crypto/crypto_keys.h create mode 100644 src/crypto/crypto_pbkdf2.cc create mode 100644 src/crypto/crypto_pbkdf2.h create mode 100644 src/crypto/crypto_random.cc create mode 100644 src/crypto/crypto_random.h create mode 100644 src/crypto/crypto_rsa.cc create mode 100644 src/crypto/crypto_rsa.h create mode 100644 src/crypto/crypto_scrypt.cc create mode 100644 src/crypto/crypto_scrypt.h create mode 100644 src/crypto/crypto_sig.cc create mode 100644 src/crypto/crypto_sig.h create mode 100644 src/crypto/crypto_spkac.cc create mode 100644 src/crypto/crypto_spkac.h create mode 100644 src/crypto/crypto_ssl.cc create mode 100644 src/crypto/crypto_ssl.h create mode 100644 src/crypto/crypto_timing.cc create mode 100644 src/crypto/crypto_timing.h create mode 100644 src/crypto/crypto_util.cc create mode 100644 src/crypto/crypto_util.h create mode 100644 test/fixtures/crypto/aes_cbc.js create mode 100644 test/fixtures/crypto/aes_ctr.js create mode 100644 test/fixtures/crypto/aes_gcm.js create mode 100644 test/fixtures/crypto/dsa.js create mode 100644 test/fixtures/crypto/ecdsa.js create mode 100644 test/fixtures/crypto/hmac.js create mode 100644 test/fixtures/crypto/rsa.js create mode 100644 test/fixtures/crypto/rsa_pkcs.js create mode 100644 test/fixtures/crypto/rsa_pss.js create mode 100644 test/parallel/test-crypto-hkdf.js create mode 100644 test/parallel/test-crypto-secret-keygen.js create mode 100644 test/parallel/test-webcrypto-cryptokey-workers.js create mode 100644 test/parallel/test-webcrypto-derivebits-ecdh.js create mode 100644 test/parallel/test-webcrypto-derivebits-hkdf.js create mode 100644 test/parallel/test-webcrypto-derivebits-node-dh.js create mode 100644 test/parallel/test-webcrypto-derivebits-pbkdf2.js create mode 100644 test/parallel/test-webcrypto-derivebits.js create mode 100644 test/parallel/test-webcrypto-derivekey-ecdh.js create mode 100644 test/parallel/test-webcrypto-derivekey.js create mode 100644 test/parallel/test-webcrypto-digest.js create mode 100644 test/parallel/test-webcrypto-encrypt-decrypt-aes.js create mode 100644 test/parallel/test-webcrypto-encrypt-decrypt-rsa.js create mode 100644 test/parallel/test-webcrypto-encrypt-decrypt.js create mode 100644 test/parallel/test-webcrypto-export-import-dsa.js create mode 100644 test/parallel/test-webcrypto-export-import-ec.js create mode 100644 test/parallel/test-webcrypto-export-import-rsa.js create mode 100644 test/parallel/test-webcrypto-export-import.js create mode 100644 test/parallel/test-webcrypto-keygen.js create mode 100644 test/parallel/test-webcrypto-random.js create mode 100644 test/parallel/test-webcrypto-sign-verify-ecdsa.js create mode 100644 test/parallel/test-webcrypto-sign-verify-hmac.js create mode 100644 test/parallel/test-webcrypto-sign-verify-node-dsa.js create mode 100644 test/parallel/test-webcrypto-sign-verify-rsa.js create mode 100644 test/parallel/test-webcrypto-sign-verify.js create mode 100644 test/parallel/test-webcrypto-wrap-unwrap.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c41a2c35c1d86f..47f7380d36ff9c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,7 +59,7 @@ /lib/crypto.js @nodejs/crypto /lib/tls.js @nodejs/crypto @nodejs/net /src/node_crypto* @nodejs/crypto -/src/node_crypto_common* @nodejs/crypto @nodejs/quic +/src/crypto/* @nodejs/crypto @nodejs/quic # http diff --git a/benchmark/crypto/hkdf.js b/benchmark/crypto/hkdf.js new file mode 100644 index 00000000000000..4be15ab59071fe --- /dev/null +++ b/benchmark/crypto/hkdf.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); +const { + hkdf, + hkdfSync +} = require('crypto'); + +const bench = common.createBenchmark(main, { + sync: [0, 1], + size: [10, 64, 1024], + key: ['a', 'secret', 'this-is-a-much-longer-secret'], + salt: ['', 'salt'], + info: ['', 'info'], + hash: ['sha256', 'sha512'], + n: [1e3], +}); + +function measureSync(n, size, salt, info, hash, key) { + bench.start(); + for (let i = 0; i < n; ++i) + hkdfSync(hash, key, salt, info, size); + bench.end(n); +} + +function measureAsync(n, size, salt, info, hash, key) { + let remaining = n; + function done(err) { + assert.ifError(err); + if (--remaining === 0) + bench.end(n); + } + bench.start(); + for (let i = 0; i < n; ++i) + hkdf(hash, key, salt, info, size, done); +} + +function main({ n, sync, size, salt, info, hash, key }) { + if (sync) + measureSync(n, size, salt, info, hash, key); + else + measureAsync(n, size, salt, info, hash, key); +} diff --git a/benchmark/crypto/keygen.js b/benchmark/crypto/keygen.js new file mode 100644 index 00000000000000..e055a02cd2b116 --- /dev/null +++ b/benchmark/crypto/keygen.js @@ -0,0 +1,71 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); +const { + generateKeyPair, + generateKeyPairSync +} = require('crypto'); + +const bench = common.createBenchmark(main, { + method: ['rsaSync', 'rsaAsync', 'dsaSync', 'dsaAsync'], + n: [1e2], +}); + +const methods = { + rsaSync(n) { + bench.start(); + for (let i = 0; i < n; ++i) { + generateKeyPairSync('rsa', { + modulusLength: 1024, + publicExponent: 0x10001 + }); + } + bench.end(n); + }, + + rsaAsync(n) { + let remaining = n; + function done(err) { + assert.ifError(err); + if (--remaining === 0) + bench.end(n); + } + bench.start(); + for (let i = 0; i < n; ++i) + generateKeyPair('rsa', { + modulusLength: 512, + publicExponent: 0x10001 + }, done); + }, + + dsaSync(n) { + bench.start(); + for (let i = 0; i < n; ++i) { + generateKeyPairSync('dsa', { + modulusLength: 512, + divisorLength: 256, + }); + } + bench.end(n); + }, + + dsaAsync(n) { + let remaining = n; + function done(err) { + assert.ifError(err); + if (--remaining === 0) + bench.end(n); + } + bench.start(); + for (let i = 0; i < n; ++i) + generateKeyPair('dsa', { + modulusLength: 512, + divisorLength: 256, + }, done); + }, +}; + +function main({ n, method }) { + methods[method](n); +} diff --git a/benchmark/crypto/webcrypto-digest.js b/benchmark/crypto/webcrypto-digest.js new file mode 100644 index 00000000000000..2d95f868f66f7c --- /dev/null +++ b/benchmark/crypto/webcrypto-digest.js @@ -0,0 +1,58 @@ +'use strict'; + +const common = require('../common.js'); +const { + createHash, + webcrypto: { + subtle, + getRandomValues + } +} = require('crypto'); + +const bench = common.createBenchmark(main, { + sync: ['createHash', 'subtle'], + data: [10, 20, 50, 100], + method: ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512'], + n: [1e3], +}); + +const kMethods = { + 'SHA-1': 'sha1', + 'SHA-256': 'sha256', + 'SHA-384': 'sha384', + 'SHA-512': 'sha512' +}; + +// This benchmark only looks at clock time and ignores factors +// such as event loop delay, event loop utilization, and memory. +// As such, it is expected that the synchronous legacy method +// will always be faster in clock time. + +function measureLegacy(n, data, method) { + method = kMethods[method]; + bench.start(); + for (let i = 0; i < n; ++i) { + createHash(method).update(data).digest(); + } + bench.end(n); +} + +function measureSubtle(n, data, method) { + const ec = new TextEncoder(); + data = ec.encode(data); + const jobs = new Array(n); + bench.start(); + for (let i = 0; i < n; i++) + jobs[i] = subtle.digest(method, data); + Promise.all(jobs).then(() => bench.end(n)).catch((err) => { + process.nextTick(() => { throw err; }); + }); +} + +function main({ n, sync, data, method }) { + data = getRandomValues(Buffer.alloc(data)); + switch (sync) { + case 'createHash': return measureLegacy(n, data, method); + case 'subtle': return measureSubtle(n, data, method); + } +} diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 9012322ec7b200..5b22fc278ae211 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -53,12 +53,18 @@ The `crypto` module provides the `Certificate` class for working with SPKAC data. The most common usage is handling output generated by the HTML5 `` element. Node.js uses [OpenSSL's SPKAC implementation][] internally. -### `Certificate.exportChallenge(spkac)` +### `Certificate.exportChallenge(spkac[, encoding])` -* `spkac` {string | Buffer | TypedArray | DataView} +* `spkac` {string|ArrayBuffer|Buffer|TypedArray|DataView} +* `encoding` {string} The [encoding][] of the `spkac` string. * Returns: {Buffer} The challenge component of the `spkac` data structure, which includes a public key and a challenge. @@ -73,9 +79,14 @@ console.log(challenge.toString('utf8')); ### `Certificate.exportPublicKey(spkac[, encoding])` -* `spkac` {string | Buffer | TypedArray | DataView} +* `spkac` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `encoding` {string} The [encoding][] of the `spkac` string. * Returns: {Buffer} The public key component of the `spkac` data structure, which includes a public key and a challenge. @@ -88,12 +99,19 @@ console.log(publicKey); // Prints: the public key as ``` -### `Certificate.verifySpkac(spkac)` +### `Certificate.verifySpkac(spkac[, encoding])` -* `spkac` {Buffer | TypedArray | DataView} +* `spkac` {string|ArrayBuffer|Buffer|TypedArray|DataView} +* `encoding` {string} The [encoding][] of the `spkac` string. * Returns: {boolean} `true` if the given `spkac` data structure is valid, `false` otherwise. @@ -123,12 +141,13 @@ const cert1 = new crypto.Certificate(); const cert2 = crypto.Certificate(); ``` -#### `certificate.exportChallenge(spkac)` +#### `certificate.exportChallenge(spkac[, encoding])` -* `spkac` {string | Buffer | TypedArray | DataView} +* `spkac` {string|ArrayBuffer|Buffer|TypedArray|DataView} +* `encoding` {string} The [encoding][] of the `spkac` string. * Returns: {Buffer} The challenge component of the `spkac` data structure, which includes a public key and a challenge. @@ -140,12 +159,13 @@ console.log(challenge.toString('utf8')); // Prints: the challenge as a UTF8 string ``` -#### `certificate.exportPublicKey(spkac)` +#### `certificate.exportPublicKey(spkac[, encoding])` -* `spkac` {string | Buffer | TypedArray | DataView} +* `spkac` {string|ArrayBuffer|Buffer|TypedArray|DataView} +* `encoding` {string} The [encoding][] of the `spkac` string. * Returns: {Buffer} The public key component of the `spkac` data structure, which includes a public key and a challenge. @@ -157,12 +177,13 @@ console.log(publicKey); // Prints: the public key as ``` -#### `certificate.verifySpkac(spkac)` +#### `certificate.verifySpkac(spkac[, encoding])` -* `spkac` {Buffer | TypedArray | DataView} +* `spkac` {string|ArrayBuffer|Buffer|TypedArray|DataView} +* `encoding` {string} The [encoding][] of the `spkac` string. * Returns: {boolean} `true` if the given `spkac` data structure is valid, `false` otherwise. @@ -199,30 +220,28 @@ const crypto = require('crypto'); const algorithm = 'aes-192-cbc'; const password = 'Password used to generate key'; -// Key length is dependent on the algorithm. In this case for aes192, it is -// 24 bytes (192 bits). -// Use async `crypto.scrypt()` instead. -const key = crypto.scryptSync(password, 'salt', 24); -// Use `crypto.randomBytes()` to generate a random iv instead of the static iv -// shown here. -const iv = Buffer.alloc(16, 0); // Initialization vector. -const cipher = crypto.createCipheriv(algorithm, key, iv); +// First, we'll generate the key. The key length is dependent on the algorithm. +// In this case for aes192, it is 24 bytes (192 bits). +crypto.scrypt(password, 'salt', 24, (err, key) => { + if (err) throw err; + // Then, we'll generate a random initialization vector + crypto.randomFill(new Uint8Array(16), (err, iv) => { + if (err) throw err; + + // Once we have the key and iv, we can create and use the cipher... + const cipher = crypto.createCipheriv(algorithm, key, iv); -let encrypted = ''; -cipher.on('readable', () => { - let chunk; - while (null !== (chunk = cipher.read())) { - encrypted += chunk.toString('hex'); - } -}); -cipher.on('end', () => { - console.log(encrypted); - // Prints: e5f79c5915c02171eec6b212d5520d44480993d7d622a7c4c2da32f6efda0ffa -}); + let encrypted = ''; + cipher.setEncoding('hex'); -cipher.write('some clear text data'); -cipher.end(); + cipher.on('data', (chunk) => encrypted += chunk); + cipher.on('end', () => console.log(encrypted)); + + cipher.write('some clear text data'); + cipher.end(); + }); +}); ``` Example: Using `Cipher` and piped streams: @@ -230,21 +249,29 @@ Example: Using `Cipher` and piped streams: ```js const crypto = require('crypto'); const fs = require('fs'); +const { pipeline } = require('stream'); const algorithm = 'aes-192-cbc'; const password = 'Password used to generate key'; -// Use the async `crypto.scrypt()` instead. -const key = crypto.scryptSync(password, 'salt', 24); -// Use `crypto.randomBytes()` to generate a random iv instead of the static iv -// shown here. -const iv = Buffer.alloc(16, 0); // Initialization vector. -const cipher = crypto.createCipheriv(algorithm, key, iv); +// First, we'll generate the key. The key length is dependent on the algorithm. +// In this case for aes192, it is 24 bytes (192 bits). +crypto.scrypt(password, 'salt', 24, (err, key) => { + if (err) throw err; + // Then, we'll generate a random initialization vector + crypto.randomFill(new Uint8Array(16), (err, iv) => { + if (err) throw err; + + const cipher = crypto.createCipheriv(algorithm, key, iv); -const input = fs.createReadStream('test.js'); -const output = fs.createWriteStream('test.enc'); + const input = fs.createReadStream('test.js'); + const output = fs.createWriteStream('test.enc'); -input.pipe(cipher).pipe(output); + pipeline(input, cipher, output, (err) => { + if (err) throw err; + }); + }); +}); ``` Example: Using the [`cipher.update()`][] and [`cipher.final()`][] methods: @@ -254,18 +281,22 @@ const crypto = require('crypto'); const algorithm = 'aes-192-cbc'; const password = 'Password used to generate key'; -// Use the async `crypto.scrypt()` instead. -const key = crypto.scryptSync(password, 'salt', 24); -// Use `crypto.randomBytes` to generate a random iv instead of the static iv -// shown here. -const iv = Buffer.alloc(16, 0); // Initialization vector. -const cipher = crypto.createCipheriv(algorithm, key, iv); +// First, we'll generate the key. The key length is dependent on the algorithm. +// In this case for aes192, it is 24 bytes (192 bits). +crypto.scrypt(password, 'salt', 24, (err, key) => { + if (err) throw err; + // Then, we'll generate a random initialization vector + crypto.randomFill(new Uint8Array(16), (err, iv) => { + if (err) throw err; -let encrypted = cipher.update('some clear text data', 'utf8', 'hex'); -encrypted += cipher.final('hex'); -console.log(encrypted); -// Prints: e5f79c5915c02171eec6b212d5520d44480993d7d622a7c4c2da32f6efda0ffa + const cipher = crypto.createCipheriv(algorithm, key, iv); + + let encrypted = cipher.update('some clear text data', 'utf8', 'hex'); + encrypted += cipher.final('hex'); + console.log(encrypted); + }); +}); ``` ### `cipher.final([outputEncoding])` @@ -287,18 +318,19 @@ once will result in an error being thrown. added: v1.0.0 --> -* `buffer` {Buffer | TypedArray | DataView} +* `buffer` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `options` {Object} [`stream.transform` options][] * `plaintextLength` {number} + * `encoding` {string} The string encoding to use when `buffer` is a string. * Returns: {Cipher} for method chaining. When using an authenticated encryption mode (`GCM`, `CCM` and `OCB` are currently supported), the `cipher.setAAD()` method sets the value used for the _additional authenticated data_ (AAD) input parameter. -The `options` argument is optional for `GCM` and `OCB`. When using `CCM`, the -`plaintextLength` option must be specified and its value must match the length -of the plaintext in bytes. See [CCM mode][]. +The `plaintextLength` option is optional for `GCM` and `OCB`. When using `CCM`, +the `plaintextLength` option must be specified and its value must match the +length of the plaintext in bytes. See [CCM mode][]. The `cipher.setAAD()` method must be called before [`cipher.update()`][]. @@ -344,7 +376,7 @@ changes: description: The default `inputEncoding` changed from `binary` to `utf8`. --> -* `data` {string | Buffer | TypedArray | DataView} +* `data` {string|Buffer|TypedArray|DataView} * `inputEncoding` {string} The [encoding][] of the data. * `outputEncoding` {string} The [encoding][] of the return value. * Returns: {Buffer | string} @@ -480,14 +512,19 @@ than once will result in an error being thrown. -* `buffer` {Buffer | TypedArray | DataView} +* `buffer` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `options` {Object} [`stream.transform` options][] * `plaintextLength` {number} + * `encoding` {string} String encoding to use when `buffer` is a string. * Returns: {Decipher} for method chaining. When using an authenticated encryption mode (`GCM`, `CCM` and `OCB` are @@ -500,10 +537,14 @@ of the ciphertext in bytes. See [CCM mode][]. The `decipher.setAAD()` method must be called before [`decipher.update()`][]. -### `decipher.setAuthTag(buffer)` +### `decipher.setAuthTag(buffer[, encoding])` -* `buffer` {Buffer | TypedArray | DataView} +* `buffer` {string|Buffer|ArrayBuffer|TypedArray|DataView} +* `encoding` {string} String encoding to use when `buffer` is a string. * Returns: {Decipher} for method chaining. When using an authenticated encryption mode (`GCM`, `CCM` and `OCB` are @@ -554,7 +596,7 @@ changes: description: The default `inputEncoding` changed from `binary` to `utf8`. --> -* `data` {string | Buffer | TypedArray | DataView} +* `data` {string|Buffer|TypedArray|DataView} * `inputEncoding` {string} The [encoding][] of the `data` string. * `outputEncoding` {string} The [encoding][] of the return value. * Returns: {Buffer | string} @@ -610,7 +652,7 @@ assert.strictEqual(aliceSecret.toString('hex'), bobSecret.toString('hex')); added: v0.5.0 --> -* `otherPublicKey` {string | Buffer | TypedArray | DataView} +* `otherPublicKey` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `inputEncoding` {string} The [encoding][] of an `otherPublicKey` string. * `outputEncoding` {string} The [encoding][] of the return value. * Returns: {Buffer | string} @@ -693,7 +735,7 @@ string is returned; otherwise a [`Buffer`][] is returned. added: v0.5.0 --> -* `privateKey` {string | Buffer | TypedArray | DataView} +* `privateKey` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `encoding` {string} The [encoding][] of the `privateKey` string. Sets the Diffie-Hellman private key. If the `encoding` argument is provided, @@ -706,7 +748,7 @@ to be a [`Buffer`][], `TypedArray`, or `DataView`. added: v0.5.0 --> -* `publicKey` {string | Buffer | TypedArray | DataView} +* `publicKey` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `encoding` {string} The [encoding][] of the `publicKey` string. Sets the Diffie-Hellman public key. If the `encoding` argument is provided, @@ -793,7 +835,7 @@ assert.strictEqual(aliceSecret.toString('hex'), bobSecret.toString('hex')); added: v10.0.0 --> -* `key` {string | Buffer | TypedArray | DataView} +* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `curve` {string} * `inputEncoding` {string} The [encoding][] of the `key` string. * `outputEncoding` {string} The [encoding][] of the return value. @@ -849,7 +891,7 @@ changes: error --> -* `otherPublicKey` {string | Buffer | TypedArray | DataView} +* `otherPublicKey` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `inputEncoding` {string} The [encoding][] of the `otherPublicKey` string. * `outputEncoding` {string} The [encoding][] of the return value. * Returns: {Buffer | string} @@ -924,7 +966,7 @@ returned. added: v0.11.14 --> -* `privateKey` {string | Buffer | TypedArray | DataView} +* `privateKey` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `encoding` {string} The [encoding][] of the `privateKey` string. Sets the EC Diffie-Hellman private key. @@ -944,7 +986,7 @@ deprecated: v5.2.0 > Stability: 0 - Deprecated -* `publicKey` {string | Buffer | TypedArray | DataView} +* `publicKey` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `encoding` {string} The [encoding][] of the `publicKey` string. Sets the EC Diffie-Hellman public key. @@ -1105,7 +1147,7 @@ changes: description: The default `inputEncoding` changed from `binary` to `utf8`. --> -* `data` {string | Buffer | TypedArray | DataView} +* `data` {string|Buffer|TypedArray|DataView} * `inputEncoding` {string} The [encoding][] of the `data` string. Updates the hash content with the given `data`, the encoding of which @@ -1202,7 +1244,7 @@ changes: description: The default `inputEncoding` changed from `binary` to `utf8`. --> -* `data` {string | Buffer | TypedArray | DataView} +* `data` {string|Buffer|TypedArray|DataView} * `inputEncoding` {string} The [encoding][] of the `data` string. Updates the `Hmac` content with the given `data`, the encoding of which @@ -1413,6 +1455,9 @@ console.log(verify.verify(publicKey, signature)); -* `privateKey` {Object | string | Buffer | KeyObject} + +* `privateKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} * `dsaEncoding` {string} * `padding` {integer} * `saltLength` {integer} * `outputEncoding` {string} The [encoding][] of the return value. * Returns: {Buffer | string} + Calculates the signature on all the data passed through using either [`sign.update()`][] or [`sign.write()`][stream-writable-write]. @@ -1471,7 +1518,7 @@ changes: description: The default `inputEncoding` changed from `binary` to `utf8`. --> -* `data` {string | Buffer | TypedArray | DataView} +* `data` {string|Buffer|TypedArray|DataView} * `inputEncoding` {string} The [encoding][] of the `data` string. Updates the `Sign` content with the given `data`, the encoding of which @@ -1511,7 +1558,7 @@ changes: description: The default `inputEncoding` changed from `binary` to `utf8`. --> -* `data` {string | Buffer | TypedArray | DataView} +* `data` {string|Buffer|TypedArray|DataView} * `inputEncoding` {string} The [encoding][] of the `data` string. Updates the `Verify` content with the given `data`, the encoding of which @@ -1526,6 +1573,9 @@ This can be called many times with new data as it is streamed. -* `object` {Object | string | Buffer | KeyObject} + +* `object` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} * `dsaEncoding` {string} * `padding` {integer} * `saltLength` {integer} -* `signature` {string | Buffer | TypedArray | DataView} +* `signature` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `signatureEncoding` {string} The [encoding][] of the `signature` string. * Returns: {boolean} `true` or `false` depending on the validity of the signature for the data and public key. + Verifies the provided data using the given `object` and `signature`. @@ -1632,6 +1684,10 @@ This property is deprecated. Please use `crypto.setFips()` and added: v0.1.94 deprecated: v10.0.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/35093 + description: The password argument can be an ArrayBuffer and is limited to + a maximum of 2 ** 31 - 1 bytes. - version: v10.10.0 pr-url: https://github.com/nodejs/node/pull/21447 description: Ciphers in OCB mode are now supported. @@ -1644,7 +1700,7 @@ changes: > Stability: 0 - Deprecated: Use [`crypto.createCipheriv()`][] instead. * `algorithm` {string} -* `password` {string | Buffer | TypedArray | DataView} +* `password` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `options` {Object} [`stream.transform` options][] * Returns: {Cipher} @@ -1687,6 +1743,10 @@ Adversaries][] for details. * `algorithm` {string} -* `key` {string | Buffer | TypedArray | DataView | KeyObject} -* `iv` {string | Buffer | TypedArray | DataView | null} +* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} +* `iv` {string|ArrayBuffer|Buffer|TypedArray|DataView|null} * `options` {Object} [`stream.transform` options][] * Returns: {Cipher} @@ -1755,7 +1815,7 @@ changes: > Stability: 0 - Deprecated: Use [`crypto.createDecipheriv()`][] instead. * `algorithm` {string} -* `password` {string | Buffer | TypedArray | DataView} +* `password` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `options` {Object} [`stream.transform` options][] * Returns: {Decipher} @@ -1805,8 +1865,8 @@ changes: --> * `algorithm` {string} -* `key` {string | Buffer | TypedArray | DataView | KeyObject} -* `iv` {string | Buffer | TypedArray | DataView | null} +* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} +* `iv` {string|ArrayBuffer|Buffer|TypedArray|DataView|null} * `options` {Object} [`stream.transform` options][] * Returns: {Decipher} @@ -1854,10 +1914,10 @@ changes: from `binary` to `utf8`. --> -* `prime` {string | Buffer | TypedArray | DataView} +* `prime` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `primeEncoding` {string} The [encoding][] of the `prime` string. -* `generator` {number | string | Buffer | TypedArray | DataView} **Default:** - `2` +* `generator` {number|string|ArrayBuffer|Buffer|TypedArray|DataView} + **Default:** `2` * `generatorEncoding` {string} The [encoding][] of the `generator` string. * Returns: {DiffieHellman} @@ -1960,14 +2020,20 @@ input.on('readable', () => { * `algorithm` {string} -* `key` {string | Buffer | TypedArray | DataView | KeyObject} +* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} * `options` {Object} [`stream.transform` options][] + * `encoding` {string} The string encoding to use when `key` is a string. * Returns: {Hmac} Creates and returns an `Hmac` object that uses the given `algorithm` and `key`. @@ -2007,15 +2073,24 @@ input.on('readable', () => { ### `crypto.createPrivateKey(key)` -* `key` {Object | string | Buffer} - * `key`: {string | Buffer} The key material, either in PEM or DER format. + +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView} + * `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView} The key material, + either in PEM or DER format. * `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`. * `type`: {string} Must be `'pkcs1'`, `'pkcs8'` or `'sec1'`. This option is required only if the `format` is `'der'` and ignored if it is `'pem'`. * `passphrase`: {string | Buffer} The passphrase to use for decryption. + * `encoding`: {string} The string encoding to use when `key` is a string. * Returns: {KeyObject} + Creates and returns a new key object containing a private key. If `key` is a string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key` @@ -2028,6 +2103,10 @@ of the passphrase is limited to 1024 bytes. -* `key` {Object | string | Buffer | KeyObject} - * `key`: {string | Buffer} + +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView} + * `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView} * `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`. * `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required only if the `format` is `'der'`. + * `encoding` {string} The string encoding to use when `key` is a string. * Returns: {KeyObject} + Creates and returns a new key object containing a public key. If `key` is a string or `Buffer`, `format` is assumed to be `'pem'`; if `key` is a `KeyObject` @@ -2059,12 +2141,18 @@ extracted from the returned `KeyObject`. Similarly, if a `KeyObject` with type `'private'` is given, a new `KeyObject` with type `'public'` will be returned and it will be impossible to extract the private key from the returned object. -### `crypto.createSecretKey(key)` +### `crypto.createSecretKey(key[, encoding])` -* `key` {Buffer | TypedArray | DataView} +* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView} +* `encoding` {string} The string encoding when `key` is a string. * Returns: {KeyObject} Creates and returns a new key object containing a secret key for symmetric @@ -2125,6 +2213,61 @@ Computes the Diffie-Hellman secret based on a `privateKey` and a `publicKey`. Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'` (for Diffie-Hellman), `'ec'` (for ECDH), `'x448'`, or `'x25519'` (for ECDH-ES). +### `crypto.generateKey(type, options, callback)` + + +* `type`: {string} The intended use of the generated secret key. Currently + accepted values are `'hmac'` and `'aes'`. +* `options`: {Object} + * `length`: {number} The bit length of the key to generate. This must be a + value greater than 0. + * If `type` is `'hmac'`, the minimum is 1, and the maximum length is + 231-1. If the value is not a multiple of 8, the generated + key will be truncated to `Math.floor(length / 8)`. + * If `type` is `'aes'`, the length must be one of `128` or `256`. +* `callback`: {Function} + * `err`: {Error} + * `key`: {KeyObject} + +Asynchronously generates a new random secret key of the given `length`. The +`type` will determine which validations will be performed on the `length`. + +```js +const { generateKey } = require('crypto'); + +generateKey('hmac', { length: 64 }, (err, key) => { + if (err) throw err; + console.log(key.export().toString('hex')); // 46e..........620 +}); +``` + +### `crypto.generateKeySync(type, options)` + + +* `type`: {string} The intended use of the generated secret key. Currently + accepted values are `'hmac'` and `'aes'`. +* `options`: {Object} + * `length`: {number} The bit length of the key to generate. + * If `type` is `'hmac'`, the minimum is 1, and the maximum length is + 231-1. If the value is not a multiple of 8, the generated + key will be truncated to `Math.floor(length / 8)`. + * If `type` is `'aes'`, the length must be one of `128` or `256`. +* Returns: {KeyObject} + +Synchronously generates a new random secret key of the given `length`. The +`type` will determine which validations will be performed on the `length`. + +```js +const { generateKeySync } = require('crypto'); + +const key = generateKeySync('hmac', 64); +console.log(key.export().toString('hex')); // e89..........41e +``` + ### `crypto.generateKeyPair(type, options, callback)` + +* `digest` {string} The digest algorithm to use. +* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} The secret + key. It must be at least one byte in length. +* `salt` {string|ArrayBuffer|Buffer|TypedArray|DataView} The salt value. Must + be provided but can be zero-length. +* `info` {string|ArrayBuffer|Buffer|TypedArray|DataView} Additional info value. + Must be provided but can be zero-length, and cannot be more than 1024 bytes. +* `keylen` {number} The length of the key to generate. Must be greater than 0. + The maximum allowable value is `255` times the number of bytes produced by + the selected digest function (e.g. `sha512` generates 64-byte hashes, making + the maximum HKDF output 16320 bytes). +* `callback` {Function} + * `err` {Error} + * `derivedKey` {Buffer} + +HKDF is a simple key derivation function defined in RFC 5869. The given `key`, +`salt` and `info` are used with the `digest` to derive a key of `keylen` bytes. + +The supplied `callback` function is called with two arguments: `err` and +`derivedKey`. If an errors occurs while deriving the key, `err` will be set; +otherwise `err` will be `null`. The successfully generated `derivedKey` will +be passed to the callback as an {ArrayBuffer}. An error will be thrown if any +of the input aguments specify invalid values or types. + +```js +const crypto = require('crypto'); +crypto.hkdf('sha512', 'key', 'salt', 'info', 64, (err, derivedKey) => { + if (err) throw err; + console.log(Buffer.from(derivedKey).toString('hex')); // '24156e2...5391653' +}); +``` + +### `crypto.hkdfSync(digest, key, salt, info, keylen)` + + +* `digest` {string} The digest algorithm to use. +* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} The secret + key. It must be at least one byte in length. +* `salt` {string|ArrayBuffer|Buffer|TypedArray|DataView} The salt value. Must + be provided but can be zero-length. +* `info` {string|ArrayBuffer|Buffer|TypedArray|DataView} Additional info value. + Must be provided but can be zero-length, and cannot be more than 1024 bytes. +* `keylen` {number} The length of the key to generate. Must be greater than 0. + The maximum allowable value is `255` times the number of bytes produced by + the selected digest function (e.g. `sha512` generates 64-byte hashes, making + the maximum HKDF output 16320 bytes). +* Returns: {ArrayBuffer} + +Provides a synchronous HKDF key derivation function as defined in RFC 5869. The +given `key`, `salt` and `info` are used with the `digest` to derive a key of +`keylen` bytes. + +The successfully generated `derivedKey` will be returned as an {ArrayBuffer}. + +An error will be thrown if any of the input aguments specify invalid values or +types, or if the derived key cannot be generated. + +```js +const crypto = require('crypto'); +const derivedKey = crypto.hkdfSync('sha512', 'key', 'salt', 'info', 64); +console.log(Buffer.from(derivedKey).toString('hex')); // '24156e2...5391653' +``` + ### `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)` -* `password` {string|Buffer|TypedArray|DataView} -* `salt` {string|Buffer|TypedArray|DataView} +* `password` {string|ArrayBuffer|Buffer|TypedArray|DataView} +* `salt` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `iterations` {number} * `keylen` {number} * `digest` {string} @@ -2497,6 +2714,12 @@ An array of supported digest functions can be retrieved using -* `privateKey` {Object | string | Buffer | KeyObject} + +* `privateKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} * `oaepHash` {string} The hash function to use for OAEP padding and MGF1. **Default:** `'sha1'` - * `oaepLabel` {Buffer | TypedArray | DataView} The label to use for OAEP - padding. If not specified, no label is used. + * `oaepLabel` {string|ArrayBuffer|Buffer|TypedArray|DataView} The label to + use for OAEP padding. If not specified, no label is used. * `padding` {crypto.constants} An optional padding value defined in `crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING`, `crypto.constants.RSA_PKCS1_PADDING`, or `crypto.constants.RSA_PKCS1_OAEP_PADDING`. -* `buffer` {Buffer | TypedArray | DataView} +* `buffer` {string|ArrayBuffer|Buffer|TypedArray|DataView} * Returns: {Buffer} A new `Buffer` with the decrypted content. + Decrypts `buffer` with `privateKey`. `buffer` was previously encrypted using the corresponding public key, for example using [`crypto.publicEncrypt()`][]. @@ -2532,19 +2757,31 @@ object, the `padding` property can be passed. Otherwise, this function uses -* `privateKey` {Object | string | Buffer | KeyObject} - * `key` {string | Buffer | KeyObject} A PEM encoded private key. - * `passphrase` {string | Buffer} An optional passphrase for the private key. + +* `privateKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} + * `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} + A PEM encoded private key. + * `passphrase` {string|ArrayBuffer|Buffer|TypedArray|DataView} An optional + passphrase for the private key. * `padding` {crypto.constants} An optional padding value defined in `crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING` or `crypto.constants.RSA_PKCS1_PADDING`. -* `buffer` {Buffer | TypedArray | DataView} + * `encoding` {string} The string encoding to use when `buffer`, `key`, + or 'passphrase` are strings. +* `buffer` {string|ArrayBuffer|Buffer|TypedArray|DataView} * Returns: {Buffer} A new `Buffer` with the encrypted content. + Encrypts `buffer` with `privateKey`. The returned data can be decrypted using the corresponding public key, for example using [`crypto.publicDecrypt()`][]. @@ -2558,18 +2795,29 @@ object, the `padding` property can be passed. Otherwise, this function uses -* `key` {Object | string | Buffer | KeyObject} - * `passphrase` {string | Buffer} An optional passphrase for the private key. + +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} + * `passphrase` {string|ArrayBuffer|Buffer|TypedArray|DataView} An optional + passphrase for the private key. * `padding` {crypto.constants} An optional padding value defined in `crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING` or `crypto.constants.RSA_PKCS1_PADDING`. -* `buffer` {Buffer | TypedArray | DataView} + * `encoding` {string} The string encoding to use when `buffer`, `key`, + or 'passphrase` are strings. +* `buffer` {string|ArrayBuffer|Buffer|TypedArray|DataView} * Returns: {Buffer} A new `Buffer` with the decrypted content. + Decrypts `buffer` with `key`.`buffer` was previously encrypted using the corresponding private key, for example using [`crypto.privateEncrypt()`][]. @@ -2586,6 +2834,12 @@ be passed instead of a public key. -* `key` {Object | string | Buffer | KeyObject} - * `key` {string | Buffer | KeyObject} A PEM encoded public or private key. + +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} + * `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} + A PEM encoded public or private key, {KeyObject}, or {CryptoKey}. * `oaepHash` {string} The hash function to use for OAEP padding and MGF1. **Default:** `'sha1'` - * `oaepLabel` {Buffer | TypedArray | DataView} The label to use for OAEP - padding. If not specified, no label is used. - * `passphrase` {string | Buffer} An optional passphrase for the private key. + * `oaepLabel` {string|ArrayBuffer|Buffer|TypedArray|DataView} The label to + use for OAEP padding. If not specified, no label is used. + * `passphrase` {string|ArrayBuffer|Buffer|TypedArray|DataView} An optional + passphrase for the private key. * `padding` {crypto.constants} An optional padding value defined in `crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING`, `crypto.constants.RSA_PKCS1_PADDING`, or `crypto.constants.RSA_PKCS1_OAEP_PADDING`. -* `buffer` {Buffer | TypedArray | DataView} + * `encoding` {string} The string encoding to use when `buffer`, `key`, + `oaepLabel`, or 'passphrase` are strings. +* `buffer` {string|ArrayBuffer|Buffer|TypedArray|DataView} * Returns: {Buffer} A new `Buffer` with the encrypted content. + Encrypts the content of `buffer` with `key` and returns a new [`Buffer`][] with encrypted content. The returned data can be decrypted using @@ -2633,7 +2893,8 @@ changes: `ERR_INVALID_CALLBACK`. --> -* `size` {number} +* `size` {number} The number of bytes to generate. The `size` must + not be larger than `2**31 - 1`. * `callback` {Function} * `err` {Error} * `buf` {Buffer} @@ -2693,10 +2954,13 @@ changes: description: The `buffer` argument may be any `TypedArray` or `DataView`. --> -* `buffer` {Buffer|TypedArray|DataView} Must be supplied. +* `buffer` {ArrayBuffer|Buffer|TypedArray|DataView} Must be supplied. The + size of the provided `buffer` must not be larger than `2**31 - 1`. * `offset` {number} **Default:** `0` -* `size` {number} **Default:** `buffer.length - offset` -* Returns: {Buffer|TypedArray|DataView} The object passed as `buffer` argument. +* `size` {number} **Default:** `buffer.length - offset`. The `size` must + not be larger than `2**31 - 1`. +* Returns: {ArrayBuffer|Buffer|TypedArray|DataView} The object passed as + `buffer` argument. Synchronous version of [`crypto.randomFill()`][]. @@ -2712,7 +2976,8 @@ crypto.randomFillSync(buf, 5, 5); console.log(buf.toString('hex')); ``` -Any `TypedArray` or `DataView` instance may be passed as `buffer`. +Any `ArrayBuffer`, `TypedArray` or `DataView` instance may be passed as +`buffer`. ```js const a = new Uint32Array(10); @@ -2726,6 +2991,9 @@ console.log(Buffer.from(crypto.randomFillSync(b).buffer, const c = new DataView(new ArrayBuffer(10)); console.log(Buffer.from(crypto.randomFillSync(c).buffer, c.byteOffset, c.byteLength).toString('hex')); + +const d = new ArrayBuffer(10); +console.log(Buffer.from(crypto.randomFillSync(d)).toString('hex')); ``` ### `crypto.randomFill(buffer[, offset][, size], callback)` @@ -2739,9 +3007,11 @@ changes: description: The `buffer` argument may be any `TypedArray` or `DataView`. --> -* `buffer` {Buffer|TypedArray|DataView} Must be supplied. +* `buffer` {ArrayBuffer|Buffer|TypedArray|DataView} Must be supplied. The + size of the provided `buffer` must not be larger than `2**31 - 1`. * `offset` {number} **Default:** `0` -* `size` {number} **Default:** `buffer.length - offset` +* `size` {number} **Default:** `buffer.length - offset`. The `size` must + not be larger than `2**31 - 1`. * `callback` {Function} `function(err, buf) {}`. This function is similar to [`crypto.randomBytes()`][] but requires the first @@ -2769,7 +3039,8 @@ crypto.randomFill(buf, 5, 5, (err, buf) => { }); ``` -Any `TypedArray` or `DataView` instance may be passed as `buffer`. +Any `ArrayBuffer` `TypedArray` or `DataView` instance may be passed as +`buffer`. ```js const a = new Uint32Array(10); @@ -2792,6 +3063,12 @@ crypto.randomFill(c, (err, buf) => { console.log(Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength) .toString('hex')); }); + +const d = new ArrayBuffer(10); +crypto.randomFill(d, (err, buf) => { + if (err) throw err; + console.log(Buffer.from(buf).toString('hex')); +}); ``` This API uses libuv's threadpool, which can have surprising and @@ -2847,6 +3124,10 @@ console.log(`The dice rolled: ${n}`); -* `password` {string|Buffer|TypedArray|DataView} -* `salt` {string|Buffer|TypedArray|DataView} +* `password` {string|ArrayBuffer|Buffer|TypedArray|DataView} +* `salt` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `keylen` {number} * `options` {Object} * `cost` {number} CPU/memory cost parameter. Must be a power of two greater @@ -3005,10 +3286,12 @@ Throws an error if FIPS mode is not available. added: v12.0.0 --> + * `algorithm` {string | null | undefined} -* `data` {Buffer | TypedArray | DataView} -* `key` {Object | string | Buffer | KeyObject} +* `data` {ArrayBuffer|Buffer|TypedArray|DataView} +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} * Returns: {Buffer} + Calculates and returns the signature for `data` using the given private key and algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is @@ -3037,10 +3320,14 @@ additional properties can be passed: ### `crypto.timingSafeEqual(a, b)` -* `a` {Buffer | TypedArray | DataView} -* `b` {Buffer | TypedArray | DataView} +* `a` {ArrayBuffer|Buffer|TypedArray|DataView} +* `b` {ArrayBuffer|Buffer|TypedArray|DataView} * Returns: {boolean} This function is based on a constant-time algorithm. @@ -3059,13 +3346,19 @@ not introduce timing vulnerabilities. ### `crypto.verify(algorithm, data, key, signature)` -* `algorithm` {string | null | undefined} -* `data` {Buffer | TypedArray | DataView} -* `key` {Object | string | Buffer | KeyObject} -* `signature` {Buffer | TypedArray | DataView} + +* `algorithm` {string|null|undefined} +* `data` {ArrayBuffer| Buffer|TypedArray|DataView} +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} +* `signature` {ArrayBuffer|Buffer|TypedArray|DataView} * Returns: {boolean} + Verifies the given signature for `data` using the given key and algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is dependent upon the @@ -3096,6 +3389,15 @@ The `signature` argument is the previously calculated signature for the `data`. Because public keys can be derived from private keys, a private key or a public key may be passed for `key`. +### `crypto.webcrypto` + + +Type: {Crypto} An implementation of the Web Crypto API standard. + +See the [Web Crypto API documentation][] for details. + ## Notes ### Legacy streams API (prior to Node.js 0.10) @@ -3573,6 +3875,7 @@ See the [list of SSL OP Flags][] for details. [RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt [RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt [RFC 5208]: https://www.rfc-editor.org/rfc/rfc5208.txt +[Web Crypto API documentation]: webcrypto.md [`Buffer`]: buffer.md [`EVP_BytesToKey`]: https://www.openssl.org/docs/man1.1.0/crypto/EVP_BytesToKey.html [`KeyObject`]: #crypto_class_keyobject @@ -3591,7 +3894,7 @@ See the [list of SSL OP Flags][] for details. [`crypto.createHmac()`]: #crypto_crypto_createhmac_algorithm_key_options [`crypto.createPrivateKey()`]: #crypto_crypto_createprivatekey_key [`crypto.createPublicKey()`]: #crypto_crypto_createpublickey_key -[`crypto.createSecretKey()`]: #crypto_crypto_createsecretkey_key +[`crypto.createSecretKey()`]: #crypto_crypto_createsecretkey_key_encoding [`crypto.createSign()`]: #crypto_crypto_createsign_algorithm_options [`crypto.createVerify()`]: #crypto_crypto_createverify_algorithm_options [`crypto.getCurves()`]: #crypto_crypto_getcurves diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 86252dc23f1249..76520a4b4d0059 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -2690,7 +2690,7 @@ The [`crypto.Certificate()` constructor][] is deprecated. Use [`crypto.randomBytes()`]: crypto.md#crypto_crypto_randombytes_size_callback [`crypto.scrypt()`]: crypto.md#crypto_crypto_scrypt_password_salt_keylen_options_callback [`decipher.final()`]: crypto.md#crypto_decipher_final_outputencoding -[`decipher.setAuthTag()`]: crypto.md#crypto_decipher_setauthtag_buffer +[`decipher.setAuthTag()`]: crypto.md#crypto_decipher_setauthtag_buffer_encoding [`domain`]: domain.md [`ecdh.setPublicKey()`]: crypto.md#crypto_ecdh_setpublickey_publickey_encoding [`emitter.listenerCount(eventName)`]: events.md#events_emitter_listenercount_eventname diff --git a/doc/api/errors.md b/doc/api/errors.md index 2bbed76b1555cb..e4354bc6f950e8 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -792,22 +792,134 @@ The given crypto keys are incompatible with the attempted operation. The selected public or private key encoding is incompatible with other options. + +### `ERR_CRYPTO_INITIALIZATION_FAILED` + + +Initialization of the crypto subsystem failed. + + +### `ERR_CRYPTO_INVALID_AUTH_TAG` + + +An invalid authentication tag was provided. + + +### `ERR_CRYPTO_INVALID_COUNTER` + + +An invalid counter was provided for a counter-mode cipher. + + +### `ERR_CRYPTO_INVALID_CURVE` + + +An invalid elliptic-curve was provided. + ### `ERR_CRYPTO_INVALID_DIGEST` An invalid [crypto digest algorithm][] was specified. + +### `ERR_CRYPTO_INVALID_IV` + + +An invalid initialization vector was provided. + + +### `ERR_CRYPTO_INVALID_JWK` + + +An invalid JSON Web Key was provided. + ### `ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE` The given crypto key object's type is invalid for the attempted operation. + +### `ERR_CRYPTO_INVALID_KEYLEN` + + +An invalid key length was provided. + + +### `ERR_CRYPTO_INVALID_KEYPAIR` + + +An invalid key pair was provided. + + +### `ERR_CRYPTO_INVALID_KEYTYPE` + + +An invalid key type was provided. + + +### `ERR_CRYPTO_INVALID_MESSAGELEN` + + +An invalid message length was provided. + + +### `ERR_CRYPTO_INVALID_SCRYPT_PARAMS` + + +Invalid scrypt algorithm parameters were provided. + ### `ERR_CRYPTO_INVALID_STATE` A crypto method was used on an object that was in an invalid state. For instance, calling [`cipher.getAuthTag()`][] before calling `cipher.final()`. + +### `ERR_CRYPTO_INVALID_TAG_LENGTH` + + +An invalid authentication tag length was provided. + + +### `ERR_CRYPTO_JOB_INIT_FAILED` + + +Initialization of an asynchronous crypto operation failed. + + +### `ERR_CRYPTO_OPERATION_FAILED` + + +A crypto operation failed for an otherwise unspecified reason. + ### `ERR_CRYPTO_PBKDF2_ERROR` @@ -853,6 +965,14 @@ An unknown Diffie-Hellman group name was given. See The [`fs.Dir`][] was previously closed. + +### `ERR_CRYPTO_UNSUPPORTED_OPERATION` + + +An attempt to invoke an unsupported crypto operation was made. + ### `ERR_DIR_CONCURRENT_OPERATION` + +> Stability: 0 - Experimental + +Node.js provides an implementation of the standard [Web Crypto API][]. + +Use `require('crypto').webcrypto` to access this module. + +```js +const { subtle } = require('crypto').webcrypto; + +(async function() { + + const key = await subtle.generateKey({ + name: 'hmac', + length: 123 + }, true, ['sign', 'verify']); + + const digest = await subtle.sign({ + name: 'hmac' + }, key, 'I love cupcakes'); + +})(); +``` + +## Examples + +### Generating keys + +The {SubtleCrypto} class can be used to generate symmetric (secret) keys +or asymmetric key pairs (public key and private key). + +#### AES keys + +```js +const { subtle } = require('crypto').webcrypto; + +async function generateAesKey(length = 256) { + const key = await subtle.generateKey({ + name: 'AES-CBC', + length + }, true, ['encrypt', 'decrypt']); + + return key; +} +``` + +#### Elliptic curve key pairs + +```js +const { subtle } = require('crypto').webcrypto; + +async function generateEcKey(namedCurve = 'P-521') { + const { + publicKey, + privateKey + } = await subtle.generateKey({ + name: 'ECDSA', + namedCurve, + }, true, ['sign', 'verify']); + + return { publicKey, privateKey }; +} +``` + +#### HMAC keys + +```js +const { subtle } = require('crypto').webcrypto; + +async function generateHmacKey(hash = 'SHA-256') { + const key = await subtle.generateKey({ + name: 'HMAC', + hash + }, true, ['sign', 'verify']); + + return key; +} +``` + +#### RSA key pairs + +```js +const { subtle } = require('crypto').webcrypto; +const publicExponent = new Uint8Array([1, 0, 1]); + +async function generateRsaKey(modulusLength = 2048, hash = 'SHA-256') { + const { + publicKey, + privateKey + } = await subtle.generateKey({ + name: 'RSASSA-PKCS1-v1_5', + modulusLength, + publicExponent, + hash, + }, true, ['sign', 'verify']); + + return { publicKey, privateKey }; +} +``` + +### Encryption and decryption + +```js +const { subtle, getRandomValues } = require('crypto').webcrypto; + +async function aesEncrypt(plaintext) { + const ec = new TextEncoder(); + const key = await generateAesKey(); + const iv = getRandomValues(new Uint8Array(16)); + + const ciphertext = await subtle.encrypt({ + name: 'AES-CBC', + iv, + }, key, ec.encode(plaintext)); + + return { + key, + iv, + ciphertext + }; +} + +async function aesDecrypt(ciphertext, key, iv) { + const dec = new TextDecoder(); + const plaintext = await subtle.decrypt({ + name: 'AES-CBC', + iv, + }, key, ciphertext); + + return dec.decode(plaintext); +} +``` + +### Exporting and importing keys + +```js +const { subtle } = require('crypto').webcrypto; + +async function generateAndExportHmacKey(format = 'jwk', hash = 'SHA-512') { + const key = await subtle.generateKey({ + name: 'HMAC', + hash + }, true, ['sign', 'verify']); + + return subtle.exportKey(format, key); +} + +async function importHmacKey(keyData, format = 'jwk', hash = 'SHA-512') { + const key = await subtle.importKey(format, keyData, { + name: 'HMAC', + hash + }, true, ['sign', 'verify']); + + return key; +} +``` + +### Wrapping and unwrapping keys + +```js +const { subtle } = require('crypto').webcrypto; + +async function generateAndWrapHmacKey(format = 'jwk', hash = 'SHA-512') { + const [ + key, + wrappingKey + ] = await Promise.all([ + subtle.generateKey({ + name: 'HMAC', hash + }, true, ['sign', 'verify']), + subtle.generateKey({ + name: 'AES-KW', + length: 256 + }, true, ['wrapKey', 'unwrapKey']) + ]); + + const wrappedKey = await subtle.wrapKey(format, key, wrappingKey, 'AES-KW'); + + return wrappedKey; +} + +async function unwrapHmacKey( + wrappedKey, + wrappingKey, + format = 'jwk', + hash = 'SHA-512') { + + const key = await subtle.unwrapKey( + format, + wrappedKey, + unwrappingKey, + 'AES-KW', + { name: 'HMAC', hash }, + true, + ['sign', 'verify']); + + return key; +} +``` + +### Sign and verify + +```js +const { subtle } = require('crypto').webcrypto; + +async function sign(key, data) { + const ec = new TextEncoder(); + const signature = + await subtle.sign('RSASSA-PKCS1-v1_5', key, ec.encode(data)); + return signature; +} + +async function verify(key, signature, data) { + const ec = new TextEncoder(); + const verified = + await subtle.verify( + 'RSASSA-PKCS1-v1_5', + key, + signature, + ec.encode(data)); + return verified; +} +``` + +### Deriving bits and keys + +```js +const { subtle } = require('crypto').webcrypto; + +async function pbkdf2(pass, salt, iterations = 1000, length = 256) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + 'PBKDF2', + false, + ['deriveBits']); + const bits = await subtle.deriveBits({ + name: 'PBKDF2', + hash: 'SHA-512', + salt: ec.encode(salt), + iterations + }, key, length); + return bits; +} + +async function pbkdf2Key(pass, salt, iterations = 1000, length = 256) { + const ec = new TextEncoder(); + const keyMaterial = await subtle.importKey( + 'raw', + ec.encode(pass), + 'PBKDF2', + false, + ['deriveBits']); + const key = await subtle.deriveKey({ + name: 'PBKDF2', + hash: 'SHA-512', + salt: ec.encode(salt), + iterations + }, keyMaterial, { + name: 'AES-GCM', + length: 256 + }, true, ['encrypt', 'decrypt']); + return key; +} +``` + +### Digest + +```js +const { subtle } = require('crypto').webcrypto; + +async function digest(data, algorithm = 'SHA-512') { + const ec = new TextEncoder(); + const digest = await subtle.digest(algorithm, ec.encode(data)); + return digest; +} +``` + +## Algorithm Matrix + +The table details the algorithms supported by the Node.js Web Crypto API +implementation and the APIs supported for each: + +| Algorithm | `generateKey` | `exportKey` | `importKey` | `encrypt` | `decrypt` | `wrapKey` | `unwrapKey` | `deriveBits` | `deriveKey` | `sign` | `verify` | `digest` | +| --------------------- | ------------- | ----------- | ----------- | --------- | --------- | --------- | ----------- | ------------ | ----------- | ------ | -------- | -------- | +| `'RSASSA-PKCS1-v1_5'` | āœ” | āœ” | āœ” | | | | | | | āœ” | āœ” | | +| `'RSA-PSS'` | āœ” | āœ” | āœ” | | | | | | | āœ” | āœ” | | +| `'RSA-OAEP'` | āœ” | āœ” | āœ” | āœ” | āœ” | āœ” | āœ” | | | | | | +| `'ECDSA'` | āœ” | āœ” | āœ” | | | | | | | āœ” | āœ” | | +| `'ECDH'` | āœ” | āœ” | āœ” | | | | | āœ” | āœ” | | | | +| `'AES-CTR'` | āœ” | āœ” | āœ” | āœ” | āœ” | āœ” | āœ” | | | | | | +| `'AES-CBC'` | āœ” | āœ” | āœ” | āœ” | āœ” | āœ” | āœ” | | | | | | +| `'AES-GCM'` | āœ” | āœ” | āœ” | āœ” | āœ” | āœ” | āœ” | | | | | | +| `'AES-KW'` | āœ” | āœ” | āœ” | | | āœ” | āœ” | | | | | | +| `'HMAC'` | āœ” | āœ” | āœ” | | | | | | | āœ” | āœ” | | +| `'HKDF'` | | āœ” | āœ” | | | | | āœ” | āœ” | | | | +| `'PBKDF2'` | | āœ” | āœ” | | | | | āœ” | āœ” | | | | +| `'SHA-1'` | | | | | | | | | | | | āœ” | +| `'SHA-256'` | | | | | | | | | | | | āœ” | +| `'SHA-384'` | | | | | | | | | | | | āœ” | +| `'SHA-512'` | | | | | | | | | | | | āœ” | +| `'NODE-DSA'`1 | āœ” | āœ” | āœ” | | | | | | | āœ” | āœ” | | +| `'NODE-DH'`1 | āœ” | āœ” | āœ” | | | | | āœ” | āœ” | | | | + +1 Node.js-specific extension + +## Class: `Crypto` + + +Calling `require('crypto').webcrypto` returns an instance of the `Crypto` class. +`Crypto` is a singleton that provides access to the remainder of the crypto API. + +### `crypto.subtle` + + +* Type: {SubtleCrypto} + +Provides access to the `SubtleCrypto` API. + +### `crypto.getRandomValues(typedArray)` + + +* `typedArray` {Buffer|TypedArray|DataView|ArrayBuffer} +* Returns: {Buffer|TypedArray|DataView|ArrayBuffer} Returns `typedArray`. + +Generates cryptographically strong random values. The given `typedArray` is +filled with random values, and a reference to `typedArray` is returned. + +An error will be thrown if the given `typedArray` is larger than 65,536 bytes. + +## Class: `CryptoKey` + + +### `cryptoKey.algorithm` + + + +* Type: {AesKeyGenParams|RsaHashedKeyGenParams|EcKeyGenParams|HmacKeyGenParams|NodeDsaKeyGenParams|NodeDhKeyGenParams} + + +An object detailing the algorithm for which the key can be used along with +additional algorithm-specific parameters. + +Read-only. + +### `cryptoKey.extractable` + + +* Type: {boolean} + +When `true`, the {CryptoKey} can be extracted using either +`subtleCrypto.exportKey()` or `subtleCrypto.wrapKey()`. + +Read-only. + +### `cryptoKey.type` + + +* Type: {string} One of `'secret'`, `'private'`, or `'public'`. + +A string identifying whether the key is a symmetric (`'secret'`) or +asymmetric (`'private'` or `'public'`) key. + +### `cryptoKey.usages` + + +* Type: {string[]} + +An array of strings identifying the operations for which the +key may be used. + +The possible usages are: + +* `'encrypt'` - The key may be used to encrypt data. +* `'decrypt'` - The key may be used to decrypt data. +* `'sign'` - The key may be used to generate digital signatures. +* `'verify'` - The key may be used to verify digital signatures. +* `'deriveKey'` - The key may be used to derive a new key. +* `'deriveBits'` - The key may be used to derive bits. +* `'wrapKey'` - The key may be used to wrap another key. +* `'unwrapKey'` - The key may be used to unwrap another key. + +Valid key usages depend on the key algorithm (identified by +`cryptokey.algorithm.name`). + +| Key Type | `'encrypt'` | `'decrypt'` | `'sign'` | `'verify'` | `'deriveKey'` | `'deriveBits'` | `'wrapKey'` | `'unwrapKey'` | +| -------------------- | ----------- | ----------- | -------- | ---------- | ------------- | --------------- | ----------- | ------------- | +| `'AES-CBC'` | āœ” | āœ” | | | | | āœ” | āœ” | +| `'AES-CTR'` | āœ” | āœ” | | | | | āœ” | āœ” | +| `'AES-GCM'` | āœ” | āœ” | | | | | āœ” | āœ” | +| `'AES-KW'` | | | | | | | āœ” | āœ” | +| `'ECDH'` | | | | | āœ” | āœ” | | | +| `'ECDSA'` | | | āœ” | āœ” | | | | | +| `'HDKF'` | | | | | āœ” | āœ” | | | +| `'HMAC'` | | | āœ” | āœ” | | | | | +| `'PBKDF2'` | | | | | āœ” | āœ” | | | +| `'RSA-OAEP'` | āœ” | āœ” | | | | | āœ” | āœ” | +| `'RSA-PSS'` | | | āœ” | āœ” | | | | | +| `'RSASSA-PKCS1-v1_5'` | | | āœ” | āœ” | | | | | +| `'NODE-DSA'` 1 | | | āœ” | āœ” | | | | | +| `'NODE-DH'` 1 | | | | | āœ” | āœ” | | | +| `'NODE-SCRYPT'` 1 | | | | | āœ” | āœ” | | | + +1 Node.js-specific extension. + +## Class: `CryptoKeyPair` + + +The `CryptoKeyPair` is a simple dictionary object with `publicKey` and +`privateKey` properties, representing an asymmetric key pair. + +### `cryptoKeyPair.privateKey` + + +* Type: {CryptoKey} A {CryptoKey} whose `type` will be `'private'`. + +### `cryptoKeyPair.publicKey` + + +* Type: {CryptoKey} A {CryptoKey} whose `type` will be `'public'`. + +## Class: `SubtleCrypto` + + +### `subtle.decrypt(algorithm, key, data)` + + +* `algorithm`: {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `key`: {CryptoKey} +* `data`: {ArrayBuffer|TypedArray|DataView|Buffer} +* Returns: {Promise} containing {ArrayBuffer} + +Using the method and parameters specified in `algorithm` and the keying +material provided by `key`, `subtle.decrypt()` attempts to decipher the +provided `data`. If successful, the returned promise will be resolved with +an {ArrayBuffer} containing the plaintext result. + +The algorithms currently supported include: + +* `'RSA-OAEP'` +* `'AES-CTR'` +* `'AES-CBC'` +* `'AES-GCM`' + +### `subtle.deriveBits(algorithm, baseKey, length)` + + + +* `algorithm`: {EcdhKeyDeriveParams|HkdfParams|Pbkdf2Params|NodeDhDeriveBitsParams|NodeScryptParams} +* `baseKey`: {CryptoKey} +* `length`: {number} +* Returns: {Promise} containing {ArrayBuffer} + + +Using the method and parameters specified in `algorithm` and the keying +material provided by `baseKey`, `subtle.deriveBits()` attempts to generate +`length` bits. The Node.js implementation requires that `length` is a +multiple of `8`. If successful, the returned promise will be resolved with +an {ArrayBuffer} containing the generated data. + +The algorithms currently supported include: + +* `'ECDH'` +* `'HKDF'` +* `'PBKDF2'` +* `'NODE-DH'`1 +* `'NODE-SCRYPT'`1 + +1 Node.js-specific extension + +### `subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages)` + + + +* `algorithm`: {EcdhKeyDeriveParams|HkdfParams|Pbkdf2Params|NodeDhDeriveBitsParams|NodeScryptParams} +* `baseKey`: {CryptoKey} +* `derivedKeyAlgorithm`: {HmacKeyGenParams|AesKeyGenParams} +* `extractable`: {boolean} +* `keyUsages`: {string[]} See [Key usages][]. +* Returns: {Promise} containing {ArrayBuffer} + + +Using the method and parameters specified in `algorithm`, and the keying +material provided by `baseKey`, `subtle.deriveKey()` attempts to generate +a new {CryptoKey} based on the method and parameters in `derivedKeyAlgorithm`. + +Calling `subtle.deriveKey()` is equivalent to calling `subtle.deriveBits()` to +generate raw keying material, then passing the result into the +`subtle.importKey()` method using the `deriveKeyAlgorithm`, `extractable`, and +`keyUsages` parameters as input. + +The algorithms currently supported include: + +* `'ECDH'` +* `'HKDF'` +* `'PBKDF2'` +* `'NODE-DH'`1 +* '`NODE-SCRYPT'`1 + +1 Node.js-specific extension + +### `subtle.digest(algorithm, data)` + + +* `algorithm`: {string|Object} +* `data`: {ArrayBuffer|TypedArray|DataView|Buffer} +* Returns: {Promise} containing {ArrayBuffer} + +Using the method identified by `algorithm`, `subtle.digest()` attempts to +generate a digest of `data`. If successful, the returned promise is resolved +with an {ArrayBuffer} containing the computed digest. + +If `algorithm` is provided as a {string}, it must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If `algorithm` is provided as an {Object}, it must have a `name` property +whose value is one of the above. + +### `subtle.encrypt(algorithm, key, data)` + + +* `algorithm`: {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `key`: {CryptoKey} +* Returns: {Promise} containing {ArrayBuffer} + +Using the method and parameters specified by `algorithm` and the keying +material provided by `key`, `subtle.encrypt()` attempts to encipher `data`. +If successful, the returned promise is resolved with an {ArrayBuffer} +containing the encrypted result. + +The algorithms currently supported include: + +* `'RSA-OAEP'` +* `'AES-CTR'` +* `'AES-CBC'` +* `'AES-GCM`' + +### `subtle.exportKey(format, key)` + + +* `format`: {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, `'jwk'`, or + `node.keyObject`. +* `key`: {CryptoKey} +* Returns: {Promise} containing {ArrayBuffer}, or, if `format` is + `node.keyObject`, a {KeyObject}. + +Exports the given key into the specified format, if supported. + +If the {CryptoKey} is not extractable, the returned promise will reject. + +When `format` is either `'pkcs8'` or `'spki'` and the export is successful, +the returned promise will be resolved with an {ArrayBuffer} containing the +exported key data. + +When `format` is `'jwk'` and the export is successful, the returned promise +will be resolved with a JavaScript object conforming to the [JSON Web Key][] +specification. + +The special `'node.keyObject'` value for `format` is a Node.js-specific +extension that allows converting a {CryptoKey} into a Node.js {KeyObject}. + +| Key Type | `'spki'` | `'pkcs8'` | `'jwk'` | `'raw'` | +| --------------------- | -------- | --------- | ------- | ------- | +| `'AES-CBC'` | | | āœ” | āœ” | +| `'AES-CTR'` | | | āœ” | āœ” | +| `'AES-GCM'` | | | āœ” | āœ” | +| `'AES-KW'` | | | āœ” | āœ” | +| `'ECDH'` | āœ” | āœ” | āœ” | āœ” | +| `'ECDSA'` | āœ” | āœ” | āœ” | āœ” | +| `'HDKF'` | | | | | +| `'HMAC'` | | | āœ” | āœ” | +| `'PBKDF2'` | | | | | +| `'RSA-OAEP'` | āœ” | āœ” | āœ” | | +| `'RSA-PSS'` | āœ” | āœ” | āœ” | | +| `'RSASSA-PKCS1-v1_5'` | āœ” | āœ” | āœ” | | +| `'NODE-DSA'` 1 | āœ” | āœ” | āœ” | | +| `'NODE-DH'` 1 | āœ” | āœ” | | | +| `'NODE-SCRYPT'` 1 | | | | | + +1 Node.js-specific extension + +### `subtle.generateKey(algorithm, extractable, keyUsages)` + + + +* `algorithm`: {RsaHashedKeyGenParams|EcKeyGenParams|HmacKeyGenParams|AesKeyGenParams|NodeDsaKeyGenParams|NodeDhKeyGenParams} + +* `extractable`: {boolean} +* `keyUsages`: {string[]} See [Key usages][]. +* Returns: {Promise} containing {CryptoKey|CryptoKeyPair} + +Using the method and parameters provided in `algorithm`, `subtle.generateKey()` +attempts to generate new keying material. Depending the method used, the method +may generate either a single {CryptoKey} or a {CryptoKeyPair}. + +The {CryptoKeyPair} (public and private key) generating algorithms supported +include: + +* `'RSASSA-PKCS1-v1_5'` +* `'RSA-PSS'` +* `'RSA-OAEP'` +* `'ECDSA'` +* `'ECDH'` +* `'NODE-DSA'` 1 +* `'NODE-DH'` 1 + +The {CryptoKey} (secret key) generating algorithms supported include: + +* `'HMAC'` +* `'AES-CTR'` +* `'AES-CBC'` +* `'AES-GCM'` +* `'AES-KW'` + +1 Non-standard Node.js extension + +### `subtle.importKey(format, keyData, algorithm, extractable, keyUsages)` + + +* `format`: {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, `'jwk'`, or + `node.keyObject`. +* `keyData`: {ArrayBuffer|TypedArray|DataView|Buffer|KeyObject} + +* `algorithm`: {RsaHashedImportParams|EcKeyImportParams|HmacImportParams|AesImportParams|Pbkdf2ImportParams|NodeDsaImportParams|NodeDhImportParams|NodeScryptImportParams} + +* `extractable`: {boolean} +* `keyUsages`: {string[]} See [Key usages][]. +* Returns: {Promise} containing {CryptoKey} + +The `subtle.importKey()` method attempts to interpret the provided `keyData` +as the given `format` to create a {CryptoKey} instance using the provided +`algorithm`, `extractable`, and `keyUsages` arguments. If the import is +successful, the returned promise will be resolved with the created {CryptoKey}. + +The special `'node.keyObject'` value for `format` is a Node.js-specific +extension that allows converting a Node.js {KeyObject} into a {CryptoKey}. + +If importing a `'PBKDF2'` key, `extractable` must be `false`. + +The algorithms currently supported include: + +| Key Type | `'spki'` | `'pkcs8'` | `'jwk'` | `'raw'` | +| --------------------- | -------- | --------- | ------- | ------- | +| `'AES-CBC'` | | | āœ” | āœ” | +| `'AES-CTR'` | | | āœ” | āœ” | +| `'AES-GCM'` | | | āœ” | āœ” | +| `'AES-KW'` | | | āœ” | āœ” | +| `'ECDH'` | āœ” | āœ” | āœ” | āœ” | +| `'ECDSA'` | āœ” | āœ” | āœ” | āœ” | +| `'HDKF'` | | | | āœ” | +| `'HMAC'` | | | āœ” | āœ” | +| `'PBKDF2'` | | | | āœ” | +| `'RSA-OAEP'` | āœ” | āœ” | āœ” | | +| `'RSA-PSS'` | āœ” | āœ” | āœ” | | +| `'RSASSA-PKCS1-v1_5'` | āœ” | āœ” | āœ” | | +| `'NODE-DSA'` 1 | āœ” | āœ” | āœ” | | +| `'NODE-DH'` 1 | āœ” | āœ” | | | +| `'NODE-SCRYPT'` 1 | | | | āœ” | + +1 Node.js-specific extension + +### `subtle.sign(algorithm, key, data)` + + + +* `algorithm`: {RsaSignParams|RsaPssParams|EcdsaParams|HmacParams|NodeDsaSignParams} +* `key`: {CryptoKey} +* `data`: {ArrayBuffer|TypedArray|DataView|Buffer} +* Returns: {Promise} containing {ArrayBuffer} + + +Using the method and parameters given by `algorithm` and the keying material +provided by `key`, `subtle.sign()` attempts to generate a cryptographic +signature of `data`. If successful, the returned promise is resolved with +an {ArrayBuffer} containing the generated signature. + +The algorithms currently supported include: + +* `'RSASSA-PKCS1-v1_5'` +* `'RSA-PSS'` +* `'ECDSA'` +* `'HMAC'` +* `'NODE-DSA'`1 + +1 Non-standadrd Node.js extension + +### `subtle.unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgo, unwrappedKeyAlgo, extractable, keyUsages)` + + +* `format`: {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `wrappedKey`: {ArrayBuffer|TypedArray|DataView|Buffer} +* `unwrappingKey`: {CryptoKey} + +* `unwrapAlgo`: {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams|AesKwParams} +* `unwrappedKeyAlgo`: {RsaHashedImportParams|EcKeyImportParams|HmacImportParams|AesImportParams} + +* `extractable`: {boolean} +* `keyUsages`: {string[]} See [Key usages][]. +* Returns: {Promise} containing {CryptoKey} + +In cryptography, "wrapping a key" refers to exporting and then encrypting the +keying material. The `subtle.unwrapKey()` method attempts to decrypt a wrapped +key and create a {CryptoKey} instance. It is equivalent to calling +`subtle.decrypt()` first on the encrypted key data (using the `wrappedKey`, +`unwrapAlgo`, and `unwrappingKey` arguments as input) then passing the results +in to the `subtle.importKey()` method using the `unwrappedKeyAlgo`, +`extractable`, and `keyUsages` arguments as inputs. If successful, the returned +promise is resolved with a {CryptoKey} object. + +The wrapping algorithms currently supported include: + +* `'RSA-OAEP'` +* `'AES-CTR'`1 +* `'AES-CBC'`1 +* `'AES-GCM'`1 +* `'AES-KW'`1 + +The unwrapped key algorithms supported include: + +* `'RSASSA-PKCS1-v1_5'` +* `'RSA-PSS'` +* `'RSA-OAEP'` +* `'ECDSA'` +* `'ECDH'` +* `'HMAC'` +* `'AES-CTR'` +* `'AES-CBC'` +* `'AES-GCM'` +* `'AES-KW'` +* `'NODE-DSA'`1 +* `'NODE-DH'`1 + +1 Non-standard Node.js extension + +### `subtle.verify(algorithm, key, signature, data)` + + + +* `algorithm`: {RsaSignParams|RsaPssParams|EcdsaParams|HmacParams|NodeDsaSignParams} +* `key`: {CryptoKey} +* `signature`: {ArrayBuffer|TypedArray|DataView|Buffer} +* `data`: {ArrayBuffer|TypedArray|DataView|Buffer} +* Returns: {Promise} containing {boolean} + + +Using the method and parameters given in `algorithm` and the keying material +provided by `key`, `subtle.verify()` attempts to verify that `signature` is +a valid cryptographic signature of `data`. The returned promise is resolved +with either `true` or `false`. + +The algorithms currently supported include: + +* `'RSASSA-PKCS1-v1_5'` +* `'RSA-PSS'` +* `'ECDSA'` +* `'HMAC'` +* `'NODE-DSA'`1 + +1 Non-standard Node.js extension + +### `subtle.wrapKey(format, key, wrappingKey, wrapAlgo)` + + +* `format`: {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `key`: {CryptoKey} +* `wrappingKey`: {CryptoKey} +* `wrapAlgo`: {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams|AesKwParams} +* Returns: {Promise} containing {ArrayBuffer} + +In cryptography, "wrapping a key" refers to exporting and then encrypting the +keying material. The `subtle.wrapKey()` method exports the keying material into +the format identified by `format`, then encrypts it using the method and +parameters specified by `wrapAlgo` and the keying material provided by +`wrappingKey`. It is the equivalent to calling `subtle.exportKey()` using +`format` and `key` as the arguments, then passing the result to the +`subtle.encrypt()` method using `wrappingKey` and `wrapAlgo` as inputs. If +successful, the returned promise will be resolved with an {ArrayBuffer} +containing the encrypted key data. + +The wrapping algorithms currently supported include: + +* `'RSA-OAEP'` +* `'AES-CTR'` +* `'AES-CBC'` +* `'AES-GCM'` +* `'AES-KW'` + +## Algorithm Parameters + +The algorithm parameter objects define the methods and parameters used by +the various {SubtleCrypto} methods. While described here as "classes", they +are simple JavaScript dictionary objects. + +### Class: `AesCbcParams` + + +#### `aesCbcParams.iv` + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer} + +Provides the initialization vector. It must be exactly 16-bytes in length +and should be unpredictable and cryptographically random. + +#### `aesCbcParams.name` + + +* Type: {string} Must be `'AES-CBC'`. + +### Class: `AesCtrParams` + + +#### `aesCtrParams.counter` + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer} + +The initial value of the counter block. This must be exactly 16 bytes long. + +The `AES-CTR` method uses the rightmost `length` bits of the block as the +counter and the remaining bits as the nonce. + +#### `aesCtrParams.length` + + +* Type: {number} The number of bits in the `aesCtrParams.counter` that are + to be used as the counter. + +#### `aesCtrParams.name` + + +* Type: {string} Must be `'AES-CTR'`. + +### Class: `AesGcmParams` + + +#### `aesGcmParams.additionalData` + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} + +With the AES-GCM method, the `additionalData` is extra input that is not +encrypted but is included in the authentication of the data. The use of +`additionalData` is optional. + +#### `aesGcmParams.iv` + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer} + +The initialization vector must be unique for every encryption operation +using a given key. It is recommended by the AES-GCM specification that +this contain at least 12 random bytes. + +#### `aesGcmParams.name` + + +* Type: {string} Must be `'AES-GCM'`. + +#### `aesGcmParams.tagLength` + + +* Type: {number} The size in bits of the generated authentication tag. + This values must be one of `32`, `64`, `96`, `104`, `112`, `120`, or + `128`. **Default**: `128`. + +### Class: `AesImportParams` + + +#### 'aesImportParams.name` + + +* Type: {string} Must be one of `'AES-CTR'`, `'AES-CBC'`, `'AES-GCM'`, or + `'AES-KW'`. + +### Class: `AesKeyGenParams` + + +#### `aesKeyGenParams.length` + + +* Type: {number} + +The length of the AES key to be generated. This must be either `128`, `192`, +or `256`. + +#### `aesKeyGenParams.name` + + +* Type: {string} Must be one of `'AES-CBC'`, `'AES-CTR'`, `'AES-GCM'`, or + `'AES-KW'` + +### Class: `AesKwParams` + + +#### `aesKwParams.name` + + +* Type: {string} Must be `'AES-KW'`. + +### Class: `EcdhKeyDeriveParams` + + +#### `ecdhKeyDeriveParams.name` + + +* Type: {string} Must be `'ECDH'`. + +#### `ecdhKeyDeriveParams.public` + + +* Type: {CryptoKey} + +ECDH key derivation operates by taking as input one parties private key and +another parties public key -- using both to generate a common shared secret. +The `ecdhKeyDeriveParams.public` property is set to the other parties public +key. + +### Class: `EcdsaParams` + + +#### `ecdsaParams.hash` + + +* Type: {string|Object} + +If represented as a {string}, the value must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If represented as an {Object}, the object must have a `name` property +whose value is one of the above listed values. + +#### `ecdsaParams.name` + + +* Type: {string} Must be `'ECDSA'`. + +### Class: `EcKeyGenParams` + + +#### `ecKeyGenParams.name` + + +* Type: {string} Must be one of `'ECDSA'` or `'ECDH'`. + +#### `ecKeyGenParams.namedCurve` + + +* Type: {string} Must be one of `'P-256'`, `'P-384'` or `'P-521'`. + +### Class: `EcKeyImportParams` + + +#### `ecKeyImportParams.name` + + +* Type: {string} Must be one of `'ECDSA'` or `'ECDH'`. + +#### `ecKeyImportParams.namedCurve` + + +* Type: {string} Must be one of `'P-256'`, `'P-384'` or `'P-521'`. + +### Class: `HkdfParams` + + +#### `hkdfParams.hash` + + +* Type: {string|Object} + +If represented as a {string}, the value must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If represented as an {Object}, the object must have a `name` property +whose value is one of the above listed values. + +#### `hkdfParams.info` + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer} + +Provides application-specific contextual input to the HKDF algorithm. +This can be zero-length but must be provided. + +#### `hkdfParams.name` + + +* Type: {string} Must be `'HKDF'`. + +#### `hkdfParams.salt` + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer} + +The salt value significantly improves the strength of the HKDF algorithm. +It should be random or pseudo-random and should be the same length as the +output of the digest function (for instance, if using `'SHA-256'` as the +digest, the salt should be 256-bits of random data). + +### Class: `HmacImportParams` + + +#### 'hmacImportParams.hash` + + +* Type: {string|Object} + +If represented as a {string}, the value must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If represented as an {Object}, the object must have a `name` property +whose value is one of the above listed values. + +#### `hmacImportParams.length` + + +* Type: {number} + +The optional number of bits in the HMAC key. This is optional and should +be omitted for most cases. + +#### `hmacImportParams.name` + + +* Type: {string} Must be `'HMAC'`. + +### Class: `HmacKeyGenParams` + + +#### `hmacKeyGenParams.hash` + + +* Type: {string|Object} + +If represented as a {string}, the value must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If represented as an {Object}, the object must have a `name` property +whose value is one of the above listed values. + +#### `hmacKeyGenParams.length` + + +* Type: {number} + +The number of bits to generate for the HMAC key. If omitted, +the length will be determined by the hash algorithm used. +This is optional and should be omitted for most cases. + +#### `hmacKeyGenParams.name` + + +* Type: {string} Must be `'HMAC'`. + +### Class: `HmacParams` + + +#### `hmacParams.name` + + +* Type: {string} Must be `'HMAC`. + +### Class: `Pbkdf2ImportParams` + + +#### `pbkdf2ImportParams.name` + + +* Type: {string} Must be `'PBKDF2'` + +### Class: `Pbkdf2Params` + + +#### `pbkdb2Params.hash` + + +* Type: {string|Object} + +If represented as a {string}, the value must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If represented as an {Object}, the object must have a `name` property +whose value is one of the above listed values. + +#### `pbkdf2Params.iterations` + + +* Type: {number} + +The number of iterations the PBKDF2 algorithm should make when deriving bits. + +#### `pbkdf2Params.name` + + +* Type: {string} Must be `'PBKDF2'`. + +#### `pbkdf2Params.salt` + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer} + +Should be at least 16 random or pseudo-random bytes. + +### Class: `RsaHashedImportParams` + + +#### `rsaHashedImportParams.hash` + + +* Type: {string|Object} + +If represented as a {string}, the value must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If represented as an {Object}, the object must have a `name` property +whose value is one of the above listed values. + +#### `rsaHashedImportParams.name` + + +* Type: {string} Must be one of `'RSASSA-PKCS1-v1_5'`, `'RSA-PSS'`, or + `'RSA-OAEP'`. + +### Class: `RsaHashedKeyGenParams` + + +#### `rsaHashedKeyGenParams.hash` + + +* Type: {string|Object} + +If represented as a {string}, the value must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If represented as an {Object}, the object must have a `name` property +whose value is one of the above listed values. + +#### `rsaHashedKeyGenParams.modulusLength` + + +* Type: {number} + +The length in bits of the RSA modulus. As a best practice, this should be +at least `2048`. + +#### `rsaHashedKeyGenParams.name` + + +* Type: {string} Must be one of `'RSASSA-PKCS1-v1_5'`, `'RSA-PSS'`, or + `'RSA-OAEP'`. + +#### `rsaHashedKeyGenParams.publicExponent` + + +* Type: {Uint8Array} + +The RSA public exponent. This must be a {Uint8Array} containing a big-endian, +unsigned integer that must fit within 32-bits. The {Uint8Array} may contain an +arbitrary number of leading zero-bits. The value must be a prime number. Unless +there is reason to use a different value, use `new Uint8Array([1, 0, 1])` +(65537) as the public exponent. + +### Class: `RsaOaepParams` + + +#### rsaOaepParams.label + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer} + +An additional collection of bytes that will not be encrypted, but will be bound +to the generated ciphertext. + +The `rsaOaepParams.label` parameter is optional. + +#### rsaOaepParams.name + + +* Type: {string} must be `'RSA-OAEP'`. + +### Class: `RsaPssParams` + + +#### `rsaPssParams.name` + + +* Type: {string} Must be `'RSA-PSS'`. + +#### `rsaPssParams.saltLength` + + +* Type: {number} + +The length (in bytes) of the random salt to use. + +### Class: `RsaSignParams` + + +#### `rsaSignParams.name` + + +* Type: {string} Must be `'RSASSA-PKCS1-v1_5'` + +## Node.js-specific extensions + +The Node.js Web Crypto API extends various aspects of the Web Crypto API. +These extensions are consistently identified by prepending names with the +`node.` prefix. For instance, the `node.keyObject` key format can be +used with the `subtle.exportKey()` and `subtle.importKey()` methods to +convert between a WebCrypto {CryptoKey} object and a Node.js {KeyObject}. + +Care should be taken when using Node.js-specific extensions as they are +not supported by other WebCrypto implementations and reduce the portability +of code to other environments. + +### `NODE-DH` Algorithm + + +The `NODE-DH` algorithm is the common implementation of Diffie-Hellman +key agreement. + +#### Class: `NodeDhImportParams` + + +##### `nodeDhImportParams.name` + + +* Type: {string} Must be `'NODE-DH'`. + +#### Class: NodeDhKeyGenParams` + + +##### `nodeDhKeyGenParams.generator` + + +* Type: {number} A custom generator. + +##### `nodeDhKeyGenParams.group` + + +* Type: {string} The Diffie-Hellman group name. + +##### `nodeDhKeyGenParams.prime` + + +* Type: {Buffer} The prime parameter. + +##### `nodeDhKeyGenParams.primeLength` + + +* Type: {number} The length in bits of the prime. + +#### Class: NodeDhDeriveBitsParams + + +##### `nodeDhDeriveBitsParams.public` + + +* Type: {CryptoKey} The other parties public key. + +### `NODE-DSA` Algorithm + + +The `NODE-DSA` algorithm is the common implementation of the DSA digital +signature algorithm. + +#### Class: `NodeDsaImportParams` + + +##### `nodeDsaImportParams.hash` + + +* Type: {string|Object} + +If represented as a {string}, the value must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If represented as an {Object}, the object must have a `name` property +whose value is one of the above listed values. + +##### `nodeDsaImportParams.name` + + +* Type: {string} Must be `'NODE-DSA'`. + +#### Class: `NodeDsaKeyGenParams` + + +##### `nodeDsaKeyGenParams.divisorLength` + + +* Type: {number} + +The optional length in bits of the DSA divisor. + +##### `nodeDsaKeyGenParams.hash` + + +* Type: {string|Object} + +If represented as a {string}, the value must be one of: + +* `'SHA-1'` +* `'SHA-256'` +* `'SHA-384'` +* `'SHA-512'` + +If represented as an {Object}, the object must have a `name` property +whose value is one of the above listed values. + +##### `nodeDsaKeyGenParams.modulusLength` + + +* Type: {number} + +The length in bits of the DSA modulus. As a best practice, this should be +at least `2048`. + +##### `nodeDsaKeyGenParams.name` + + +* Type: {string} Must be `'NODE-DSA'`. + +#### Class: `NodeDsaSignParams` + + +##### `nodeDsaSignParams.name` + + +* Type: {string} Must be `'NODE-DSA'` + +### `NODE-SCRYPT` Algorithm + + +The `NODE-SCRYPT` algorithm is the common implementation of the scrypt key +derivation algorithm. + +#### Class: `NodeScryptImportParams` + + +##### `nodeScryptImportParams.name` + + +* Type: {string} Must be `'NODE-SCRYPT'`. + +#### Class: `NodeScryptParams` + + +##### `nodeScryptParams.encoding` + + +* Type: {string} The string encoding when `salt` is a string. + +##### `nodeScryptParams.maxmem` + + +* Type: {number} Memory upper bound. It is an error when (approximately) + `127 * N * r > maxmem`. **Default:** `32 * 1024 * 1024`. + +##### `nodeScryptParams.N` + + +* Type: {number} The CPU/memory cost parameter. Must e a power of two + greater than 1. **Default** `16384`. + +##### `nodeScryptParams.p` + + +* Type: {number} Parallelization parameter. **Default** `1`. + +##### `nodeScryptParams.r` + + +* Type: {number} Block size parameter. **Default**: `8`. + +##### `nodeScryptParams.salt` + + +* Type: {string|ArrayBuffer|Buffer|TypedArray|DataView} + +[JSON Web Key]: https://tools.ietf.org/html/rfc7517 +[Key usages]: #webcrypto_cryptokey_usages +[Web Crypto API]: https://www.w3.org/TR/WebCryptoAPI/ diff --git a/lib/crypto.js b/lib/crypto.js index b2bcc4d0a44592..736f732abc1e8c 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -63,9 +63,15 @@ const { scrypt, scryptSync } = require('internal/crypto/scrypt'); +const { + hkdf, + hkdfSync +} = require('internal/crypto/hkdf'); const { generateKeyPair, - generateKeyPairSync + generateKeyPairSync, + generateKey, + generateKeySync, } = require('internal/crypto/keygen'); const { createSecretKey, @@ -106,6 +112,7 @@ const { getHashes, setDefaultEncoding, setEngine, + lazyRequire, } = require('internal/crypto/util'); const Certificate = require('internal/crypto/certificate'); @@ -174,10 +181,14 @@ module.exports = { getCurves, getDiffieHellman: createDiffieHellmanGroup, getHashes, + hkdf, + hkdfSync, pbkdf2, pbkdf2Sync, generateKeyPair, generateKeyPairSync, + generateKey, + generateKeySync, privateDecrypt, privateEncrypt, publicDecrypt, @@ -266,6 +277,12 @@ ObjectDefineProperties(module.exports, { value: constants }, + webcrypto: { + configurable: false, + enumerable: true, + get() { return lazyRequire('internal/crypto/webcrypto'); } + }, + // Aliases for randomBytes are deprecated. // The ecosystem needs those to exist for backwards compatibility. prng: { diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js new file mode 100644 index 00000000000000..3ae60057c30cf8 --- /dev/null +++ b/lib/internal/crypto/aes.js @@ -0,0 +1,342 @@ +'use strict'; + +const { + ArrayFrom, + ArrayPrototypeIncludes, + ArrayPrototypePush, + MathFloor, + Promise, + SafeSet, +} = primordials; + +const { + AESCipherJob, + KeyObjectHandle, + kCryptoJobAsync, + kKeyVariantAES_CTR_128, + kKeyVariantAES_CBC_128, + kKeyVariantAES_GCM_128, + kKeyVariantAES_KW_128, + kKeyVariantAES_CTR_192, + kKeyVariantAES_CBC_192, + kKeyVariantAES_GCM_192, + kKeyVariantAES_KW_192, + kKeyVariantAES_CTR_256, + kKeyVariantAES_CBC_256, + kKeyVariantAES_GCM_256, + kKeyVariantAES_KW_256, + kWebCryptoCipherDecrypt, + kWebCryptoCipherEncrypt, +} = internalBinding('crypto'); + +const { + getArrayBufferOrView, + hasAnyNotIn, + jobPromise, + lazyDOMException, + validateByteLength, + validateKeyOps, + validateMaxBufferLength, + kAesKeyLengths, + kHandle, + kKeyObject, +} = require('internal/crypto/util'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + } +} = require('internal/errors'); + +const { + InternalCryptoKey, + SecretKeyObject, + createSecretKey, + isKeyObject, +} = require('internal/crypto/keys'); + +const { + generateKey, +} = require('internal/crypto/keygen'); + +const { + validateInteger, + validateOneOf, +} = require('internal/validators'); + +const kMaxCounterLength = 128; +const kTagLengths = [32, 64, 96, 104, 112, 120, 128]; + +function getAlgorithmName(name, length) { + switch (name) { + case 'AES-CBC': return `A${length}CBC`; + case 'AES-CTR': return `A${length}CTR`; + case 'AES-GCM': return `A${length}GCM`; + case 'AES-KW': return `A${length}KW`; + } +} + +function validateKeyLength(length) { + if (length !== 128 && length !== 192 && length !== 256) + throw lazyDOMException('Invalid key length', 'DataError'); +} + +function getVariant(name, length) { + switch (name) { + case 'AES-CBC': + switch (length) { + case 128: return kKeyVariantAES_CBC_128; + case 192: return kKeyVariantAES_CBC_192; + case 256: return kKeyVariantAES_CBC_256; + } + break; + case 'AES-CTR': + switch (length) { + case 128: return kKeyVariantAES_CTR_128; + case 192: return kKeyVariantAES_CTR_192; + case 256: return kKeyVariantAES_CTR_256; + } + break; + case 'AES-GCM': + switch (length) { + case 128: return kKeyVariantAES_GCM_128; + case 192: return kKeyVariantAES_GCM_192; + case 256: return kKeyVariantAES_GCM_256; + } + break; + case 'AES-KW': + switch (length) { + case 128: return kKeyVariantAES_KW_128; + case 192: return kKeyVariantAES_KW_192; + case 256: return kKeyVariantAES_KW_256; + } + break; + } +} + +function asyncAesCtrCipher(mode, key, data, { counter, length }) { + counter = getArrayBufferOrView(counter, 'algorithm.counter'); + validateByteLength(counter, 'algorithm.counter', 16); + // The length must specify an integer between 1 and 128. While + // there is no default, this should typically be 64. + if (typeof length !== 'number' || + length <= 0 || + length > kMaxCounterLength) { + throw lazyDOMException( + 'AES-CTR algorithm.length must be between 1 and 128', + 'OperationError'); + } + + return jobPromise(new AESCipherJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + data, + getVariant('AES-CTR', key.algorithm.length), + counter, + length)); +} + +function asyncAesCbcCipher(mode, key, data, { iv }) { + iv = getArrayBufferOrView(iv, 'algorithm.iv'); + validateByteLength(iv, 'algorithm.iv', 16); + return jobPromise(new AESCipherJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + data, + getVariant('AES-CBC', key.algorithm.length), + iv)); +} + +function asyncAesKwCipher(mode, key, data) { + return jobPromise(new AESCipherJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + data, + getVariant('AES-KW', key.algorithm.length))); +} + +function asyncAesGcmCipher( + mode, + key, + data, + { iv, additionalData, tagLength = 128 }) { + if (!ArrayPrototypeIncludes(kTagLengths, tagLength)) { + throw lazyDOMException( + `${tagLength} is not a valid AES-GCM tag length`, + 'OperationError'); + } + + iv = getArrayBufferOrView(iv, 'algorithm.iv'); + validateMaxBufferLength(iv, 'algorithm.iv'); + + if (additionalData !== undefined) { + additionalData = + getArrayBufferOrView(additionalData, 'algorithm.additionalData'); + validateMaxBufferLength(additionalData, 'algorithm.additionalData'); + } + + const tagByteLength = MathFloor(tagLength / 8); + let tag; + switch (mode) { + case kWebCryptoCipherDecrypt: + tag = data.slice(-tagByteLength); + data = data.slice(0, -tagByteLength); + break; + case kWebCryptoCipherEncrypt: + tag = tagByteLength; + break; + } + + return jobPromise(new AESCipherJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + data, + getVariant('AES-GCM', key.algorithm.length), + iv, + tag, + additionalData)); +} + +function aesCipher(mode, key, data, algorithm) { + switch (algorithm.name) { + case 'AES-CTR': return asyncAesCtrCipher(mode, key, data, algorithm); + case 'AES-CBC': return asyncAesCbcCipher(mode, key, data, algorithm); + case 'AES-GCM': return asyncAesGcmCipher(mode, key, data, algorithm); + case 'AES-KW': return asyncAesKwCipher(mode, key, data); + } +} + +async function aesGenerateKey(algorithm, extractable, keyUsages) { + const { name, length } = algorithm; + validateInteger(length, 'algorithm.length'); + validateOneOf(length, 'algorithm.length', kAesKeyLengths); + + const usageSet = new SafeSet(keyUsages); + + if (hasAnyNotIn(usageSet, 'encrypt', 'decrypt', 'wrapKey', 'unwrapKey')) { + throw lazyDOMException( + 'Unsupported key usage for an AES key', + 'SyntaxError'); + } + return new Promise((resolve, reject) => { + generateKey('aes', { length }, (err, key) => { + if (err) { + return reject(lazyDOMException( + 'The operation failed for an operation-specific reason ' + + `[${err.message}]`, + 'OperationError')); + } + + resolve(new InternalCryptoKey( + key, + { name, length }, + ArrayFrom(usageSet), + extractable)); + }); + }); +} + +async function aesImportKey( + algorithm, + format, + keyData, + extractable, + keyUsages) { + const { name } = algorithm; + const checkUsages = ['wrapKey', 'unwrapKey']; + if (name !== 'AES-KW') + ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt'); + + const usagesSet = new SafeSet(keyUsages); + if (hasAnyNotIn(usagesSet, ...checkUsages)) { + throw lazyDOMException( + 'Unsupported key usage for an AES key', + 'SyntaxError'); + } + + let keyObject; + let length; + switch (format) { + case 'node.keyObject': { + if (!isKeyObject(keyData)) + throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData); + + if (keyData.type !== 'secret') { + throw lazyDOMException( + `Unable to import AES key with format ${format}`, + 'NotSupportedError'); + } + + keyObject = keyData; + break; + } + case 'raw': { + validateKeyLength(keyData.byteLength * 8); + keyObject = createSecretKey(keyData); + break; + } + case 'jwk': { + if (keyData == null || typeof keyData !== 'object') + throw lazyDOMException('Invalid JWK keyData', 'DataError'); + + if (keyData.kty !== 'oct') + throw lazyDOMException('Invalid key type', 'DataError'); + + if (usagesSet.size > 0 && + keyData.use !== undefined && + keyData.use !== 'enc') { + throw lazyDOMException('Invalid use type', 'DataError'); + } + + validateKeyOps(keyData.key_ops, usagesSet); + + if (keyData.ext !== undefined && + keyData.ext === false && + extractable === true) { + throw lazyDOMException('JWK is not extractable', 'DataError'); + } + + const handle = new KeyObjectHandle(); + handle.initJwk(keyData); + + ({ length } = handle.keyDetail({ })); + validateKeyLength(length); + + if (keyData.alg !== undefined) { + if (typeof keyData.alg !== 'string') + throw lazyDOMException('Invalid alg', 'DataError'); + if (keyData.alg !== getAlgorithmName(algorithm.name, length)) + throw lazyDOMException('Algorithm mismatch', 'DataError'); + } + + keyObject = new SecretKeyObject(handle); + break; + } + default: + throw lazyDOMException( + `Unable to import AES key with format ${format}`, + 'NotSupportedError'); + } + + if (length === undefined) { + ({ length } = keyObject[kHandle].keyDetail({ })); + validateKeyLength(length); + } + + return new InternalCryptoKey( + keyObject, + { name, length }, + keyUsages, + extractable); +} + +module.exports = { + aesCipher, + aesGenerateKey, + aesImportKey, + getAlgorithmName, +}; diff --git a/lib/internal/crypto/certificate.js b/lib/internal/crypto/certificate.js index b9a86b00702366..000122f345b4fe 100644 --- a/lib/internal/crypto/certificate.js +++ b/lib/internal/crypto/certificate.js @@ -3,33 +3,38 @@ const { certExportChallenge, certExportPublicKey, - certVerifySpkac + certVerifySpkac, } = internalBinding('crypto'); -const { - validateBuffer -} = require('internal/validators'); const { - getArrayBufferView + getArrayBufferOrView, } = require('internal/crypto/util'); -function verifySpkac(spkac) { - validateBuffer(spkac, 'spkac'); - return certVerifySpkac(spkac); +// The functions contained in this file cover the SPKAC format +// (also refered to as Netscape SPKI). A general description of +// the format can be found at https://en.wikipedia.org/wiki/SPKAC + +function verifySpkac(spkac, encoding) { + return certVerifySpkac( + getArrayBufferOrView(spkac, 'spkac', encoding)); } function exportPublicKey(spkac, encoding) { return certExportPublicKey( - getArrayBufferView(spkac, 'spkac', encoding) - ); + getArrayBufferOrView(spkac, 'spkac', encoding)); } function exportChallenge(spkac, encoding) { return certExportChallenge( - getArrayBufferView(spkac, 'spkac', encoding) - ); + getArrayBufferOrView(spkac, 'spkac', encoding)); } +// The legacy implementation of this exposed the Certificate +// object and required that users create an instance before +// calling the member methods. This API pattern has been +// deprecated, however, as the method implementations do not +// rely on any object state. + // For backwards compatibility reasons, this cannot be converted into a // ES6 Class. function Certificate() { diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index a0244b19a8f137..44e174961318dc 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -5,39 +5,52 @@ const { } = primordials; const { - RSA_PKCS1_OAEP_PADDING, - RSA_PKCS1_PADDING -} = internalBinding('constants').crypto; + CipherBase, + privateDecrypt: _privateDecrypt, + privateEncrypt: _privateEncrypt, + publicDecrypt: _publicDecrypt, + publicEncrypt: _publicEncrypt, +} = internalBinding('crypto'); const { - ERR_CRYPTO_INVALID_STATE, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE -} = require('internal/errors').codes; -const { validateEncoding, validateString } = require('internal/validators'); + crypto: { + RSA_PKCS1_OAEP_PADDING, + RSA_PKCS1_PADDING, + } +} = internalBinding('constants'); + +const { + codes: { + ERR_CRYPTO_INVALID_STATE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + } +} = require('internal/errors'); + +const { + validateEncoding, + validateString, +} = require('internal/validators'); const { preparePrivateKey, preparePublicOrPrivateKey, - prepareSecretKey + prepareSecretKey, } = require('internal/crypto/keys'); + const { getDefaultEncoding, + getArrayBufferOrView, + getStringOption, kHandle, - getArrayBufferView } = require('internal/crypto/util'); -const { isArrayBufferView } = require('internal/util/types'); - const { - CipherBase, - privateDecrypt: _privateDecrypt, - privateEncrypt: _privateEncrypt, - publicDecrypt: _publicDecrypt, - publicEncrypt: _publicEncrypt -} = internalBinding('crypto'); + isArrayBufferView, +} = require('internal/util/types'); const assert = require('internal/assert'); + const LazyTransform = require('internal/streams/lazy_transform'); const { normalizeEncoding } = require('internal/util'); @@ -52,14 +65,13 @@ function rsaFunctionFor(method, defaultPadding, keyType) { preparePrivateKey(options) : preparePublicOrPrivateKey(options); const padding = options.padding || defaultPadding; - const { oaepHash, oaepLabel } = options; - if (oaepHash !== undefined && typeof oaepHash !== 'string') - throw new ERR_INVALID_ARG_TYPE('options.oaepHash', 'string', oaepHash); - if (oaepLabel !== undefined && !isArrayBufferView(oaepLabel)) { - throw new ERR_INVALID_ARG_TYPE('options.oaepLabel', - ['Buffer', 'TypedArray', 'DataView'], - oaepLabel); - } + const { oaepHash, encoding } = options; + let { oaepLabel } = options; + if (oaepHash !== undefined) + validateString(oaepHash, 'key.oaepHash'); + if (oaepLabel !== undefined) + oaepLabel = getArrayBufferOrView(oaepLabel, 'key.oaepLabel', encoding); + buffer = getArrayBufferOrView(buffer, 'buffer', encoding); return method(data, format, type, passphrase, buffer, padding, oaepHash, oaepLabel); }; @@ -95,7 +107,6 @@ function getUIntOption(options, key) { function createCipherBase(cipher, credential, options, decipher, iv) { const authTagLength = getUIntOption(options, 'authTagLength'); - this[kHandle] = new CipherBase(decipher); if (iv === undefined) { this[kHandle].init(cipher, credential, authTagLength); @@ -109,18 +120,24 @@ function createCipherBase(cipher, credential, options, decipher, iv) { function createCipher(cipher, password, options, decipher) { validateString(cipher, 'cipher'); - password = getArrayBufferView(password, 'password'); + password = getArrayBufferOrView(password, 'password'); createCipherBase.call(this, cipher, password, options, decipher); } function createCipherWithIV(cipher, key, options, decipher, iv) { validateString(cipher, 'cipher'); - key = prepareSecretKey(key); - iv = iv === null ? null : getArrayBufferView(iv, 'iv'); + const encoding = getStringOption(options, 'encoding'); + key = prepareSecretKey(key, encoding); + iv = iv === null ? null : getArrayBufferOrView(iv, 'iv'); createCipherBase.call(this, cipher, key, options, decipher, iv); } +// The Cipher class is part of the legacy Node.js crypto API. It exposes +// a stream-based encryption/decryption model. For backwards compatibility +// the Cipher class is defined using the legacy function syntax rather than +// ES6 classes. + function Cipher(cipher, password, options) { if (!(this instanceof Cipher)) return new Cipher(cipher, password, options); @@ -196,30 +213,27 @@ Cipher.prototype.getAuthTag = function getAuthTag() { }; -function setAuthTag(tagbuf) { - if (!isArrayBufferView(tagbuf)) { - throw new ERR_INVALID_ARG_TYPE('buffer', - ['Buffer', 'TypedArray', 'DataView'], - tagbuf); - } +function setAuthTag(tagbuf, encoding) { + tagbuf = getArrayBufferOrView(tagbuf, 'buffer', encoding); if (!this[kHandle].setAuthTag(tagbuf)) throw new ERR_CRYPTO_INVALID_STATE('setAuthTag'); return this; } Cipher.prototype.setAAD = function setAAD(aadbuf, options) { - if (!isArrayBufferView(aadbuf)) { - throw new ERR_INVALID_ARG_TYPE('buffer', - ['Buffer', 'TypedArray', 'DataView'], - aadbuf); - } - + const encoding = getStringOption(options, 'encoding'); const plaintextLength = getUIntOption(options, 'plaintextLength'); + aadbuf = getArrayBufferOrView(aadbuf, 'aadbuf', encoding); if (!this[kHandle].setAAD(aadbuf, plaintextLength)) throw new ERR_CRYPTO_INVALID_STATE('setAAD'); return this; }; +// The Cipheriv class is part of the legacy Node.js crypto API. It exposes +// a stream-based encryption/decryption model. For backwards compatibility +// the Cipheriv class is defined using the legacy function syntax rather than +// ES6 classes. + function Cipheriv(cipher, key, iv, options) { if (!(this instanceof Cipheriv)) return new Cipheriv(cipher, key, iv, options); @@ -245,6 +259,11 @@ ObjectSetPrototypeOf(Cipheriv.prototype, LazyTransform.prototype); ObjectSetPrototypeOf(Cipheriv, LazyTransform); addCipherPrototypeFunctions(Cipheriv); +// The Decipher class is part of the legacy Node.js crypto API. It exposes +// a stream-based encryption/decryption model. For backwards compatibility +// the Decipher class is defined using the legacy function syntax rather than +// ES6 classes. + function Decipher(cipher, password, options) { if (!(this instanceof Decipher)) return new Decipher(cipher, password, options); @@ -256,6 +275,10 @@ ObjectSetPrototypeOf(Decipher.prototype, LazyTransform.prototype); ObjectSetPrototypeOf(Decipher, LazyTransform); addCipherPrototypeFunctions(Decipher); +// The Decipheriv class is part of the legacy Node.js crypto API. It exposes +// a stream-based encryption/decryption model. For backwards compatibility +// the Decipheriv class is defined using the legacy function syntax rather than +// ES6 classes. function Decipheriv(cipher, key, iv, options) { if (!(this instanceof Decipheriv)) diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index c932a0df11b78b..8279f3058c449e 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -1,42 +1,84 @@ 'use strict'; const { + FunctionPrototypeCall, + MathFloor, ObjectDefineProperty, - Set + Promise, + SafeSet, } = primordials; const { Buffer } = require('buffer'); + const { - ERR_CRYPTO_ECDH_INVALID_FORMAT, - ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY, - ERR_CRYPTO_INCOMPATIBLE_KEY, - ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, -} = require('internal/errors').codes; + DHBitsJob, + DHKeyExportJob, + DiffieHellman: _DiffieHellman, + DiffieHellmanGroup: _DiffieHellmanGroup, + ECDH: _ECDH, + ECDHBitsJob, + ECDHConvertKey: _ECDHConvertKey, + statelessDH, + kCryptoJobAsync, +} = internalBinding('crypto'); + +const { + codes: { + ERR_CRYPTO_ECDH_INVALID_FORMAT, + ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY, + ERR_CRYPTO_INCOMPATIBLE_KEY, + ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_CALLBACK, + } +} = require('internal/errors'); + const { - validateString, validateInt32, + validateObject, + validateString, + validateUint32, } = require('internal/validators'); -const { isArrayBufferView } = require('internal/util/types'); -const { KeyObject } = require('internal/crypto/keys'); + +const { + isArrayBufferView, + isAnyArrayBuffer, +} = require('internal/util/types'); + +const { + KeyObject, + InternalCryptoKey, + createPrivateKey, + createPublicKey, + isCryptoKey, + isKeyObject, +} = require('internal/crypto/keys'); + +const { + generateKeyPair, +} = require('internal/crypto/keygen'); + const { + getArrayBufferOrView, getDefaultEncoding, + getUsagesUnion, + hasAnyNotIn, + jobPromise, + lazyDOMException, + toBuf, kHandle, - toBuf + kKeyObject, + kNamedCurveAliases, } = require('internal/crypto/util'); + const { - DiffieHellman: _DiffieHellman, - DiffieHellmanGroup: _DiffieHellmanGroup, - ECDH: _ECDH, - ECDHConvertKey: _ECDHConvertKey, - statelessDH -} = internalBinding('crypto'); -const { - POINT_CONVERSION_COMPRESSED, - POINT_CONVERSION_HYBRID, - POINT_CONVERSION_UNCOMPRESSED -} = internalBinding('constants').crypto; + crypto: { + POINT_CONVERSION_COMPRESSED, + POINT_CONVERSION_HYBRID, + POINT_CONVERSION_UNCOMPRESSED, + } +} = internalBinding('constants'); const DH_GENERATOR = 2; @@ -46,10 +88,11 @@ function DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding) { if (typeof sizeOrKey !== 'number' && typeof sizeOrKey !== 'string' && - !isArrayBufferView(sizeOrKey)) { + !isArrayBufferView(sizeOrKey) && + !isAnyArrayBuffer(sizeOrKey)) { throw new ERR_INVALID_ARG_TYPE( 'sizeOrKey', - ['number', 'string', 'Buffer', 'TypedArray', 'DataView'], + ['number', 'string', 'ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], sizeOrKey ); } @@ -122,7 +165,8 @@ function dhComputeSecret(key, inEnc, outEnc) { const encoding = getDefaultEncoding(); inEnc = inEnc || encoding; outEnc = outEnc || encoding; - const ret = this[kHandle].computeSecret(toBuf(key, inEnc)); + key = getArrayBufferOrView(key, 'key', inEnc); + const ret = this[kHandle].computeSecret(key); if (typeof ret === 'string') throw new ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY(); return encode(ret, outEnc); @@ -175,14 +219,16 @@ function dhGetPrivateKey(encoding) { DiffieHellman.prototype.setPublicKey = function setPublicKey(key, encoding) { encoding = encoding || getDefaultEncoding(); - this[kHandle].setPublicKey(toBuf(key, encoding)); + key = getArrayBufferOrView(key, 'key', encoding); + this[kHandle].setPublicKey(key); return this; }; DiffieHellman.prototype.setPrivateKey = function setPrivateKey(key, encoding) { encoding = encoding || getDefaultEncoding(); - this[kHandle].setPrivateKey(toBuf(key, encoding)); + key = getArrayBufferOrView(key, 'key', encoding); + this[kHandle].setPrivateKey(key); return this; }; @@ -214,21 +260,12 @@ ECDH.prototype.getPublicKey = function getPublicKey(encoding, format) { }; ECDH.convertKey = function convertKey(key, curve, inEnc, outEnc, format) { - if (typeof key !== 'string' && !isArrayBufferView(key)) { - throw new ERR_INVALID_ARG_TYPE( - 'key', - ['string', 'Buffer', 'TypedArray', 'DataView'], - key - ); - } - validateString(curve, 'curve'); - - const encoding = getDefaultEncoding(); - inEnc = inEnc || encoding; + const encoding = inEnc || getDefaultEncoding(); + key = getArrayBufferOrView(key, 'key', encoding); outEnc = outEnc || encoding; const f = getFormat(format); - const convertedKey = _ECDHConvertKey(toBuf(key, inEnc), curve, f); + const convertedKey = _ECDHConvertKey(key, curve, f); return encode(convertedKey, outEnc); }; @@ -250,7 +287,7 @@ function getFormat(format) { return POINT_CONVERSION_UNCOMPRESSED; } -const dhEnabledKeyTypes = new Set(['dh', 'ec', 'x448', 'x25519']); +const dhEnabledKeyTypes = new SafeSet(['dh', 'ec', 'x448', 'x25519']); function diffieHellman(options) { if (typeof options !== 'object') @@ -281,9 +318,294 @@ function diffieHellman(options) { return statelessDH(privateKey[kHandle], publicKey[kHandle]); } +// The deriveBitsECDH function is part of the Web Crypto API and serves both +// deriveKeys and deriveBits functions. +function deriveBitsECDH(name, publicKey, privateKey, callback) { + validateString(name, 'name'); + validateObject(publicKey, 'publicKey'); + validateObject(privateKey, 'privateKey'); + if (typeof callback !== 'function') + throw new ERR_INVALID_CALLBACK(callback); + const job = new ECDHBitsJob(kCryptoJobAsync, name, publicKey, privateKey); + job.ondone = (error, bits) => { + if (error) return FunctionPrototypeCall(callback, job, error); + FunctionPrototypeCall(callback, job, null, bits); + }; + job.run(); +} + +// The deriveBitsDH function is part of the Web Crypto API and serves both +// deriveKeys and deriveBits functions. +function deriveBitsDH(publicKey, privateKey, callback) { + validateObject(publicKey, 'publicKey'); + validateObject(privateKey, 'privateKey'); + if (typeof callback !== 'function') + throw new ERR_INVALID_CALLBACK(callback); + const job = new DHBitsJob(kCryptoJobAsync, publicKey, privateKey); + job.ondone = (error, bits) => { + if (error) return FunctionPrototypeCall(callback, job, error); + FunctionPrototypeCall(callback, job, null, bits); + }; + job.run(); +} + +function verifyAcceptableDhKeyUse(name, type, usages) { + let checkSet; + switch (type) { + case 'private': + checkSet = ['deriveBits', 'deriveKey']; + break; + case 'public': + checkSet = []; + break; + } + if (hasAnyNotIn(usages, ...checkSet)) { + throw lazyDOMException( + `Unsupported key usage for an ${name} key`, + 'SyntaxError'); + } +} + +async function dhGenerateKey( + algorithm, + extractable, + keyUsages) { + const usageSet = new SafeSet(keyUsages); + + if (hasAnyNotIn(usageSet, 'deriveKey', 'deriveBits')) { + throw lazyDOMException( + 'Unsupported key usage for a DH key', + 'SyntaxError'); + } + + const { + name, + primeLength, + generator, + group + } = algorithm; + let { prime } = algorithm; + + if (prime !== undefined) + prime = getArrayBufferOrView(prime); + + return new Promise((resolve, reject) => { + generateKeyPair('dh', { + prime, + primeLength, + generator, + group, + }, (err, pubKey, privKey) => { + if (err) { + return reject(lazyDOMException( + 'The operation failed for an operation-specific reason', + 'OperationError')); + } + + const algorithm = { name, prime, primeLength, generator, group }; + + const publicKey = new InternalCryptoKey(pubKey, algorithm, [], true); + + const privateKey = + new InternalCryptoKey( + privKey, + algorithm, + getUsagesUnion(usageSet, 'deriveBits', 'deriveKey'), + extractable); + + resolve({ publicKey, privateKey }); + }); + }); +} + +async function asyncDeriveBitsECDH(algorithm, baseKey, length) { + const { 'public': key } = algorithm; + + // Null means that we're not asking for a specific number of bits, just + // give us everything that is generated. + if (length !== null) + validateUint32(length, 'length'); + if (!isCryptoKey(key)) + throw new ERR_INVALID_ARG_TYPE('algorithm.public', 'CryptoKey', key); + + if (key.type !== 'public') { + throw lazyDOMException( + 'algorithm.public must be a public key', 'InvalidAccessError'); + } + if (baseKey.type !== 'private') { + throw lazyDOMException( + 'baseKey must be a private key', 'InvalidAccessError'); + } + + if (key.algorithm.name !== 'ECDH') { + throw lazyDOMException('Keys must be ECDH keys', 'InvalidAccessError'); + } + + if (key.algorithm.name !== baseKey.algorithm.name) { + throw lazyDOMException( + 'The public and private keys must be of the same type', + 'InvalidAccessError'); + } + + if (key.algorithm.namedCurve !== baseKey.algorithm.namedCurve) + throw lazyDOMException('Named curve mismatch', 'InvalidAccessError'); + + const bits = await new Promise((resolve, reject) => { + deriveBitsECDH( + kNamedCurveAliases[baseKey.algorithm.namedCurve], + key[kKeyObject][kHandle], + baseKey[kKeyObject][kHandle], (err, bits) => { + if (err) return reject(err); + resolve(bits); + }); + }); + + // If a length is not specified, return the full derived secret + if (length === null) + return bits; + + // If the length is not a multiple of 8, it will be truncated + // down to the nearest multiple of 8. + length = MathFloor(length / 8); + const { byteLength } = bits; + + // If the length is larger than the derived secret, throw. + // Otherwise, we either return the secret or a truncated + // slice. + if (byteLength < length) + throw lazyDOMException('derived bit length is too small', 'OperationError'); + + return length === byteLength ? bits : bits.slice(0, length); +} + +async function asyncDeriveBitsDH(algorithm, baseKey, length) { + const { 'public': key } = algorithm; + // Null has a specific meaning for DH + if (length !== null) + validateUint32(length, 'length'); + if (!isCryptoKey(key)) + throw new ERR_INVALID_ARG_TYPE('algorithm.public', 'CryptoKey', key); + + if (key.type !== 'public') { + throw lazyDOMException( + 'algorithm.public must be a public key', 'InvalidAccessError'); + } + if (baseKey.type !== 'private') { + throw lazyDOMException( + 'baseKey must be a private key', 'InvalidAccessError'); + } + + if (key.algorithm.name !== 'NODE-DH') + throw lazyDOMException('Keys must be DH keys', 'InvalidAccessError'); + + if (key.algorithm.name !== baseKey.algorithm.name) { + throw lazyDOMException( + 'The public and private keys must be of the same type', + 'InvalidAccessError'); + } + + const bits = await new Promise((resolve, reject) => { + deriveBitsDH( + key[kKeyObject][kHandle], + baseKey[kKeyObject][kHandle], (err, bits) => { + if (err) return reject(err); + resolve(bits); + }); + }); + + // If a length is not specified, return the full derived secret + if (length === null) + return bits; + + // If the length is not a multiple of 8, it will be truncated + // down to the nearest multiple of 8. + length = MathFloor(length / 8); + const { byteLength } = bits; + + // If the length is larger than the derived secret, throw. + // Otherwise, we either return the secret or a truncated + // slice. + if (byteLength < length) + throw lazyDOMException('derived bit length is too small', 'OperationError'); + + return length === byteLength ? bits : bits.slice(0, length); +} + +function dhExportKey(key, format) { + return jobPromise(new DHKeyExportJob( + kCryptoJobAsync, + format, + key[kKeyObject][kHandle])); +} + +async function dhImportKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + const usagesSet = new SafeSet(keyUsages); + let keyObject; + switch (format) { + case 'node.keyObject': { + if (!isKeyObject(keyData)) + throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData); + if (keyData.type === 'secret') + throw lazyDOMException('Invalid key type', 'InvalidAccessException'); + verifyAcceptableDhKeyUse(algorithm.name, keyData.type, usagesSet); + keyObject = keyData; + break; + } + case 'spki': { + verifyAcceptableDhKeyUse(algorithm.name, 'public', usagesSet); + keyObject = createPublicKey({ + key: keyData, + format: 'der', + type: 'spki' + }); + break; + } + case 'pkcs8': { + verifyAcceptableDhKeyUse(algorithm.name, 'private', usagesSet); + keyObject = createPrivateKey({ + key: keyData, + format: 'der', + type: 'pkcs8' + }); + break; + } + default: + throw lazyDOMException( + `Unable to import DH key with format ${format}`, + 'NotSupportedError'); + } + + const { + prime, + primeLength, + generator, + group, + } = keyObject[kHandle].keyDetail({}); + + return new InternalCryptoKey(keyObject, { + name: algorithm.name, + prime, + primeLength, + generator, + group, + }, keyUsages, extractable); +} + module.exports = { DiffieHellman, DiffieHellmanGroup, ECDH, - diffieHellman + diffieHellman, + deriveBitsECDH, + deriveBitsDH, + dhGenerateKey, + asyncDeriveBitsECDH, + asyncDeriveBitsDH, + dhExportKey, + dhImportKey, }; diff --git a/lib/internal/crypto/dsa.js b/lib/internal/crypto/dsa.js new file mode 100644 index 00000000000000..0baabc4680b108 --- /dev/null +++ b/lib/internal/crypto/dsa.js @@ -0,0 +1,265 @@ +'use strict'; + +const { + Promise, + SafeSet, +} = primordials; + +const { + DSAKeyExportJob, + KeyObjectHandle, + SignJob, + kCryptoJobAsync, + kKeyTypePrivate, + kSignJobModeSign, + kSignJobModeVerify, +} = internalBinding('crypto'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_MISSING_OPTION, + } +} = require('internal/errors'); + +const { + validateUint32, +} = require('internal/validators'); + +const { + InternalCryptoKey, + PrivateKeyObject, + PublicKeyObject, + createPrivateKey, + createPublicKey, + isKeyObject, +} = require('internal/crypto/keys'); + +const { + generateKeyPair, +} = require('internal/crypto/keygen'); + +const { + getUsagesUnion, + hasAnyNotIn, + jobPromise, + lazyDOMException, + normalizeHashName, + validateKeyOps, + kKeyObject, + kHandle, +} = require('internal/crypto/util'); + +function verifyAcceptableDsaKeyUse(name, type, usages) { + let checkSet; + switch (type) { + case 'private': + checkSet = ['sign']; + break; + case 'public': + checkSet = ['verify']; + break; + } + if (hasAnyNotIn(usages, ...checkSet)) { + throw lazyDOMException( + `Unsupported key usage for an ${name} key`, + 'SyntaxError'); + } +} + +async function dsaGenerateKey( + algorithm, + extractable, + keyUsages) { + const { + name, + modulusLength, + divisorLength, + hash + } = algorithm; + + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + validateUint32(modulusLength, 'algorithm.modulusLength'); + + const usageSet = new SafeSet(keyUsages); + + if (hasAnyNotIn(usageSet, 'sign', 'verify')) { + throw lazyDOMException( + 'Unsupported key usage for a DSA key', + 'SyntaxError'); + } + + return new Promise((resolve, reject) => { + generateKeyPair('dsa', { + modulusLength, + divisorLength, + }, (err, pubKey, privKey) => { + if (err) { + return reject(lazyDOMException( + 'The operation failed for an operation-specific reason', + 'OperationError')); + } + + const algorithm = { + name, + modulusLength, + divisorLength, + hash: { name: hash.name } + }; + + const publicKey = + new InternalCryptoKey( + pubKey, + algorithm, + getUsagesUnion(usageSet, 'verify'), + true); + + const privateKey = + new InternalCryptoKey( + privKey, + algorithm, + getUsagesUnion(usageSet, 'sign'), + extractable); + + resolve({ publicKey, privateKey }); + }); + }); +} + +function dsaExportKey(key, format) { + return jobPromise(new DSAKeyExportJob( + kCryptoJobAsync, + format, + key[kKeyObject][kHandle])); +} + +async function dsaImportKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + const { hash } = algorithm; + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + + const usagesSet = new SafeSet(keyUsages); + let keyObject; + switch (format) { + case 'node.keyObject': { + if (!isKeyObject(keyData)) + throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData); + if (keyData.type === 'secret') + throw lazyDOMException('Invalid key type', 'InvalidAccessException'); + verifyAcceptableDsaKeyUse(algorithm.name, keyData.type, usagesSet); + keyObject = keyData; + break; + } + case 'spki': { + verifyAcceptableDsaKeyUse(algorithm.name, 'public', usagesSet); + keyObject = createPublicKey({ + key: keyData, + format: 'der', + type: 'spki' + }); + break; + } + case 'pkcs8': { + verifyAcceptableDsaKeyUse(algorithm.name, 'private', usagesSet); + keyObject = createPrivateKey({ + key: keyData, + format: 'der', + type: 'pkcs8' + }); + break; + } + case 'jwk': { + if (keyData == null || typeof keyData !== 'object') + throw lazyDOMException('Invalid JWK keyData', 'DataError'); + + verifyAcceptableDsaKeyUse( + algorithm.name, + keyData.x !== undefined ? 'private' : 'public', + usagesSet); + + if (keyData.kty !== 'DSA') + throw lazyDOMException('Invalid key type', 'DataError'); + + if (usagesSet.size > 0 && + keyData.use !== undefined && + keyData.use !== 'sig') { + throw lazyDOMException('Invalid use type', 'DataError'); + } + + validateKeyOps(keyData.key_ops, usagesSet); + + if (keyData.ext !== undefined && + keyData.ext === false && + extractable === true) { + throw lazyDOMException('JWK is not extractable', 'DataError'); + } + + if (keyData.alg !== undefined) { + if (typeof keyData.alg !== 'string') + throw lazyDOMException('Invalid alg', 'DataError'); + const hash = + normalizeHashName(keyData.alg, normalizeHashName.kContextWebCrypto); + if (hash !== algorithm.hash.name) + throw lazyDOMException('Hash mismatch', 'DataError'); + } + + const handle = new KeyObjectHandle(); + const type = handle.initJwk(keyData); + if (type === undefined) + throw lazyDOMException('Invalid JWK keyData', 'DataError'); + + keyObject = type === kKeyTypePrivate ? + new PrivateKeyObject(handle) : + new PublicKeyObject(handle); + + break; + } + default: + throw lazyDOMException( + `Unable to import DSA key with format ${format}`, + 'NotSupportedError'); + } + + const { + modulusLength, + divisorLength, + } = keyObject[kHandle].keyDetail({}); + + return new InternalCryptoKey(keyObject, { + name: algorithm.name, + modulusLength, + divisorLength, + hash: algorithm.hash + }, keyUsages, extractable); +} + +function dsaSignVerify(key, data, algorithm, signature) { + const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; + const type = mode === kSignJobModeSign ? 'private' : 'public'; + + if (key.type !== type) + throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + + return jobPromise(new SignJob( + kCryptoJobAsync, + signature === undefined ? kSignJobModeSign : kSignJobModeVerify, + key[kKeyObject][kHandle], + data, + normalizeHashName(key.algorithm.hash.name), + undefined, // Salt-length is not used in DSA + undefined, // Padding is not used in DSA + signature)); +} + +module.exports = { + dsaExportKey, + dsaGenerateKey, + dsaImportKey, + dsaSignVerify, +}; diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js new file mode 100644 index 00000000000000..c72eb4007bd49f --- /dev/null +++ b/lib/internal/crypto/ec.js @@ -0,0 +1,304 @@ +'use strict'; + +const { + ObjectKeys, + Promise, + SafeSet, +} = primordials; + +const { + ECKeyExportJob, + KeyObjectHandle, + SignJob, + kCryptoJobAsync, + kKeyTypePrivate, + kSignJobModeSign, + kSignJobModeVerify, +} = internalBinding('crypto'); + +const { + validateOneOf, + validateString, +} = require('internal/validators'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_MISSING_OPTION, + } +} = require('internal/errors'); + +const { + getArrayBufferOrView, + getUsagesUnion, + hasAnyNotIn, + jobPromise, + lazyDOMException, + normalizeHashName, + validateKeyOps, + kHandle, + kKeyObject, + kNamedCurveAliases, +} = require('internal/crypto/util'); + +const { + generateKeyPair, +} = require('internal/crypto/keygen'); + +const { + InternalCryptoKey, + PrivateKeyObject, + PublicKeyObject, + createPrivateKey, + createPublicKey, + isKeyObject, +} = require('internal/crypto/keys'); + +function verifyAcceptableEcKeyUse(name, type, usages) { + let checkSet; + switch (name) { + case 'ECDH': + checkSet = ['deriveKey', 'deriveBits']; + break; + case 'ECDSA': + switch (type) { + case 'private': + checkSet = ['sign']; + break; + case 'public': + checkSet = ['verify']; + break; + } + } + if (hasAnyNotIn(usages, ...checkSet)) { + throw lazyDOMException( + `Unsupported key usage for a ${name} key`, + 'SyntaxError'); + } +} + +function createECPublicKeyRaw(namedCurve, keyData) { + const handle = new KeyObjectHandle(); + keyData = getArrayBufferOrView(keyData, 'keyData'); + if (handle.initECRaw(kNamedCurveAliases[namedCurve], keyData)) + return new PublicKeyObject(handle); +} + +async function ecGenerateKey(algorithm, extractable, keyUsages) { + const { name, namedCurve } = algorithm; + validateString(namedCurve, 'algorithm.namedCurve'); + validateOneOf( + namedCurve, + 'algorithm.namedCurve', + ObjectKeys(kNamedCurveAliases)); + + const usageSet = new SafeSet(keyUsages); + switch (name) { + case 'ECDSA': + if (hasAnyNotIn(usageSet, 'sign', 'verify')) { + throw lazyDOMException( + 'Unsupported key usage for an ECDSA key', + 'SyntaxError'); + } + break; + case 'ECDH': + if (hasAnyNotIn(usageSet, 'deriveKey', 'deriveBits')) { + throw lazyDOMException( + 'Unsupported key usage for an ECDH key', + 'SyntaxError'); + } + } + return new Promise((resolve, reject) => { + generateKeyPair('ec', { namedCurve }, (err, pubKey, privKey) => { + if (err) { + return reject(lazyDOMException( + 'The operation failed for an operation-specific reason', + 'OperationError')); + } + + const algorithm = { name, namedCurve }; + + let publicUsages; + let privateUsages; + switch (name) { + case 'ECDSA': + publicUsages = getUsagesUnion(usageSet, 'verify'); + privateUsages = getUsagesUnion(usageSet, 'sign'); + break; + case 'ECDH': + publicUsages = []; + privateUsages = getUsagesUnion(usageSet, 'deriveKey', 'deriveBits'); + break; + } + + const publicKey = + new InternalCryptoKey( + pubKey, + algorithm, + publicUsages, + true); + + const privateKey = + new InternalCryptoKey( + privKey, + algorithm, + privateUsages, + extractable); + + resolve({ publicKey, privateKey }); + }); + }); +} + +function ecExportKey(key, format) { + return jobPromise(new ECKeyExportJob( + kCryptoJobAsync, + format, + key[kKeyObject][kHandle])); +} + +async function ecImportKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + + const { name, namedCurve } = algorithm; + validateString(namedCurve, 'algorithm.namedCurve'); + validateOneOf( + namedCurve, + 'algorithm.namedCurve', + ObjectKeys(kNamedCurveAliases)); + + let keyObject; + const usagesSet = new SafeSet(keyUsages); + switch (format) { + case 'node.keyObject': { + if (!isKeyObject(keyData)) + throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData); + if (keyData.type === 'secret') + throw lazyDOMException('Invalid key type', 'InvalidAccessException'); + verifyAcceptableEcKeyUse(name, keyData.type, usagesSet); + keyObject = keyData; + break; + } + case 'spki': { + verifyAcceptableEcKeyUse(name, 'public', usagesSet); + keyObject = createPublicKey({ + key: keyData, + format: 'der', + type: 'spki' + }); + break; + } + case 'pkcs8': { + verifyAcceptableEcKeyUse(name, 'private', usagesSet); + keyObject = createPrivateKey({ + key: keyData, + format: 'der', + type: 'pkcs8' + }); + break; + } + case 'jwk': { + let curve; + if (keyData == null || typeof keyData !== 'object') + throw lazyDOMException('Invalid JWK keyData', 'DataError'); + + if (keyData.kty !== 'EC') + throw lazyDOMException('Invalid key type', 'DataError'); + + if (keyData.d !== undefined) { + verifyAcceptableEcKeyUse(name, 'private', usagesSet); + } else { + verifyAcceptableEcKeyUse(name, 'public', usagesSet); + } + + if (usagesSet.size > 0 && keyData.use !== undefined) { + if (algorithm.name === 'ECDSA' && keyData.use !== 'sig') + throw lazyDOMException('Invalid use type', 'DataError'); + if (algorithm.name === 'ECDH' && keyData.use !== 'enc') + throw lazyDOMException('Invalid use type', 'DataError'); + } + + validateKeyOps(keyData.key_ops, usagesSet); + + if (keyData.ext !== undefined && + keyData.ext === false && + extractable === true) { + throw lazyDOMException('JWK is not extractable', 'DataError'); + } + + if (keyData.alg !== undefined) { + if (typeof keyData.alg !== 'string') + throw lazyDOMException('Invalid alg', 'DataError'); + switch (keyData.alg) { + case 'ES256': curve = 'P-256'; break; + case 'ES384': curve = 'P-384'; break; + case 'ES512': curve = 'P-521'; break; + } + if (curve !== namedCurve) + throw lazyDOMException('Named curve mismatch', 'DataError'); + } + + const handle = new KeyObjectHandle(); + const type = handle.initJwk(keyData, namedCurve); + if (type === undefined) + throw lazyDOMException('Invalid JWK keyData', 'DataError'); + + keyObject = type === kKeyTypePrivate ? + new PrivateKeyObject(handle) : + new PublicKeyObject(handle); + + break; + } + case 'raw': { + verifyAcceptableEcKeyUse(name, 'public', usagesSet); + keyObject = createECPublicKeyRaw(namedCurve, keyData); + if (keyObject === undefined) + throw lazyDOMException('Unable to import EC key', 'OperationError'); + break; + } + } + + const { + namedCurve: checkNamedCurve + } = keyObject[kHandle].keyDetail({}); + if (kNamedCurveAliases[namedCurve] !== checkNamedCurve) + throw lazyDOMException('Named curve mismatch', 'DataError'); + + return new InternalCryptoKey( + keyObject, + { name, namedCurve }, + keyUsages, + extractable); +} + +function ecdsaSignVerify(key, data, { hash }, signature) { + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + + const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; + const type = mode === kSignJobModeSign ? 'private' : 'public'; + + if (key.type !== type) + throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + + return jobPromise(new SignJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + data, + normalizeHashName(hash.name), + undefined, // Salt length, not used with ECDSA + undefined, // PSS Padding, not used with ECDSA + signature)); +} + +module.exports = { + ecExportKey, + ecImportKey, + ecGenerateKey, + ecdsaSignVerify, +}; diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 1cf0188da2f35e..4647e98c671ab8 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -7,30 +7,50 @@ const { const { Hash: _Hash, - Hmac: _Hmac + HashJob, + Hmac: _Hmac, + kCryptoJobAsync, } = internalBinding('crypto'); const { + getArrayBufferOrView, getDefaultEncoding, + getStringOption, + jobPromise, + normalizeAlgorithm, + normalizeHashName, + validateMaxBufferLength, kHandle, - toBuf } = require('internal/crypto/util'); const { - prepareSecretKey + prepareSecretKey, } = require('internal/crypto/keys'); -const { Buffer } = require('buffer'); +const { + Buffer, +} = require('buffer'); + +const { + codes: { + ERR_CRYPTO_HASH_FINALIZED, + ERR_CRYPTO_HASH_UPDATE_FAILED, + ERR_INVALID_ARG_TYPE, + } +} = require('internal/errors'); const { - ERR_CRYPTO_HASH_FINALIZED, - ERR_CRYPTO_HASH_UPDATE_FAILED, - ERR_INVALID_ARG_TYPE -} = require('internal/errors').codes; -const { validateEncoding, validateString, validateUint32 } = - require('internal/validators'); -const { isArrayBufferView } = require('internal/util/types'); + validateEncoding, + validateString, + validateUint32, +} = require('internal/validators'); + +const { + isArrayBufferView, +} = require('internal/util/types'); + const LazyTransform = require('internal/streams/lazy_transform'); + const kState = Symbol('kState'); const kFinalized = Symbol('kFinalized'); @@ -103,14 +123,14 @@ Hash.prototype.digest = function digest(outputEncoding) { return ret; }; - function Hmac(hmac, key, options) { if (!(this instanceof Hmac)) return new Hmac(hmac, key, options); validateString(hmac, 'hmac'); - key = prepareSecretKey(key); + const encoding = getStringOption(options, 'encoding'); + key = prepareSecretKey(key, encoding); this[kHandle] = new _Hmac(); - this[kHandle].init(hmac, toBuf(key)); + this[kHandle].init(hmac, key); this[kState] = { [kFinalized]: false }; @@ -140,7 +160,25 @@ Hmac.prototype.digest = function digest(outputEncoding) { Hmac.prototype._flush = Hash.prototype._flush; Hmac.prototype._transform = Hash.prototype._transform; +// Implementation for WebCrypto subtle.digest() + +async function asyncDigest(algorithm, data) { + algorithm = normalizeAlgorithm(algorithm); + data = getArrayBufferOrView(data, 'data'); + validateMaxBufferLength(data, 'data'); + + if (algorithm.length !== undefined) + validateUint32(algorithm.length, 'algorithm.length'); + + return jobPromise(new HashJob( + kCryptoJobAsync, + normalizeHashName(algorithm.name), + data, + algorithm.length)); +} + module.exports = { Hash, - Hmac + Hmac, + asyncDigest, }; diff --git a/lib/internal/crypto/hashnames.js b/lib/internal/crypto/hashnames.js new file mode 100644 index 00000000000000..be5064532c86c9 --- /dev/null +++ b/lib/internal/crypto/hashnames.js @@ -0,0 +1,90 @@ +'use strict'; + +const { + ObjectKeys, + StringPrototypeToLowerCase, +} = primordials; + +const kHashContextNode = 1; +const kHashContextWebCrypto = 2; +const kHashContextJwkRsa = 3; +const kHashContextJwkRsaPss = 4; +const kHashContextJwkRsaOaep = 5; +const kHashContextJwkHmac = 6; +const kHashContextJwkDsa = 7; + +// WebCrypto and JWK use a bunch of different names for the +// standard set of SHA-* digest algorithms... which is ... fun. +// Here we provide a utility for mapping between them in order +// make it easier in the code. + +const kHashNames = { + sha1: { + [kHashContextNode]: 'sha1', + [kHashContextWebCrypto]: 'SHA-1', + [kHashContextJwkRsa]: 'RS1', + [kHashContextJwkRsaPss]: 'PS1', + [kHashContextJwkRsaOaep]: 'RSA-OAEP', + [kHashContextJwkHmac]: 'HS1', + [kHashContextJwkDsa]: 'NODE-DSA-SHA-1', + }, + sha256: { + [kHashContextNode]: 'sha256', + [kHashContextWebCrypto]: 'SHA-256', + [kHashContextJwkRsa]: 'RS256', + [kHashContextJwkRsaPss]: 'PS256', + [kHashContextJwkRsaOaep]: 'RSA-OAEP-256', + [kHashContextJwkHmac]: 'HS256', + [kHashContextJwkDsa]: 'NODE-DSA-SHA-256', + }, + sha384: { + [kHashContextNode]: 'sha384', + [kHashContextWebCrypto]: 'SHA-384', + [kHashContextJwkRsa]: 'RS384', + [kHashContextJwkRsaPss]: 'PS384', + [kHashContextJwkRsaOaep]: 'RSA-OAEP-384', + [kHashContextJwkHmac]: 'HS384', + [kHashContextJwkDsa]: 'NODE-DSA-SHA-384', + }, + sha512: { + [kHashContextNode]: 'sha512', + [kHashContextWebCrypto]: 'SHA-512', + [kHashContextJwkRsa]: 'RS512', + [kHashContextJwkRsaPss]: 'PS512', + [kHashContextJwkRsaOaep]: 'RSA-OAEP-512', + [kHashContextJwkHmac]: 'HS512', + [kHashContextJwkDsa]: 'NODE-DSA-SHA-512', + } +}; + +{ + // Index the aliases + const keys = ObjectKeys(kHashNames); + for (let n = 0; n < keys.length; n++) { + const contexts = ObjectKeys(kHashNames[keys[n]]); + for (let i = 0; i < contexts.length; i++) { + const alias = + StringPrototypeToLowerCase(kHashNames[keys[n]][contexts[i]]); + if (kHashNames[alias] === undefined) + kHashNames[alias] = kHashNames[keys[n]]; + } + } +} + +function normalizeHashName(name, context = kHashContextNode) { + if (typeof name !== 'string') + return name; + name = StringPrototypeToLowerCase(name); + const alias = kHashNames[name] && kHashNames[name][context]; + return alias || name; +} + +normalizeHashName.kContextNode = kHashContextNode; +normalizeHashName.kContextWebCrypto = kHashContextWebCrypto; +normalizeHashName.kContextJwkRsa = kHashContextJwkRsa; +normalizeHashName.kContextJwkRsaPss = kHashContextJwkRsaPss; +normalizeHashName.kContextJwkRsaOaep = kHashContextJwkRsaOaep; +normalizeHashName.kContextJwkHmac = kHashContextJwkHmac; +normalizeHashName.kContextJwkDsa = kHashContextJwkDsa; + +module.exports = normalizeHashName; diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js new file mode 100644 index 00000000000000..1b013e5ff1591c --- /dev/null +++ b/lib/internal/crypto/hkdf.js @@ -0,0 +1,183 @@ +'use strict'; + +const { + FunctionPrototypeCall, + Promise, + Uint8Array, +} = primordials; + +const { + HKDFJob, + kCryptoJobAsync, + kCryptoJobSync, +} = internalBinding('crypto'); + +const { + validateInteger, + validateString, + validateUint32, +} = require('internal/validators'); + +const { kMaxLength } = require('buffer'); + +const { + getArrayBufferOrView, + lazyDOMException, + normalizeHashName, + toBuf, + validateByteSource, + kKeyObject, +} = require('internal/crypto/util'); + +const { + createSecretKey, + isKeyObject, +} = require('internal/crypto/keys'); + +const { + isAnyArrayBuffer, + isArrayBufferView, +} = require('internal/util/types'); + +const { + codes: { + ERR_INVALID_CALLBACK, + ERR_INVALID_ARG_TYPE, + ERR_OUT_OF_RANGE, + ERR_MISSING_OPTION, + }, + hideStackFrames, +} = require('internal/errors'); + +const validateParameters = hideStackFrames((hash, key, salt, info, length) => { + key = prepareKey(key); + salt = toBuf(salt); + info = toBuf(info); + + validateString(hash, 'digest'); + validateByteSource(salt, 'salt'); + validateByteSource(info, 'info'); + + validateInteger(length, 'length', 0, kMaxLength); + + if (info.byteLength > 1024) { + throw ERR_OUT_OF_RANGE( + 'info', + 'must not contain more than 1024 bytes', + info.byteLength); + } + + return { + hash, + key, + salt, + info, + length, + }; +}); + +function prepareKey(key) { + if (isKeyObject(key)) + return key; + + // TODO(@jasnell): createSecretKey should allow using an ArrayBuffer + if (isAnyArrayBuffer(key)) + return createSecretKey(new Uint8Array(key)); + + key = toBuf(key); + + if (!isArrayBufferView(key)) { + throw new ERR_INVALID_ARG_TYPE( + 'key', + [ + 'string', + 'SecretKeyObject', + 'ArrayBuffer', + 'TypedArray', + 'DataView', + 'Buffer' + ], + key); + } + + return createSecretKey(key); +} + +function hkdf(hash, key, salt, info, length, callback) { + ({ + hash, + key, + salt, + info, + length, + } = validateParameters(hash, key, salt, info, length)); + + if (typeof callback !== 'function') + throw new ERR_INVALID_CALLBACK(callback); + + const job = new HKDFJob(kCryptoJobAsync, hash, key, salt, info, length); + + job.ondone = (error, bits) => { + if (error) return FunctionPrototypeCall(callback, job, error); + FunctionPrototypeCall(callback, job, null, bits); + }; + + job.run(); +} + +function hkdfSync(hash, key, salt, info, length) { + ({ + hash, + key, + salt, + info, + length, + } = validateParameters(hash, key, salt, info, length)); + + const job = new HKDFJob(kCryptoJobSync, hash, key, salt, info, length); + const [err, bits] = job.run(); + if (err !== undefined) + throw err; + + return bits; +} + +async function hkdfDeriveBits(algorithm, baseKey, length) { + validateUint32(length, 'length'); + const { hash } = algorithm; + const salt = getArrayBufferOrView(algorithm.salt, 'algorithm.salt'); + const info = getArrayBufferOrView(algorithm.info, 'algorithm.info'); + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + + let byteLength = 512 / 8; + if (length !== undefined) { + if (length === 0) + throw lazyDOMException('length cannot be zero', 'OperationError'); + if (length % 8) { + throw lazyDOMException( + 'length must be a multiple of 8', + 'OperationError'); + } + byteLength = length / 8; + } + + return new Promise((resolve, reject) => { + hkdf( + normalizeHashName(hash.name), + baseKey[kKeyObject], + salt, + info, + byteLength, + (err, bits) => { + if (err) return reject(err); + resolve(bits); + }); + }); +} + +module.exports = { + hkdf, + hkdfSync, + hkdfDeriveBits, +}; diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index be0f857d845177..8260f59588c714 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -1,43 +1,59 @@ 'use strict'; const { + FunctionPrototypeCall, ObjectDefineProperty, } = primordials; -const { AsyncWrap, Providers } = internalBinding('async_wrap'); const { - generateKeyPairRSA, - generateKeyPairRSAPSS, - generateKeyPairDSA, - generateKeyPairEC, - generateKeyPairNid, - generateKeyPairDH, + DhKeyPairGenJob, + DsaKeyPairGenJob, + EcKeyPairGenJob, + NidKeyPairGenJob, + RsaKeyPairGenJob, + SecretKeyGenJob, + kCryptoJobAsync, + kCryptoJobSync, + kKeyVariantRSA_PSS, + kKeyVariantRSA_SSA_PKCS1_V1_5, EVP_PKEY_ED25519, EVP_PKEY_ED448, EVP_PKEY_X25519, EVP_PKEY_X448, OPENSSL_EC_NAMED_CURVE, - OPENSSL_EC_EXPLICIT_CURVE + OPENSSL_EC_EXPLICIT_CURVE, } = internalBinding('crypto'); + const { + PublicKeyObject, + PrivateKeyObject, + SecretKeyObject, parsePublicKeyEncoding, parsePrivateKeyEncoding, - - PublicKeyObject, - PrivateKeyObject } = require('internal/crypto/keys'); + +const { + kAesKeyLengths, +} = require('internal/crypto/util'); + const { customPromisifyArgs } = require('internal/util'); + const { isUint32, validateString, + validateInteger, validateObject, + validateOneOf, } = require('internal/validators'); + const { - ERR_INCOMPATIBLE_OPTION_PAIR, - ERR_INVALID_ARG_VALUE, - ERR_INVALID_CALLBACK, - ERR_MISSING_OPTION -} = require('internal/errors').codes; + codes: { + ERR_INCOMPATIBLE_OPTION_PAIR, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_CALLBACK, + ERR_MISSING_OPTION, + } +} = require('internal/errors'); const { isArrayBufferView } = require('internal/util/types'); @@ -52,22 +68,21 @@ function generateKeyPair(type, options, callback) { callback = options; options = undefined; } - - const impl = check(type, options); - if (typeof callback !== 'function') throw new ERR_INVALID_CALLBACK(callback); - const wrap = new AsyncWrap(Providers.KEYPAIRGENREQUEST); - wrap.ondone = (ex, pubkey, privkey) => { - if (ex) return callback.call(wrap, ex); + const job = check(kCryptoJobAsync, type, options); + + job.ondone = (error, result) => { + if (error) return FunctionPrototypeCall(callback, job, error); // If no encoding was chosen, return key objects instead. + let [pubkey, privkey] = result; pubkey = wrapKey(pubkey, PublicKeyObject); privkey = wrapKey(privkey, PrivateKeyObject); - callback.call(wrap, null, pubkey, privkey); + FunctionPrototypeCall(callback, job, null, pubkey, privkey); }; - handleError(impl(wrap)); + job.run(); } ObjectDefineProperty(generateKeyPair, customPromisifyArgs, { @@ -76,15 +91,14 @@ ObjectDefineProperty(generateKeyPair, customPromisifyArgs, { }); function generateKeyPairSync(type, options) { - const impl = check(type, options); - return handleError(impl()); + return handleError(check(kCryptoJobSync, type, options).run()); } function handleError(ret) { if (ret === undefined) return; // async - const [err, publicKey, privateKey] = ret; + const [err, [publicKey, privateKey]] = ret; if (err !== undefined) throw err; @@ -95,7 +109,7 @@ function handleError(ret) { }; } -function parseKeyEncoding(keyType, options) { +function parseKeyEncoding(keyType, options = {}) { const { publicKeyEncoding, privateKeyEncoding } = options; let publicFormat, publicType; @@ -128,194 +142,238 @@ function parseKeyEncoding(keyType, options) { privateKeyEncoding); } - return { - cipher, passphrase, publicType, publicFormat, privateType, privateFormat - }; + return [ + publicFormat, + publicType, + privateFormat, + privateType, + cipher, + passphrase + ]; } -function check(type, options, callback) { +function check(mode, type, options) { validateString(type, 'type'); - // These will be set after parsing the type and type-specific options to make - // the order a bit more intuitive. - let cipher, passphrase, publicType, publicFormat, privateType, privateFormat; + const encoding = parseKeyEncoding(type, options); if (options !== undefined) validateObject(options, 'options'); - let impl; switch (type) { case 'rsa': case 'rsa-pss': - { - validateObject(options, 'options'); - const { modulusLength } = options; - if (!isUint32(modulusLength)) - throw new ERR_INVALID_ARG_VALUE('options.modulusLength', - modulusLength); - - let { publicExponent } = options; - if (publicExponent == null) { - publicExponent = 0x10001; - } else if (!isUint32(publicExponent)) { - throw new ERR_INVALID_ARG_VALUE('options.publicExponent', - publicExponent); - } - - if (type === 'rsa') { - impl = (wrap) => generateKeyPairRSA(modulusLength, publicExponent, - publicFormat, publicType, - privateFormat, privateType, - cipher, passphrase, wrap); - break; - } - - const { hash, mgf1Hash, saltLength } = options; - if (hash !== undefined && typeof hash !== 'string') - throw new ERR_INVALID_ARG_VALUE('options.hash', hash); - if (mgf1Hash !== undefined && typeof mgf1Hash !== 'string') - throw new ERR_INVALID_ARG_VALUE('options.mgf1Hash', mgf1Hash); - if (saltLength !== undefined && !isUint32(saltLength)) - throw new ERR_INVALID_ARG_VALUE('options.saltLength', saltLength); - - impl = (wrap) => generateKeyPairRSAPSS(modulusLength, publicExponent, - hash, mgf1Hash, saltLength, - publicFormat, publicType, - privateFormat, privateType, - cipher, passphrase, wrap); + { + validateObject(options, 'options'); + const { modulusLength } = options; + if (!isUint32(modulusLength)) + throw new ERR_INVALID_ARG_VALUE('options.modulusLength', modulusLength); + + let { publicExponent } = options; + if (publicExponent == null) { + publicExponent = 0x10001; + } else if (!isUint32(publicExponent)) { + throw new ERR_INVALID_ARG_VALUE( + 'options.publicExponent', + publicExponent); } - break; + + if (type === 'rsa') { + return new RsaKeyPairGenJob( + mode, + kKeyVariantRSA_SSA_PKCS1_V1_5, // Used also for RSA-OAEP + modulusLength, + publicExponent, + ...encoding); + } + + const { hash, mgf1Hash, saltLength } = options; + if (hash !== undefined && typeof hash !== 'string') + throw new ERR_INVALID_ARG_VALUE('options.hash', hash); + if (mgf1Hash !== undefined && typeof mgf1Hash !== 'string') + throw new ERR_INVALID_ARG_VALUE('options.mgf1Hash', mgf1Hash); + if (saltLength !== undefined && !isUint32(saltLength)) + throw new ERR_INVALID_ARG_VALUE('options.saltLength', saltLength); + + return new RsaKeyPairGenJob( + mode, + kKeyVariantRSA_PSS, + modulusLength, + publicExponent, + hash, + mgf1Hash, + saltLength, + ...encoding); + } case 'dsa': - { - validateObject(options, 'options'); - const { modulusLength } = options; - if (!isUint32(modulusLength)) - throw new ERR_INVALID_ARG_VALUE('options.modulusLength', - modulusLength); - - let { divisorLength } = options; - if (divisorLength == null) { - divisorLength = -1; - } else if (!isUint32(divisorLength)) { - throw new ERR_INVALID_ARG_VALUE('options.divisorLength', - divisorLength); - } - - impl = (wrap) => generateKeyPairDSA(modulusLength, divisorLength, - publicFormat, publicType, - privateFormat, privateType, - cipher, passphrase, wrap); + { + validateObject(options, 'options'); + const { modulusLength } = options; + if (!isUint32(modulusLength)) + throw new ERR_INVALID_ARG_VALUE('options.modulusLength', modulusLength); + + let { divisorLength } = options; + if (divisorLength == null) { + divisorLength = -1; + } else if (!isUint32(divisorLength)) { + throw new ERR_INVALID_ARG_VALUE('options.divisorLength', divisorLength); } - break; + + return new DsaKeyPairGenJob( + mode, + modulusLength, + divisorLength, + ...encoding); + } case 'ec': - { - validateObject(options, 'options'); - const { namedCurve } = options; - if (typeof namedCurve !== 'string') - throw new ERR_INVALID_ARG_VALUE('options.namedCurve', namedCurve); - let { paramEncoding } = options; - if (paramEncoding == null || paramEncoding === 'named') - paramEncoding = OPENSSL_EC_NAMED_CURVE; - else if (paramEncoding === 'explicit') - paramEncoding = OPENSSL_EC_EXPLICIT_CURVE; - else - throw new ERR_INVALID_ARG_VALUE('options.paramEncoding', - paramEncoding); - - impl = (wrap) => generateKeyPairEC(namedCurve, paramEncoding, - publicFormat, publicType, - privateFormat, privateType, - cipher, passphrase, wrap); - } - break; + { + validateObject(options, 'options'); + const { namedCurve } = options; + if (typeof namedCurve !== 'string') + throw new ERR_INVALID_ARG_VALUE('options.namedCurve', namedCurve); + let { paramEncoding } = options; + if (paramEncoding == null || paramEncoding === 'named') + paramEncoding = OPENSSL_EC_NAMED_CURVE; + else if (paramEncoding === 'explicit') + paramEncoding = OPENSSL_EC_EXPLICIT_CURVE; + else + throw new ERR_INVALID_ARG_VALUE('options.paramEncoding', paramEncoding); + + return new EcKeyPairGenJob( + mode, + namedCurve, + paramEncoding, + ...encoding); + } case 'ed25519': case 'ed448': case 'x25519': case 'x448': - { - let id; - switch (type) { - case 'ed25519': - id = EVP_PKEY_ED25519; - break; - case 'ed448': - id = EVP_PKEY_ED448; - break; - case 'x25519': - id = EVP_PKEY_X25519; - break; - case 'x448': - id = EVP_PKEY_X448; - break; - } - impl = (wrap) => generateKeyPairNid(id, - publicFormat, publicType, - privateFormat, privateType, - cipher, passphrase, wrap); + { + let id; + switch (type) { + case 'ed25519': + id = EVP_PKEY_ED25519; + break; + case 'ed448': + id = EVP_PKEY_ED448; + break; + case 'x25519': + id = EVP_PKEY_X25519; + break; + case 'x448': + id = EVP_PKEY_X448; + break; } - break; + return new NidKeyPairGenJob(mode, id, ...encoding); + } case 'dh': - { - validateObject(options, 'options'); - const { group, primeLength, prime, generator } = options; - let args; - if (group != null) { - if (prime != null) - throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'prime'); - if (primeLength != null) - throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'primeLength'); - if (generator != null) - throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'generator'); - if (typeof group !== 'string') - throw new ERR_INVALID_ARG_VALUE('options.group', group); - args = [group]; - } else { - if (prime != null) { - if (primeLength != null) - throw new ERR_INCOMPATIBLE_OPTION_PAIR('prime', 'primeLength'); - if (!isArrayBufferView(prime)) - throw new ERR_INVALID_ARG_VALUE('options.prime', prime); - } else if (primeLength != null) { - if (!isUint32(primeLength)) - throw new ERR_INVALID_ARG_VALUE('options.primeLength', - primeLength); - } else { - throw new ERR_MISSING_OPTION( - 'At least one of the group, prime, or primeLength options'); - } - - if (generator != null) { - if (!isUint32(generator)) - throw new ERR_INVALID_ARG_VALUE('options.generator', generator); - } - - args = [prime != null ? prime : primeLength, - generator == null ? 2 : generator]; - } - - impl = (wrap) => generateKeyPairDH(...args, - publicFormat, publicType, - privateFormat, privateType, - cipher, passphrase, wrap); + { + validateObject(options, 'options'); + const { group, primeLength, prime, generator } = options; + if (group != null) { + if (prime != null) + throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'prime'); + if (primeLength != null) + throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'primeLength'); + if (generator != null) + throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'generator'); + if (typeof group !== 'string') + throw new ERR_INVALID_ARG_VALUE('options.group', group); + + return new DhKeyPairGenJob(mode, group, ...encoding); + } + + if (prime != null) { + if (primeLength != null) + throw new ERR_INCOMPATIBLE_OPTION_PAIR('prime', 'primeLength'); + if (!isArrayBufferView(prime)) + throw new ERR_INVALID_ARG_VALUE('options.prime', prime); + } else if (primeLength != null) { + if (!isUint32(primeLength)) + throw new ERR_INVALID_ARG_VALUE('options.primeLength', primeLength); + } else { + throw new ERR_MISSING_OPTION( + 'At least one of the group, prime, or primeLength options'); } + + if (generator != null) { + if (!isUint32(generator)) + throw new ERR_INVALID_ARG_VALUE('options.generator', generator); + } + return new DhKeyPairGenJob( + mode, + prime != null ? prime : primeLength, + generator == null ? 2 : generator, + ...encoding); + } + default: + // Fall through + } + throw new ERR_INVALID_ARG_VALUE('type', type, 'must be a supported key type'); +} + +// Symmetric Key Generation + +function generateKeyJob(mode, keyType, options) { + validateString(keyType, 'type'); + validateObject(options, 'options'); + const { length } = options; + switch (keyType) { + case 'hmac': + validateInteger(length, 'options.length', 1, 2 ** 31 - 1); + break; + case 'aes': + validateOneOf(length, 'options.length', kAesKeyLengths); break; default: - throw new ERR_INVALID_ARG_VALUE('type', type, - 'must be a supported key type'); + throw new ERR_INVALID_ARG_VALUE( + 'type', + keyType, + 'must be a supported key type'); } - if (options) { - ({ - cipher, - passphrase, - publicType, - publicFormat, - privateType, - privateFormat - } = parseKeyEncoding(type, options)); + return new SecretKeyGenJob(mode, length); +} + +function handleGenerateKeyError(ret) { + if (ret === undefined) + return; // async + + const [err, key] = ret; + if (err !== undefined) + throw err; + + return wrapKey(key, SecretKeyObject); +} + +function generateKey(type, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; } - return impl; + if (typeof callback !== 'function') + throw new ERR_INVALID_CALLBACK(callback); + + const job = generateKeyJob(kCryptoJobAsync, type, options); + + job.ondone = (error, key) => { + if (error) return FunctionPrototypeCall(callback, job, error); + FunctionPrototypeCall(callback, job, null, wrapKey(key, SecretKeyObject)); + }; + + handleGenerateKeyError(job.run()); +} + +function generateKeySync(type, options) { + return handleGenerateKeyError( + generateKeyJob(kCryptoJobSync, type, options).run()); } -module.exports = { generateKeyPair, generateKeyPairSync }; +module.exports = { + generateKeyPair, + generateKeyPairSync, + generateKey, + generateKeySync, +}; diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 2131233c55ac30..f54393a5e3b4bd 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -1,7 +1,9 @@ 'use strict'; const { + ArrayFrom, ObjectDefineProperty, + ObjectSetPrototypeOf, Symbol, } = primordials; @@ -16,20 +18,47 @@ const { kKeyEncodingPKCS1, kKeyEncodingPKCS8, kKeyEncodingSPKI, - kKeyEncodingSEC1 + kKeyEncodingSEC1, } = internalBinding('crypto'); + +const { + codes: { + ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, + ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_OUT_OF_RANGE, + ERR_OPERATION_FAILED, + } +} = require('internal/errors'); + const { - ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, - ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - ERR_OUT_OF_RANGE -} = require('internal/errors').codes; -const { kHandle } = require('internal/crypto/util'); + kHandle, + kKeyObject, + getArrayBufferOrView, +} = require('internal/crypto/util'); -const { isArrayBufferView } = require('internal/util/types'); +const { + isAnyArrayBuffer, + isArrayBufferView, +} = require('internal/util/types'); +const { + JSTransferable, + kClone, + kDeserialize, +} = require('internal/worker/js_transferable'); + +const { + customInspectSymbol: kInspect, +} = require('internal/util'); + +const { inspect } = require('internal/util/inspect'); + +const kAlgorithm = Symbol('kAlgorithm'); +const kExtractable = Symbol('kExtractable'); const kKeyType = Symbol('kKeyType'); +const kKeyUsages = Symbol('kKeyUsages'); // Key input contexts. const kConsumePublic = 0; @@ -76,6 +105,12 @@ const [ get type() { return this[kKeyType]; } + + static from(key) { + if (!isCryptoKey(key)) + throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); + return key[kKeyObject]; + } } class SecretKeyObject extends KeyObject { @@ -191,7 +226,9 @@ function parseKeyFormatAndType(enc, keyType, isPublic, objName) { } function isStringOrBuffer(val) { - return typeof val === 'string' || isArrayBufferView(val); + return typeof val === 'string' || + isArrayBufferView(val) || + isAnyArrayBuffer(val); } function parseKeyEncoding(enc, keyType, isPublic, objName) { @@ -205,9 +242,9 @@ function parseKeyEncoding(enc, keyType, isPublic, objName) { type } = parseKeyFormatAndType(enc, keyType, isPublic, objName); - let cipher, passphrase; + let cipher, passphrase, encoding; if (isPublic !== true) { - ({ cipher, passphrase } = enc); + ({ cipher, passphrase, encoding } = enc); if (!isInput) { if (cipher != null) { @@ -232,6 +269,9 @@ function parseKeyEncoding(enc, keyType, isPublic, objName) { } } + if (passphrase !== undefined) + passphrase = getArrayBufferOrView(passphrase, 'key.passphrase', encoding); + return { format, type, cipher, passphrase }; } @@ -253,7 +293,7 @@ function getKeyObjectHandle(key, ctx) { if (ctx === kCreatePrivate) { throw new ERR_INVALID_ARG_TYPE( 'key', - ['string', 'Buffer', 'TypedArray', 'DataView'], + ['string', 'ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], key ); } @@ -270,38 +310,52 @@ function getKeyObjectHandle(key, ctx) { return key[kHandle]; } +function getKeyTypes(allowKeyObject, bufferOnly = false) { + return [ + 'ArrayBuffer', + 'Buffer', + 'TypedArray', + 'DataView', + ...(!bufferOnly ? ['string'] : []), + ...(!bufferOnly && allowKeyObject ? ['KeyObject', 'CryptoKey'] : [])]; +} + function prepareAsymmetricKey(key, ctx) { if (isKeyObject(key)) { // Best case: A key object, as simple as that. return { data: getKeyObjectHandle(key, ctx) }; - } else if (typeof key === 'string' || isArrayBufferView(key)) { + } else if (isCryptoKey(key)) { + return { data: getKeyObjectHandle(key[kKeyObject], ctx) }; + } else if (isStringOrBuffer(key)) { // Expect PEM by default, mostly for backward compatibility. - return { format: kKeyFormatPEM, data: key }; + return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, 'key') }; } else if (typeof key === 'object') { - const data = key.key; + const { key: data, encoding } = 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, ctx) }; + else if (isCryptoKey(data)) + return { data: getKeyObjectHandle(data[kKeyObject], ctx) }; // Either PEM or DER using PKCS#1 or SPKI. if (!isStringOrBuffer(data)) { throw new ERR_INVALID_ARG_TYPE( 'key.key', - ['string', 'Buffer', 'TypedArray', 'DataView', - ...(ctx !== kCreatePrivate ? ['KeyObject'] : [])], + getKeyTypes(ctx !== kCreatePrivate), data); } const isPublic = (ctx === kConsumePrivate || ctx === kCreatePrivate) ? false : undefined; - return { data, ...parseKeyEncoding(key, undefined, isPublic) }; + return { + data: getArrayBufferOrView(data, 'key', encoding), + ...parseKeyEncoding(key, undefined, isPublic) + }; } throw new ERR_INVALID_ARG_TYPE( 'key', - ['string', 'Buffer', 'TypedArray', 'DataView', - ...(ctx !== kCreatePrivate ? ['KeyObject'] : [])], - key - ); + getKeyTypes(ctx !== kCreatePrivate), + key); } function preparePrivateKey(key) { @@ -312,24 +366,31 @@ function preparePublicOrPrivateKey(key) { return prepareAsymmetricKey(key, kConsumePublic); } -function prepareSecretKey(key, bufferOnly = false) { - if (!isArrayBufferView(key) && (bufferOnly || typeof key !== 'string')) { - if (isKeyObject(key) && !bufferOnly) { +function prepareSecretKey(key, encoding, bufferOnly = false) { + if (!bufferOnly) { + if (isKeyObject(key)) { if (key.type !== 'secret') throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret'); return key[kHandle]; + } else if (isCryptoKey(key)) { + if (key.type !== 'secret') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret'); + return key[kKeyObject][kHandle]; } + } + if (typeof key !== 'string' && + !isArrayBufferView(key) && + !isAnyArrayBuffer(key)) { throw new ERR_INVALID_ARG_TYPE( 'key', - ['Buffer', 'TypedArray', 'DataView', - ...(bufferOnly ? [] : ['string', 'KeyObject'])], + getKeyTypes(!bufferOnly, bufferOnly), key); } - return key; + return getArrayBufferOrView(key, 'key', encoding); } -function createSecretKey(key) { - key = prepareSecretKey(key, true); +function createSecretKey(key, encoding) { + key = prepareSecretKey(key, encoding, true); if (key.byteLength === 0) throw new ERR_OUT_OF_RANGE('key.byteLength', '> 0', key.byteLength); const handle = new KeyObjectHandle(); @@ -356,21 +417,123 @@ function isKeyObject(key) { return key instanceof KeyObject; } +// Our implementation of CryptoKey is a simple wrapper around a KeyObject +// that adapts it to the standard interface. This implementation also +// extends the JSTransferable class, allowing the CryptoKey to be cloned +// to Workers. +// TODO(@jasnell): Embedder environments like electron may have issues +// here similar to other things like URL. A chromium provided CryptoKey +// will not be recognized as a Node.js CryptoKey, and vice versa. It +// would be fantastic if we could find a way of making those interop. +class CryptoKey extends JSTransferable { + constructor() { + throw new ERR_OPERATION_FAILED('Illegal constructor'); + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1 + }; + + return `CryptoKey ${inspect({ + type: this.type, + extractable: this.extractable, + algorithm: this.algorithm, + usages: this.usages + }, opts)}`; + } + + get type() { + return this[kKeyObject].type; + } + + get extractable() { + return this[kExtractable]; + } + + get algorithm() { + return this[kAlgorithm]; + } + + get usages() { + return ArrayFrom(this[kKeyUsages]); + } + + [kClone]() { + const keyObject = this[kKeyObject]; + const algorithm = this.algorithm; + const extractable = this.extractable; + const usages = this.usages; + + return { + data: { + keyObject, + algorithm, + usages, + extractable, + }, + deserializeInfo: 'internal/crypto/keys:InternalCryptoKey' + }; + } + + [kDeserialize]({ keyObject, algorithm, usages, extractable }) { + this[kKeyObject] = keyObject; + this[kAlgorithm] = algorithm; + this[kKeyUsages] = usages; + this[kExtractable] = extractable; + } +} + +// All internal code must use new InternalCryptoKey to create +// CryptoKey instances. The CryptoKey class is exposed to end +// user code but is not permitted to be constructed directly. +class InternalCryptoKey extends JSTransferable { + constructor( + keyObject, + algorithm, + keyUsages, + extractable) { + super(); + // Using symbol properties here currently instead of private + // properties because (for now) the performance penalty of + // private fields is still too high. + this[kKeyObject] = keyObject; + this[kAlgorithm] = algorithm; + this[kExtractable] = extractable; + this[kKeyUsages] = keyUsages; + } +} + +InternalCryptoKey.prototype.constructor = CryptoKey; +ObjectSetPrototypeOf(InternalCryptoKey.prototype, CryptoKey.prototype); + +function isCryptoKey(obj) { + return obj != null && obj[kKeyObject] !== undefined; +} + module.exports = { // Public API. createSecretKey, createPublicKey, createPrivateKey, KeyObject, + CryptoKey, + InternalCryptoKey, // These are designed for internal use only and should not be exposed. parsePublicKeyEncoding, parsePrivateKeyEncoding, + parseKeyEncoding, preparePrivateKey, preparePublicOrPrivateKey, prepareSecretKey, SecretKeyObject, PublicKeyObject, PrivateKeyObject, - isKeyObject + isKeyObject, + isCryptoKey, }; diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js new file mode 100644 index 00000000000000..af6b95340f73bd --- /dev/null +++ b/lib/internal/crypto/mac.js @@ -0,0 +1,211 @@ +'use strict'; + +const { + ArrayFrom, + Promise, + SafeSet, +} = primordials; + +const { + HmacJob, + KeyObjectHandle, + kCryptoJobAsync, + kSignJobModeSign, + kSignJobModeVerify, +} = internalBinding('crypto'); + +const { + getHashLength, + hasAnyNotIn, + jobPromise, + lazyDOMException, + normalizeHashName, + validateBitLength, + validateKeyOps, + kHandle, + kKeyObject, +} = require('internal/crypto/util'); + +const { + codes: { + ERR_MISSING_OPTION, + ERR_INVALID_ARG_TYPE, + } +} = require('internal/errors'); + +const { + generateKey, +} = require('internal/crypto/keygen'); + +const { + InternalCryptoKey, + SecretKeyObject, + createSecretKey, + isKeyObject, +} = require('internal/crypto/keys'); + +async function hmacGenerateKey(algorithm, extractable, keyUsages) { + const { hash, name } = algorithm; + let { length } = algorithm; + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + + if (length === undefined) + length = getHashLength(hash.name); + + validateBitLength(length, 'algorithm.length', true); + + const usageSet = new SafeSet(keyUsages); + if (hasAnyNotIn(usageSet, 'sign', 'verify')) { + throw lazyDOMException( + 'Unsupported key usage for an HMAC key', + 'SyntaxError'); + } + return new Promise((resolve, reject) => { + generateKey('hmac', { length }, (err, key) => { + if (err) { + return reject(lazyDOMException( + 'The operation failed for an operation-specific reason', + 'OperationError')); + } + + resolve(new InternalCryptoKey( + key, + { name, length, hash: { name: hash.name } }, + ArrayFrom(usageSet), + extractable)); + }); + }); +} + +async function hmacImportKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + const { hash } = algorithm; + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + + const usagesSet = new SafeSet(keyUsages); + if (hasAnyNotIn(usagesSet, 'sign', 'verify')) { + throw lazyDOMException( + 'Unsupported key usage for an HMAC key', + 'SyntaxError'); + } + let keyObject; + switch (format) { + case 'node.keyObject': { + if (!isKeyObject(keyData)) + throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData); + + if (keyData.type !== 'secret') { + throw lazyDOMException( + `Unable to import HMAC key with format ${format}`); + } + + keyObject = keyData; + break; + } + case 'raw': { + const checkLength = keyData.byteLength * 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 = createSecretKey(keyData); + break; + } + case 'jwk': { + if (keyData == null || typeof keyData !== 'object') + throw lazyDOMException('Invalid JWK keyData', 'DataError'); + + if (keyData.kty !== 'oct') + throw lazyDOMException('Invalid key type', 'DataError'); + + if (usagesSet.size > 0 && + keyData.use !== undefined && + keyData.use !== 'sig') { + throw lazyDOMException('Invalid use type', 'DataError'); + } + + validateKeyOps(keyData.key_ops, usagesSet); + + if (keyData.ext !== undefined && + keyData.ext === false && + extractable === true) { + throw lazyDOMException('JWK is not extractable', 'DataError'); + } + + if (keyData.alg !== undefined) { + if (typeof keyData.alg !== 'string') + throw lazyDOMException('Invalid alg', 'DataError'); + switch (keyData.alg) { + case 'HS1': + if (algorithm.hash.name !== 'SHA-1') + throw lazyDOMException('Digest algorithm mismatch', 'DataError'); + break; + case 'HS256': + if (algorithm.hash.name !== 'SHA-256') + throw lazyDOMException('Digest algorithm mismatch', 'DataError'); + break; + case 'HS384': + if (algorithm.hash.name !== 'SHA-384') + throw lazyDOMException('Digest algorithm mismatch', 'DataError'); + break; + case 'HS512': + if (algorithm.hash.name !== 'SHA-512') + throw lazyDOMException('Digest algorithm mismatch', 'DataError'); + break; + default: + throw lazyDOMException('Unsupported digest algorithm', 'DataError'); + } + } + + const handle = new KeyObjectHandle(); + handle.initJwk(keyData); + keyObject = new SecretKeyObject(handle); + break; + } + default: + throw lazyDOMException(`Unable to import HMAC key with format ${format}`); + } + + const { length } = keyObject[kHandle].keyDetail({}); + + return new InternalCryptoKey( + keyObject, { + name: 'HMAC', + hash: algorithm.hash, + length, + }, + keyUsages, + extractable); +} + +function hmacSignVerify(key, data, algorithm, signature) { + const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; + return jobPromise(new HmacJob( + kCryptoJobAsync, + mode, + normalizeHashName(key.algorithm.hash.name), + key[kKeyObject][kHandle], + data, + signature)); +} + +module.exports = { + hmacImportKey, + hmacGenerateKey, + hmacSignVerify, +}; diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js index 00a031c6ece0a0..868e0f7fbb214e 100644 --- a/lib/internal/crypto/pbkdf2.js +++ b/lib/internal/crypto/pbkdf2.js @@ -1,18 +1,35 @@ 'use strict'; -const { AsyncWrap, Providers } = internalBinding('async_wrap'); +const { + FunctionPrototypeCall, + Promise, +} = primordials; + const { Buffer } = require('buffer'); -const { pbkdf2: _pbkdf2 } = internalBinding('crypto'); -const { validateUint32 } = require('internal/validators'); + +const { + PBKDF2Job, + kCryptoJobAsync, + kCryptoJobSync, +} = internalBinding('crypto'); + +const { + validateInteger, + validateUint32, +} = require('internal/validators'); + const { - ERR_CRYPTO_INVALID_DIGEST, - ERR_CRYPTO_PBKDF2_ERROR, ERR_INVALID_ARG_TYPE, ERR_INVALID_CALLBACK, + ERR_MISSING_OPTION, } = require('internal/errors').codes; + const { + getArrayBufferOrView, getDefaultEncoding, - getArrayBufferView, + lazyDOMException, + normalizeHashName, + kKeyObject, } = require('internal/crypto/util'); function pbkdf2(password, salt, iterations, keylen, digest, callback) { @@ -27,51 +44,95 @@ function pbkdf2(password, salt, iterations, keylen, digest, callback) { if (typeof callback !== 'function') throw new ERR_INVALID_CALLBACK(callback); - const encoding = getDefaultEncoding(); - const keybuf = Buffer.alloc(keylen); + const job = new PBKDF2Job( + kCryptoJobAsync, + password, + salt, + iterations, + keylen, + digest); - const wrap = new AsyncWrap(Providers.PBKDF2REQUEST); - wrap.ondone = (ok) => { // Retains keybuf while request is in flight. - if (!ok) return callback.call(wrap, new ERR_CRYPTO_PBKDF2_ERROR()); - if (encoding === 'buffer') return callback.call(wrap, null, keybuf); - callback.call(wrap, null, keybuf.toString(encoding)); + const encoding = getDefaultEncoding(); + job.ondone = (err, result) => { + if (err !== undefined) + return FunctionPrototypeCall(callback, job, err); + const buf = Buffer.from(result); + if (encoding === 'buffer') + return FunctionPrototypeCall(callback, job, null, buf); + FunctionPrototypeCall(callback, job, null, buf.toString(encoding)); }; - handleError(_pbkdf2(keybuf, password, salt, iterations, digest, wrap), - digest); + job.run(); } function pbkdf2Sync(password, salt, iterations, keylen, digest) { ({ password, salt, iterations, keylen, digest } = check(password, salt, iterations, keylen, digest)); - const keybuf = Buffer.alloc(keylen); - handleError(_pbkdf2(keybuf, password, salt, iterations, digest), digest); + + const job = new PBKDF2Job( + kCryptoJobSync, + password, + salt, + iterations, + keylen, + digest); + + const [err, result] = job.run(); + if (err !== undefined) + throw err; + + const buf = Buffer.from(result); const encoding = getDefaultEncoding(); - if (encoding === 'buffer') return keybuf; - return keybuf.toString(encoding); + return encoding === 'buffer' ? buf : buf.toString(encoding); } function check(password, salt, iterations, keylen, digest) { if (typeof digest !== 'string') throw new ERR_INVALID_ARG_TYPE('digest', 'string', digest); - password = getArrayBufferView(password, 'password'); - salt = getArrayBufferView(salt, 'salt'); + password = getArrayBufferOrView(password, 'password'); + salt = getArrayBufferOrView(salt, 'salt'); validateUint32(iterations, 'iterations', true); validateUint32(keylen, 'keylen'); return { password, salt, iterations, keylen, digest }; } -function handleError(rc, digest) { - if (rc === -1) - throw new ERR_CRYPTO_INVALID_DIGEST(digest); +async function pbkdf2DeriveBits(algorithm, baseKey, length) { + validateUint32(length, 'length'); + const { iterations } = algorithm; + let { hash } = algorithm; + const salt = getArrayBufferOrView(algorithm.salt, 'algorithm.salt'); + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + validateInteger(iterations, 'algorithm.iterations', 1); + + hash = normalizeHashName(hash.name); + + const raw = baseKey[kKeyObject].export(); + + let byteLength = 64; // the default + if (length !== undefined) { + if (length === 0) + throw lazyDOMException('length cannot be zero', 'OperationError'); + if (length % 8) { + throw lazyDOMException( + 'length must be a multiple of 8', + 'OperationError'); + } + byteLength = length / 8; + } - if (rc === false) - throw new ERR_CRYPTO_PBKDF2_ERROR(); + return new Promise((resolve, reject) => { + pbkdf2(raw, salt, iterations, byteLength, hash, (err, result) => { + if (err) return reject(err); + resolve(result); + }); + }); } module.exports = { pbkdf2, - pbkdf2Sync + pbkdf2Sync, + pbkdf2DeriveBits, }; diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index 9e0500ea3a67bb..ae4111bbb07e8d 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -1,21 +1,43 @@ 'use strict'; const { + FunctionPrototypeBind, + FunctionPrototypeCall, MathMin, NumberIsNaN, - NumberIsSafeInteger + NumberIsSafeInteger, } = primordials; -const { AsyncWrap, Providers } = internalBinding('async_wrap'); +const { + RandomBytesJob, + kCryptoJobAsync, + kCryptoJobSync, +} = internalBinding('crypto'); + +const { + lazyDOMException, +} = require('internal/crypto/util'); + const { kMaxLength } = require('buffer'); -const { randomBytes: _randomBytes } = internalBinding('crypto'); + const { - ERR_INVALID_ARG_TYPE, - ERR_INVALID_CALLBACK, - ERR_OUT_OF_RANGE -} = require('internal/errors').codes; + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_CALLBACK, + ERR_OUT_OF_RANGE, + } +} = require('internal/errors'); + const { validateNumber } = require('internal/validators'); -const { isArrayBufferView } = require('internal/util/types'); + +const { + isArrayBufferView, + isAnyArrayBuffer, + isBigInt64Array, + isFloat32Array, + isFloat64Array, +} = require('internal/util/types'); + const { FastBuffer } = require('internal/buffer'); const kMaxUint32 = 2 ** 32 - 1; @@ -56,20 +78,24 @@ function randomBytes(size, callback) { const buf = new FastBuffer(size); - if (!callback) return handleError(_randomBytes(buf, 0, size), buf); - - const wrap = new AsyncWrap(Providers.RANDOMBYTESREQUEST); - wrap.ondone = (ex) => { // Retains buf while request is in flight. - if (ex) return callback.call(wrap, ex); - callback.call(wrap, null, buf); - }; + if (callback === undefined) { + randomFillSync(buf.buffer, 0, size); + return buf; + } - _randomBytes(buf, 0, size, wrap); + // Keep the callback as a regular function so this is propagated. + randomFill(buf.buffer, 0, size, function(error) { + if (error) FunctionPrototypeCall(callback, this, error); + FunctionPrototypeCall(callback, this, null, buf); + }); } function randomFillSync(buf, offset = 0, size) { - if (!isArrayBufferView(buf)) { - throw new ERR_INVALID_ARG_TYPE('buf', 'ArrayBufferView', buf); + if (!isAnyArrayBuffer(buf) && !isArrayBufferView(buf)) { + throw new ERR_INVALID_ARG_TYPE( + 'buf', + ['ArrayBuffer', 'ArrayBufferView'], + buf); } const elementSize = buf.BYTES_PER_ELEMENT || 1; @@ -82,12 +108,28 @@ function randomFillSync(buf, offset = 0, size) { size = assertSize(size, elementSize, offset, buf.byteLength); } - return handleError(_randomBytes(buf, offset, size), buf); + if (size === 0) + return buf; + + const job = new RandomBytesJob( + kCryptoJobSync, + buf.buffer || buf, + offset, + size); + + const [ err ] = job.run(); + if (err) + throw err; + + return buf; } function randomFill(buf, offset, size, callback) { - if (!isArrayBufferView(buf)) { - throw new ERR_INVALID_ARG_TYPE('buf', 'ArrayBufferView', buf); + if (!isAnyArrayBuffer(buf) && !isArrayBufferView(buf)) { + throw new ERR_INVALID_ARG_TYPE( + 'buf', + ['ArrayBuffer', 'ArrayBufferView'], + buf); } const elementSize = buf.BYTES_PER_ELEMENT || 1; @@ -111,13 +153,19 @@ function randomFill(buf, offset, size, callback) { size = assertSize(size, elementSize, offset, buf.byteLength); } - const wrap = new AsyncWrap(Providers.RANDOMBYTESREQUEST); - wrap.ondone = (ex) => { // Retains buf while request is in flight. - if (ex) return callback.call(wrap, ex); - callback.call(wrap, null, buf); - }; + if (size === 0) { + callback(null, buf); + return; + } - _randomBytes(buf, offset, size, wrap); + // TODO(@jasnell): This is not yet handling byte offsets right + const job = new RandomBytesJob( + kCryptoJobAsync, + buf, + offset, + size); + job.ondone = FunctionPrototypeBind(onJobDone, job, buf, callback); + job.run(); } // Largest integer we can read from a buffer. @@ -198,14 +246,42 @@ function randomInt(min, max, callback) { } } -function handleError(ex, buf) { - if (ex) throw ex; - return buf; + +function onJobDone(buf, callback, error) { + if (error) return FunctionPrototypeCall(callback, this, error); + FunctionPrototypeCall(callback, this, null, buf); +} + +// Really just the Web Crypto API alternative +// to require('crypto').randomFillSync() with an +// additional limitation that the input buffer is +// not allowed to exceed 65536 bytes, and can only +// be an integer-type TypedArray. +function getRandomValues(data) { + if (!isArrayBufferView(data) || + isBigInt64Array(data) || + isFloat32Array(data) || + isFloat64Array(data)) { + // Ordinarily this would be an ERR_INVALID_ARG_TYPE. However, + // the Web Crypto API and web platform tests expect this to + // be a DOMException with type TypeMismatchError. + throw lazyDOMException( + 'The data argument must be an integer-type TypedArray', + 'TypeMismatchError'); + } + if (data.byteLength > 65536) { + throw lazyDOMException( + 'The requested length exceeds 65,536 bytes', + 'QuotaExceededError'); + } + randomFillSync(data, 0); + return data; } module.exports = { randomBytes, randomFill, randomFillSync, - randomInt + randomInt, + getRandomValues, }; diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js new file mode 100644 index 00000000000000..1c90b57f439177 --- /dev/null +++ b/lib/internal/crypto/rsa.js @@ -0,0 +1,371 @@ +'use strict'; + +const { + Promise, + SafeSet, + Uint8Array, +} = primordials; + +const { + KeyObjectHandle, + RSACipherJob, + RSAKeyExportJob, + SignJob, + kCryptoJobAsync, + kSignJobModeSign, + kSignJobModeVerify, + kKeyVariantRSA_SSA_PKCS1_V1_5, + kKeyVariantRSA_PSS, + kKeyVariantRSA_OAEP, + kKeyTypePrivate, + kWebCryptoCipherEncrypt, + RSA_PKCS1_PSS_PADDING, +} = internalBinding('crypto'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_MISSING_OPTION, + } +} = require('internal/errors'); + +const { + validateUint32, +} = require('internal/validators'); + +const { + bigIntArrayToUnsignedInt, + getArrayBufferOrView, + getUsagesUnion, + hasAnyNotIn, + jobPromise, + lazyDOMException, + normalizeHashName, + validateKeyOps, + validateMaxBufferLength, + kHandle, + kKeyObject, +} = require('internal/crypto/util'); + +const { + isUint8Array, +} = require('internal/util/types'); + +const { + InternalCryptoKey, + PrivateKeyObject, + PublicKeyObject, + createPublicKey, + createPrivateKey, + isKeyObject, +} = require('internal/crypto/keys'); + +const { + generateKeyPair, +} = require('internal/crypto/keygen'); + +const kRsaVariants = { + 'RSASSA-PKCS1-V1_5': kKeyVariantRSA_SSA_PKCS1_V1_5, + 'RSA-PSS': kKeyVariantRSA_PSS, + 'RSA-OAEP': kKeyVariantRSA_OAEP, +}; + +function verifyAcceptableRsaKeyUse(name, type, usages) { + let checkSet; + switch (name) { + case 'RSA-OAEP': + switch (type) { + case 'private': + checkSet = ['decrypt', 'unwrapKey']; + break; + case 'public': + checkSet = ['encrypt', 'wrapKey']; + break; + } + break; + default: + switch (type) { + case 'private': + checkSet = ['sign']; + break; + case 'public': + checkSet = ['verify']; + break; + } + } + if (hasAnyNotIn(usages, ...checkSet)) { + throw lazyDOMException( + `Unsupported key usage for an ${name} key`, + 'SyntaxError'); + } +} + +function rsaOaepCipher(mode, key, data, { label }) { + const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private'; + if (key.type !== type) { + throw lazyDOMException( + 'The requested operation is not valid for the provided key', + 'InvalidAccessError'); + } + if (label !== undefined) { + label = getArrayBufferOrView(label, 'algorithm.label'); + validateMaxBufferLength(label, 'algorithm.label'); + } + + return jobPromise(new RSACipherJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + data, + kKeyVariantRSA_OAEP, + normalizeHashName(key.algorithm.hash.name), + label)); +} + +async function rsaKeyGenerate( + algorithm, + extractable, + keyUsages) { + + const { + name, + modulusLength, + publicExponent, + hash + } = algorithm; + + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + validateUint32(modulusLength, 'algorithm.modulusLength'); + if (!isUint8Array(publicExponent)) { + throw new ERR_INVALID_ARG_TYPE( + 'algorithm.publicExponent', + 'Uint8Array', + publicExponent); + } + + const usageSet = new SafeSet(keyUsages); + + const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent); + if (publicExponentConverted === undefined) { + throw lazyDOMException( + 'The publicExponent must be equivalent to an unsigned 32-bit value', + 'OperationError'); + } + + switch (name) { + case 'RSA-OAEP': + if (hasAnyNotIn(usageSet, 'encrypt', 'decrypt', 'wrapKey', 'unwrapKey')) { + throw lazyDOMException( + 'Unsupported key usage for a RSA key', + 'SyntaxError'); + } + break; + default: + if (hasAnyNotIn(usageSet, 'sign', 'verify')) { + throw lazyDOMException( + 'Unsupported key usage for a RSA key', + 'SyntaxError'); + } + } + + return new Promise((resolve, reject) => { + generateKeyPair('rsa', { + modulusLength, + publicExponentConverted, + }, (err, pubKey, privKey) => { + if (err) { + return reject(lazyDOMException( + 'The operation failed for an operation-specific reason', + 'OperationError')); + } + + const algorithm = { + name, + modulusLength, + publicExponent, + hash: { name: hash.name } + }; + + let publicUsages; + let privateUsages; + switch (name) { + case 'RSA-OAEP': { + publicUsages = getUsagesUnion(usageSet, 'encrypt', 'wrapKey'); + privateUsages = getUsagesUnion(usageSet, 'decrypt', 'unwrapKey'); + break; + } + default: { + publicUsages = getUsagesUnion(usageSet, 'verify'); + privateUsages = getUsagesUnion(usageSet, 'sign'); + break; + } + } + + const publicKey = + new InternalCryptoKey( + pubKey, + algorithm, + publicUsages, + true); + + const privateKey = + new InternalCryptoKey( + privKey, + algorithm, + privateUsages, + extractable); + + resolve({ publicKey, privateKey }); + }); + }); +} + +function rsaExportKey(key, format) { + return jobPromise(new RSAKeyExportJob( + kCryptoJobAsync, + format, + key[kKeyObject][kHandle], + kRsaVariants[key.algorithm.name])); +} + +async function rsaImportKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + const { hash } = algorithm; + if (hash === undefined) + throw new ERR_MISSING_OPTION('algorithm.hash'); + + const usagesSet = new SafeSet(keyUsages); + let keyObject; + switch (format) { + case 'node.keyObject': { + if (!isKeyObject(keyData)) + throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData); + if (keyData.type === 'secret') + throw lazyDOMException('Invalid key type', 'InvalidAccessException'); + verifyAcceptableRsaKeyUse(algorithm.name, keyData.type, usagesSet); + keyObject = keyData; + break; + } + case 'spki': { + verifyAcceptableRsaKeyUse(algorithm.name, 'public', usagesSet); + keyObject = createPublicKey({ + key: keyData, + format: 'der', + type: 'spki' + }); + break; + } + case 'pkcs8': { + verifyAcceptableRsaKeyUse(algorithm.name, 'private', usagesSet); + keyObject = createPrivateKey({ + key: keyData, + format: 'der', + type: 'pkcs8' + }); + break; + } + case 'jwk': { + if (keyData == null || typeof keyData !== 'object') + throw lazyDOMException('Invalid JWK keyData', 'DataError'); + + verifyAcceptableRsaKeyUse( + algorithm.name, + keyData.d !== undefined ? 'private' : 'public', + usagesSet); + + if (keyData.kty !== 'RSA') + throw lazyDOMException('Invalid key type', 'DataError'); + + if (usagesSet.size > 0 && keyData.use !== undefined) { + const checkUse = algorithm.name === 'RSA-OAEP' ? 'enc' : 'sig'; + if (keyData.use !== checkUse) + throw lazyDOMException('Invalid use type', 'DataError'); + } + + validateKeyOps(keyData.key_ops, usagesSet); + + if (keyData.ext !== undefined && + keyData.ext === false && + extractable === true) { + throw lazyDOMException('JWK is not extractable', 'DataError'); + } + + if (keyData.alg !== undefined) { + if (typeof keyData.alg !== 'string') + throw lazyDOMException('Invalid alg', 'DataError'); + const hash = + normalizeHashName(keyData.alg, normalizeHashName.kContextWebCrypto); + if (hash !== algorithm.hash.name) + throw lazyDOMException('Hash mismatch', 'DataError'); + } + + const handle = new KeyObjectHandle(); + const type = handle.initJwk(keyData); + if (type === undefined) + throw lazyDOMException('Invalid JWK keyData', 'DataError'); + + keyObject = type === kKeyTypePrivate ? + new PrivateKeyObject(handle) : + new PublicKeyObject(handle); + + break; + } + default: + throw lazyDOMException( + `Unable to import RSA key with format ${format}`, + 'NotSupportedError'); + } + + const { + modulusLength, + publicExponent, + } = keyObject[kHandle].keyDetail({}); + + return new InternalCryptoKey(keyObject, { + name: algorithm.name, + modulusLength, + publicExponent: new Uint8Array(publicExponent), + hash: algorithm.hash + }, keyUsages, extractable); +} + +function rsaSignVerify(key, data, { saltLength }, signature) { + let padding; + if (key.algorithm.name === 'RSA-PSS') { + padding = RSA_PKCS1_PSS_PADDING; + // TODO(@jasnell): Validate maximum size of saltLength + // based on the key size: + // Math.ceil((keySizeInBits - 1)/8) - digestSizeInBytes - 2 + validateUint32(saltLength, 'algorithm.saltLength'); + } + + const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; + const type = mode === kSignJobModeSign ? 'private' : 'public'; + + if (key.type !== type) + throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + + return jobPromise(new SignJob( + kCryptoJobAsync, + signature === undefined ? kSignJobModeSign : kSignJobModeVerify, + key[kKeyObject][kHandle], + data, + normalizeHashName(key.algorithm.hash.name), + saltLength, + padding, + signature)); +} + + +module.exports = { + rsaCipher: rsaOaepCipher, + rsaExportKey, + rsaImportKey, + rsaKeyGenerate, + rsaSignVerify, +}; diff --git a/lib/internal/crypto/scrypt.js b/lib/internal/crypto/scrypt.js index cd18f671b18b73..76549c88289ecc 100644 --- a/lib/internal/crypto/scrypt.js +++ b/lib/internal/crypto/scrypt.js @@ -1,17 +1,36 @@ 'use strict'; -const { AsyncWrap, Providers } = internalBinding('async_wrap'); +const { + FunctionPrototypeCall, + Promise, +} = primordials; + const { Buffer } = require('buffer'); -const { scrypt: _scrypt } = internalBinding('crypto'); -const { validateInteger, validateUint32 } = require('internal/validators'); + const { - ERR_CRYPTO_SCRYPT_INVALID_PARAMETER, - ERR_CRYPTO_SCRYPT_NOT_SUPPORTED, - ERR_INVALID_CALLBACK -} = require('internal/errors').codes; + ScryptJob, + kCryptoJobAsync, + kCryptoJobSync, +} = internalBinding('crypto'); + +const { + validateInteger, + validateUint32, +} = require('internal/validators'); + const { + codes: { + ERR_CRYPTO_SCRYPT_INVALID_PARAMETER, + ERR_CRYPTO_SCRYPT_NOT_SUPPORTED, + ERR_INVALID_CALLBACK, + } +} = require('internal/errors'); + +const { + getArrayBufferOrView, getDefaultEncoding, - getArrayBufferView, + lazyDOMException, + kKeyObject, } = require('internal/crypto/util'); const defaults = { @@ -34,46 +53,44 @@ function scrypt(password, salt, keylen, options, callback = defaults) { if (typeof callback !== 'function') throw new ERR_INVALID_CALLBACK(callback); - const encoding = getDefaultEncoding(); - const keybuf = Buffer.alloc(keylen); + const job = new ScryptJob( + kCryptoJobAsync, password, salt, N, r, p, maxmem, keylen); - const wrap = new AsyncWrap(Providers.SCRYPTREQUEST); - wrap.ondone = (ex) => { // Retains keybuf while request is in flight. - if (ex) return callback.call(wrap, ex); - if (encoding === 'buffer') return callback.call(wrap, null, keybuf); - callback.call(wrap, null, keybuf.toString(encoding)); + const encoding = getDefaultEncoding(); + job.ondone = (error, result) => { + if (error !== undefined) + return FunctionPrototypeCall(callback, job, error); + const buf = Buffer.from(result); + if (encoding === 'buffer') + return FunctionPrototypeCall(callback, job, null, buf); + FunctionPrototypeCall(callback, job, null, buf.toString(encoding)); }; - handleError(_scrypt(keybuf, password, salt, N, r, p, maxmem, wrap)); + job.run(); } function scryptSync(password, salt, keylen, options = defaults) { options = check(password, salt, keylen, options); const { N, r, p, maxmem } = options; ({ password, salt, keylen } = options); - const keybuf = Buffer.alloc(keylen); - handleError(_scrypt(keybuf, password, salt, N, r, p, maxmem)); - const encoding = getDefaultEncoding(); - if (encoding === 'buffer') return keybuf; - return keybuf.toString(encoding); -} - -function handleError(ex) { - if (ex === undefined) - return; + const job = new ScryptJob( + kCryptoJobSync, password, salt, N, r, p, maxmem, keylen); + const [err, result] = job.run(); - if (ex === null) - throw new ERR_CRYPTO_SCRYPT_INVALID_PARAMETER(); // Bad N, r, p, or maxmem. + if (err !== undefined) + throw err; - throw ex; // Scrypt operation failed, exception object contains details. + const buf = Buffer.from(result); + const encoding = getDefaultEncoding(); + return encoding === 'buffer' ? buf : buf.toString(encoding); } function check(password, salt, keylen, options) { - if (_scrypt === undefined) + if (ScryptJob === undefined) throw new ERR_CRYPTO_SCRYPT_NOT_SUPPORTED(); - password = getArrayBufferView(password, 'password'); - salt = getArrayBufferView(salt, 'salt'); + password = getArrayBufferOrView(password, 'password'); + salt = getArrayBufferOrView(salt, 'salt'); validateUint32(keylen, 'keylen'); let { N, r, p, maxmem } = defaults; @@ -119,4 +136,45 @@ function check(password, salt, keylen, options) { return { password, salt, keylen, N, r, p, maxmem }; } -module.exports = { scrypt, scryptSync }; +async function scryptDeriveBits(algorithm, baseKey, length) { + validateUint32(length, 'length'); + const { + N = 16384, + r = 8, + p = 1, + maxmem = 32 * 1024 * 1024, + encoding, + } = algorithm; + validateUint32(N, 'algorithm.N'); + validateUint32(r, 'algorithm.r'); + validateUint32(p, 'algorithm.p'); + validateUint32(maxmem, 'algorithm.maxmem'); + const salt = getArrayBufferOrView(algorithm.salt, 'algorithm.salt', encoding); + + const raw = baseKey[kKeyObject].export(); + + let byteLength = 64; // the default + if (length !== undefined) { + if (length === 0) + throw lazyDOMException('length cannot be zero', 'OperationError'); + if (length % 8) { + throw lazyDOMException( + 'length must be a multiple of 8', + 'OperationError'); + } + byteLength = length / 8; + } + + return new Promise((resolve, reject) => { + scrypt(raw, salt, byteLength, { N, r, p, maxmem }, (err, result) => { + if (err) return reject(err); + resolve(result); + }); + }); +} + +module.exports = { + scrypt, + scryptSync, + scryptDeriveBits, +}; diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js index 7c70765e005dac..27b26a87f0d6f3 100644 --- a/lib/internal/crypto/sig.js +++ b/lib/internal/crypto/sig.js @@ -5,30 +5,43 @@ const { } = primordials; const { - ERR_CRYPTO_SIGN_KEY_REQUIRED, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, -} = require('internal/errors').codes; -const { validateEncoding, validateString } = require('internal/validators'); + codes: { + ERR_CRYPTO_SIGN_KEY_REQUIRED, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + } +} = require('internal/errors'); + +const { + validateEncoding, + validateString, +} = require('internal/validators'); + const { Sign: _Sign, Verify: _Verify, + signOneShot: _signOneShot, + verifyOneShot: _verifyOneShot, kSigEncDER, kSigEncP1363, - signOneShot: _signOneShot, - verifyOneShot: _verifyOneShot } = internalBinding('crypto'); + const { + getArrayBufferOrView, getDefaultEncoding, kHandle, - getArrayBufferView, } = require('internal/crypto/util'); + const { + preparePublicOrPrivateKey, preparePrivateKey, - preparePublicOrPrivateKey } = require('internal/crypto/keys'); + const { Writable } = require('stream'); -const { isArrayBufferView } = require('internal/util/types'); + +const { + isArrayBufferView, +} = require('internal/util/types'); function Sign(algorithm, options) { if (!(this instanceof Sign)) @@ -121,13 +134,7 @@ function signOneShot(algorithm, data, key) { if (algorithm != null) validateString(algorithm, 'algorithm'); - if (!isArrayBufferView(data)) { - throw new ERR_INVALID_ARG_TYPE( - 'data', - ['Buffer', 'TypedArray', 'DataView'], - data - ); - } + data = getArrayBufferOrView(data, 'data'); if (!key) throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); @@ -183,7 +190,7 @@ Verify.prototype.verify = function verify(options, signature, sigEncoding) { // Options specific to (EC)DSA const dsaSigEnc = getDSASignatureEncoding(options); - signature = getArrayBufferView(signature, 'signature', sigEncoding); + signature = getArrayBufferOrView(signature, 'signature', sigEncoding); return this[kHandle].verify(data, format, type, passphrase, signature, rsaPadding, pssSaltLength, dsaSigEnc); @@ -193,6 +200,8 @@ function verifyOneShot(algorithm, data, key, signature) { if (algorithm != null) validateString(algorithm, 'algorithm'); + data = getArrayBufferOrView(data, 'data'); + if (!isArrayBufferView(data)) { throw new ERR_INVALID_ARG_TYPE( 'data', @@ -231,5 +240,5 @@ module.exports = { Sign, signOneShot, Verify, - verifyOneShot + verifyOneShot, }; diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 8d43514906fde8..20d8058e585565 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -1,6 +1,12 @@ 'use strict'; const { + ArrayPrototypeIncludes, + ArrayPrototypePush, + FunctionPrototypeBind, + Promise, + StringPrototypeToLowerCase, + StringPrototypeToUpperCase, Symbol, } = primordials; @@ -12,27 +18,58 @@ const { } = internalBinding('crypto'); const { - ENGINE_METHOD_ALL -} = internalBinding('constants').crypto; + crypto: { + ENGINE_METHOD_ALL + } +} = internalBinding('constants'); + +const normalizeHashName = require('internal/crypto/hashnames'); const { hideStackFrames, codes: { ERR_CRYPTO_ENGINE_UNKNOWN, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_OUT_OF_RANGE, } } = require('internal/errors'); -const { validateString } = require('internal/validators'); + +const { + validateArray, + validateString +} = require('internal/validators'); + const { Buffer } = require('buffer'); + const { cachedResult, - filterDuplicateStrings + filterDuplicateStrings, } = require('internal/util'); + const { - isArrayBufferView + isArrayBufferView, + isAnyArrayBuffer, } = require('internal/util/types'); const kHandle = Symbol('kHandle'); +const kKeyObject = Symbol('kKeyObject'); + +const lazyRequireCache = {}; + +function lazyRequire(name) { + let ret = lazyRequireCache[name]; + if (ret === undefined) + ret = lazyRequireCache[name] = require(name); + return ret; +} + +let DOMException; +const lazyDOMException = hideStackFrames((message, name) => { + if (DOMException === undefined) + DOMException = internalBinding('messaging').DOMException; + return new DOMException(message, name); +}); var defaultEncoding = 'buffer'; @@ -74,7 +111,9 @@ function setEngine(id, flags) { throw new ERR_CRYPTO_ENGINE_UNKNOWN(id); } -const getArrayBufferView = hideStackFrames((buffer, name, encoding) => { +const getArrayBufferOrView = hideStackFrames((buffer, name, encoding) => { + if (isAnyArrayBuffer(buffer)) + return buffer; if (typeof buffer === 'string') { if (encoding === 'buffer') encoding = 'utf8'; @@ -83,21 +122,284 @@ const getArrayBufferView = hideStackFrames((buffer, name, encoding) => { if (!isArrayBufferView(buffer)) { throw new ERR_INVALID_ARG_TYPE( name, - ['string', 'Buffer', 'TypedArray', 'DataView'], + [ + 'string', + 'ArrayBuffer', + 'Buffer', + 'TypedArray', + 'DataView' + ], buffer ); } return buffer; }); +// The maximum buffer size that we'll support in the WebCrypto impl +const kMaxBufferLength = (2 ** 31) - 1; + +// The EC named curves that we currently support via the Web Crypto API. +const kNamedCurveAliases = { + 'P-256': 'prime256v1', + 'P-384': 'secp384r1', + 'P-521': 'secp521r1', +}; + +const kAesKeyLengths = [128, 192, 256]; + +// These are the only algorithms we currently support +// via the Web Crypto API +const kAlgorithms = [ + 'rsassa-pkcs1-v1_5', + 'rsa-pss', + 'rsa-oaep', + 'ecdsa', + 'ecdh', + 'aes-ctr', + 'aes-cbc', + 'aes-gcm', + 'aes-kw', + 'hmac', + 'sha-1', + 'sha-256', + 'sha-384', + 'sha-512', + 'hkdf', + 'pbkdf2', + // Following here are Node.js specific extensions. All + // should be prefixed with 'node-' + 'node-dsa', + 'node-dh', + 'node-scrypt' +]; + +// These are the only export and import formats we currently +// support via the Web Crypto API +const kExportFormats = [ + 'raw', + 'pkcs8', + 'spki', + 'jwk', + 'node.keyObject']; + +// These are the only hash algorithms we currently support via +// the Web Crypto API. +const kHashTypes = [ + 'SHA-1', + 'SHA-256', + 'SHA-384', + 'SHA-512' +]; + +function validateMaxBufferLength(data, name) { + if (data.byteLength > kMaxBufferLength) { + throw lazyDOMException( + `${name} must be less than ${kMaxBufferLength + 1} bits`, + 'OperationError'); + } +} + +function normalizeAlgorithm(algorithm, label = 'algorithm') { + if (algorithm != null) { + if (typeof algorithm === 'string') + algorithm = { name: algorithm }; + + if (typeof algorithm === 'object') { + const { name } = algorithm; + let hash; + if (typeof name !== 'string' || + !ArrayPrototypeIncludes( + kAlgorithms, + StringPrototypeToLowerCase(name))) { + throw lazyDOMException('Unrecognized name.', 'NotSupportedError'); + } + if (algorithm.hash !== undefined) { + hash = normalizeAlgorithm(algorithm.hash, 'algorithm.hash'); + if (!ArrayPrototypeIncludes(kHashTypes, hash.name)) + throw lazyDOMException('Unrecognized name.', 'NotSupportedError'); + } + return { ...algorithm, name: StringPrototypeToUpperCase(name), hash }; + } + } + throw lazyDOMException('Unrecognized name.', 'NotSupportedError'); +} + +function hasAnyNotIn(set, ...check) { + for (const s of set) + if (!ArrayPrototypeIncludes(check, s)) + return true; + return false; +} + +function validateBitLength(length, name, required = false) { + if (length !== undefined || required) { + if (typeof length !== 'number') + throw new ERR_INVALID_ARG_TYPE(name, 'number', length); + if (length < 0) + throw new ERR_OUT_OF_RANGE(name, '> 0'); + if (length % 8) { + throw new ERR_INVALID_ARG_VALUE( + name, + length, + 'must be a multiple of 8'); + } + } +} + +function validateByteLength(buf, name, target) { + if (buf.byteLength !== target) { + throw lazyDOMException( + `${name} must contain exactly ${target} bytes`, + 'OperationError'); + } +} + +const validateByteSource = hideStackFrames((val, name) => { + val = toBuf(val); + + if (isAnyArrayBuffer(val) || isArrayBufferView(val)) + return; + + throw new ERR_INVALID_ARG_TYPE( + name, + [ + 'string', + 'ArrayBuffer', + 'TypedArray', + 'DataView', + 'Buffer' + ], + val); +}); + +function onDone(resolve, reject, err, result) { + if (err) return reject(err); + resolve(result); +} + +function jobPromise(job) { + return new Promise((resolve, reject) => { + job.ondone = FunctionPrototypeBind(onDone, job, resolve, reject); + job.run(); + }); +} + +// In WebCrypto, the publicExponent option in RSA is represented as a +// WebIDL "BigInteger"... that is, a Uint8Array that allows an arbitrary +// number of leading zero bits. Our conventional APIs for reading +// an unsigned int from a Buffer are not adequate. The implementation +// here is adapted from the chromium implementation here: +// https://github.com/chromium/chromium/blob/master/third_party/blink/public/platform/web_crypto_algorithm_params.h, but ported to JavaScript +// Returns undefined if the conversion was unsuccessful. +function bigIntArrayToUnsignedInt(input) { + let result = 0; + + for (let n = 0; n < input.length; ++n) { + const n_reversed = input.length - n - 1; + if (n_reversed >= 4 && input[n]) + return; // Too large + result |= input[n] << 8 * n_reversed; + } + + return result; +} + +function getStringOption(options, key) { + let value; + if (options && (value = options[key]) != null) + validateString(value, `options.${key}`); + return value; +} + +function getUsagesUnion(usageSet, ...usages) { + const newset = []; + for (let n = 0; n < usages.length; n++) { + if (usageSet.has(usages[n])) + ArrayPrototypePush(newset, usages[n]); + } + return newset; +} + +function getHashLength(name) { + switch (name) { + case 'SHA-1': return 160; + case 'SHA-256': return 256; + case 'SHA-384': return 384; + case 'SHA-512': return 512; + } +} + +const kKeyOps = { + sign: 1, + verify: 2, + encrypt: 3, + decrypt: 4, + wrapKey: 5, + unwrapKey: 6, + deriveKey: 7, + deriveBits: 8, +}; + +function validateKeyOps(keyOps, usagesSet) { + if (keyOps === undefined) return; + validateArray(keyOps, 'keyData.key_ops'); + let flags = 0; + for (let n = 0; n < keyOps.length; n++) { + const op = keyOps[n]; + const op_flag = kKeyOps[op]; + // Skipping unknown key ops + if (op_flag === undefined) + continue; + // Have we seen it already? if so, error + if (flags & (1 << op_flag)) + throw lazyDOMException('Duplicate key operation', 'DataError'); + flags |= (1 << op_flag); + + // TODO(@jasnell): RFC7517 section 4.3 strong recommends validating + // key usage combinations. Specifically, it says that unrelated key + // ops SHOULD NOT be used together. We're not yet validating that here. + } + + if (usagesSet !== undefined) { + for (const use of usagesSet) { + if (!ArrayPrototypeIncludes(keyOps, use)) { + throw lazyDOMException( + 'Key operations and usage mismatch', + 'DataError'); + } + } + } +} + module.exports = { - getArrayBufferView, + getArrayBufferOrView, getCiphers, getCurves, getDefaultEncoding, getHashes, kHandle, + kKeyObject, setDefaultEncoding, setEngine, - toBuf + toBuf, + + kHashTypes, + kNamedCurveAliases, + kAesKeyLengths, + kExportFormats, + normalizeAlgorithm, + normalizeHashName, + hasAnyNotIn, + lazyDOMException, + validateBitLength, + validateByteLength, + validateByteSource, + validateKeyOps, + jobPromise, + lazyRequire, + validateMaxBufferLength, + bigIntArrayToUnsignedInt, + getStringOption, + getUsagesUnion, + getHashLength, }; diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js new file mode 100644 index 00000000000000..0a7101bfcc78c2 --- /dev/null +++ b/lib/internal/crypto/webcrypto.js @@ -0,0 +1,760 @@ +'use strict'; + +const { + ArrayPrototypeIncludes, + JSONParse, + JSONStringify, + ObjectDefineProperties, + SafeSet, + SymbolToStringTag, +} = primordials; + +const { + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatSPKI, + kWebCryptoCipherEncrypt, + kWebCryptoCipherDecrypt, +} = internalBinding('crypto'); + +const { + validateArray, + validateBoolean, + validateObject, + validateOneOf, + validateString, +} = require('internal/validators'); + +const { TextDecoder } = require('internal/encoding'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + } +} = require('internal/errors'); + +const { + CryptoKey, + InternalCryptoKey, + createSecretKey, + isCryptoKey, + isKeyObject, +} = require('internal/crypto/keys'); + +const { + asyncDigest, +} = require('internal/crypto/hash'); + +const { + getArrayBufferOrView, + hasAnyNotIn, + lazyDOMException, + lazyRequire, + normalizeAlgorithm, + normalizeHashName, + validateMaxBufferLength, + kExportFormats, + kHandle, + kKeyObject, +} = require('internal/crypto/util'); + +const { + getRandomValues, +} = require('internal/crypto/random'); + +async function generateKey( + algorithm, + extractable, + keyUsages) { + algorithm = normalizeAlgorithm(algorithm); + validateBoolean(extractable, 'extractable'); + validateArray(keyUsages, 'keyUsages'); + switch (algorithm.name) { + case 'RSASSA-PKCS1-V1_5': + // Fall through + case 'RSA-PSS': + // Fall through + case 'RSA-OAEP': + return lazyRequire('internal/crypto/rsa') + .rsaKeyGenerate(algorithm, extractable, keyUsages); + case 'ECDSA': + // Fall through + case 'ECDH': + return lazyRequire('internal/crypto/ec') + .ecGenerateKey(algorithm, extractable, keyUsages); + case 'HMAC': + return lazyRequire('internal/crypto/mac') + .hmacGenerateKey(algorithm, extractable, keyUsages); + case 'AES-CTR': + // Fall through + case 'AES-CBC': + // Fall through + case 'AES-GCM': + // Fall through + case 'AES-KW': + return lazyRequire('internal/crypto/aes') + .aesGenerateKey(algorithm, extractable, keyUsages); + + // Following are Node.js specific extensions. Names must be prefixed + // with the `NODE-` + case 'NODE-DSA': + return lazyRequire('internal/crypto/dsa') + .dsaGenerateKey(algorithm, extractable, keyUsages); + case 'NODE-DH': + return lazyRequire('internal/crypto/diffiehellman') + .dhGenerateKey(algorithm, extractable, keyUsages); + default: + throw lazyDOMException('Unrecognized name.'); + } +} + +async function deriveBits(algorithm, baseKey, length) { + algorithm = normalizeAlgorithm(algorithm); + if (!isCryptoKey(baseKey)) + throw new ERR_INVALID_ARG_TYPE('baseKey', 'CryptoKey', baseKey); + if (!ArrayPrototypeIncludes(baseKey.usages, 'deriveBits')) { + throw lazyDOMException( + 'baseKey does not have deriveBits usage', + 'InvalidAccessError'); + } + if (baseKey.algorithm.name !== algorithm.name) + throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); + switch (algorithm.name) { + case 'ECDH': + return lazyRequire('internal/crypto/diffiehellman') + .asyncDeriveBitsECDH(algorithm, baseKey, length); + case 'HKDF': + return lazyRequire('internal/crypto/hkdf') + .hkdfDeriveBits(algorithm, baseKey, length); + case 'PBKDF2': + return lazyRequire('internal/crypto/pbkdf2') + .pbkdf2DeriveBits(algorithm, baseKey, length); + case 'NODE-SCRYPT': + return lazyRequire('internal/crypto/scrypt') + .asyncScryptDeriveBits(algorithm, baseKey, length); + case 'NODE-DH': + return lazyRequire('internal/crypto/diffiehellman') + .asyncDeriveBitsDH(algorithm, baseKey, length); + } + throw lazyDOMException('Unrecognized name.'); +} + +async function deriveKey( + algorithm, + baseKey, + derivedKeyAlgorithm, + extractable, + keyUsages) { + algorithm = normalizeAlgorithm(algorithm); + derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm); + if (!isCryptoKey(baseKey)) + throw new ERR_INVALID_ARG_TYPE('baseKey', 'CryptoKey', baseKey); + if (!ArrayPrototypeIncludes(baseKey.usages, 'deriveKey')) { + throw lazyDOMException( + 'baseKey does not have deriveKey usage', + 'InvalidAccessError'); + } + if (baseKey.algorithm.name !== algorithm.name) + throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); + validateObject(derivedKeyAlgorithm, 'derivedKeyAlgorithm'); + validateBoolean(extractable, 'extractable'); + validateArray(keyUsages, 'keyUsages'); + + const { length } = derivedKeyAlgorithm; + let bits; + switch (algorithm.name) { + case 'ECDH': + bits = await lazyRequire('internal/crypto/diffiehellman') + .asyncDeriveBitsECDH(algorithm, baseKey, length); + break; + case 'HKDF': + bits = await lazyRequire('internal/crypto/hkdf') + .hkdfDeriveBits(algorithm, baseKey, length); + break; + case 'PBKDF2': + bits = await lazyRequire('internal/crypto/pbkdf2') + .pbkdf2DeriveBits(algorithm, baseKey, length); + break; + case 'NODE-SCRYPT': + bits = await lazyRequire('internal/crypto/scrypt') + .scryptDeriveBits(algorithm, baseKey, length); + break; + case 'NODE-DH': + bits = await lazyRequire('internal/crypto/diffiehellman') + .asyncDeriveBitsDH(algorithm, baseKey, length); + break; + default: + throw lazyDOMException('Unrecognized name.'); + } + + return importKey('raw', bits, derivedKeyAlgorithm, extractable, keyUsages); +} + +async function exportKeySpki(key) { + switch (key.algorithm.name) { + case 'RSASSA-PKCS1-V1_5': + // Fall through + case 'RSA-PSS': + // Fall through + case 'RSA-OAEP': + if (key.type === 'public') { + return lazyRequire('internal/crypto/rsa') + .rsaExportKey(key, kWebCryptoKeyFormatSPKI); + } + break; + case 'ECDSA': + // Fall through + case 'ECDH': + if (key.type === 'public') { + return lazyRequire('internal/crypto/ec') + .ecExportKey(key, kWebCryptoKeyFormatSPKI); + } + break; + case 'NODE-DSA': + if (key.type === 'public') { + return lazyRequire('internal/crypto/dsa') + .dsaExportKey(key, kWebCryptoKeyFormatSPKI); + } + break; + case 'NODE-DH': + if (key.type === 'public') { + return lazyRequire('internal/crypto/diffiehellman') + .dhExportKey(key, kWebCryptoKeyFormatSPKI); + } + break; + } + + throw lazyDOMException( + `Unable to export a raw ${key.algorithm.name} ${key.type} key`, + 'InvalidAccessError'); +} + +async function exportKeyPkcs8(key) { + switch (key.algorithm.name) { + case 'RSASSA-PKCS1-V1_5': + // Fall through + case 'RSA-PSS': + // Fall through + case 'RSA-OAEP': + if (key.type === 'private') { + return lazyRequire('internal/crypto/rsa') + .rsaExportKey(key, kWebCryptoKeyFormatPKCS8); + } + break; + case 'ECDSA': + // Fall through + case 'ECDH': + if (key.type === 'private') { + return lazyRequire('internal/crypto/ec') + .ecExportKey(key, kWebCryptoKeyFormatPKCS8); + } + break; + case 'NODE-DSA': + if (key.type === 'private') { + return lazyRequire('internal/crypto/dsa') + .dsaExportKey(key, kWebCryptoKeyFormatPKCS8); + } + break; + case 'NODE-DH': + if (key.type === 'private') { + return lazyRequire('internal/crypto/diffiehellman') + .dhExportKey(key, kWebCryptoKeyFormatPKCS8); + } + break; + } + + throw lazyDOMException( + `Unable to export a pkcs8 ${key.algorithm.name} ${key.type} key`, + 'InvalidAccessError'); +} + +async function exportKeyRaw(key) { + switch (key.algorithm.name) { + case 'ECDSA': + // Fall through + case 'ECDH': + if (key.type === 'public') { + return lazyRequire('internal/crypto/ec') + .ecExportKey(key, kWebCryptoKeyFormatRaw); + } + break; + case 'AES-CTR': + // Fall through + case 'AES-CBC': + // Fall through + case 'AES-GCM': + // Fall through + case 'AES-KW': + // Fall through + case 'HMAC': + return key[kKeyObject].export().buffer; + } + + throw lazyDOMException( + `Unable to export a raw ${key.algorithm.name} ${key.type} key`, + 'InvalidAccessError'); +} + +async function exportKeyJWK(key) { + const jwk = key[kKeyObject][kHandle].exportJwk({ + key_ops: key.usages, + ext: key.extractable, + }); + switch (key.algorithm.name) { + case 'RSASSA-PKCS1-V1_5': + jwk.alg = normalizeHashName( + key.algorithm.hash.name, + normalizeHashName.kContextJwkRsa); + return jwk; + case 'RSA-PSS': + jwk.alg = normalizeHashName( + key.algorithm.hash.name, + normalizeHashName.kContextJwkRsaPss); + return jwk; + case 'RSA-OAEP': + jwk.alg = normalizeHashName( + key.algorithm.hash.name, + normalizeHashName.kContextJwkRsaOaep); + return jwk; + case 'ECDSA': + // Fall through + case 'ECDH': + jwk.crv = key.algorithm.namedCurve; + return jwk; + case 'AES-CTR': + // Fall through + case 'AES-CBC': + // Fall through + case 'AES-GCM': + // Fall through + case 'AES-KW': + jwk.alg = lazyRequire('internal/crypto/aes') + .getAlgorithmName(key.algorithm.name, key.algorithm.length); + return jwk; + case 'HMAC': + jwk.alg = normalizeHashName( + key.algorithm.hash.name, + normalizeHashName.kContextJwkHmac); + return jwk; + case 'NODE-DSA': + jwk.alg = normalizeHashName( + key.algorithm.hash.name, + normalizeHashName.kContextJwkDsa); + return jwk; + default: + // Fall through + } + + throw lazyDOMException('Not yet supported', 'NotSupportedError'); +} + +async function exportKey(format, key) { + validateString(format, 'format'); + validateOneOf(format, 'format', kExportFormats); + if (!isCryptoKey(key)) + throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); + + if (!key.extractable) + throw lazyDOMException('key is not extractable', 'InvalidAccessException'); + + switch (format) { + case 'node.keyObject': return key[kKeyObject]; + case 'spki': return exportKeySpki(key); + case 'pkcs8': return exportKeyPkcs8(key); + case 'jwk': return exportKeyJWK(key); + case 'raw': return exportKeyRaw(key); + } + throw lazyDOMException( + 'Export format is unsupported', 'NotSupportedError'); +} + +async 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 'node.keyObject': { + if (!isKeyObject(keyData)) + throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData); + + if (keyData.type === 'secret') + return new InternalCryptoKey(keyData, { name }, keyUsages, extractable); + + break; + } + case 'raw': + if (hasAnyNotIn(usagesSet, 'deriveKey', 'deriveBits')) { + throw lazyDOMException( + `Unsupported key usage for a ${name} key`, + 'SyntaxError'); + } + + const checkLength = keyData.byteLength * 8; + + if (checkLength === 0 || 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 (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'); +} + +async function importKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + validateString(format, 'format'); + validateOneOf(format, 'format', kExportFormats); + if (format !== 'node.keyObject' && format !== 'jwk') + keyData = getArrayBufferOrView(keyData, 'keyData'); + algorithm = normalizeAlgorithm(algorithm); + validateBoolean(extractable, 'extractable'); + validateArray(keyUsages, 'keyUsages'); + switch (algorithm.name) { + case 'RSASSA-PKCS1-V1_5': + // Fall through + case 'RSA-PSS': + // Fall through + case 'RSA-OAEP': + return lazyRequire('internal/crypto/rsa') + .rsaImportKey(format, keyData, algorithm, extractable, keyUsages); + case 'ECDSA': + // Fall through + case 'ECDH': + return lazyRequire('internal/crypto/ec') + .ecImportKey(format, keyData, algorithm, extractable, keyUsages); + case 'HMAC': + return lazyRequire('internal/crypto/mac') + .hmacImportKey(format, keyData, algorithm, extractable, keyUsages); + case 'AES-CTR': + // Fall through + case 'AES-CBC': + // Fall through + case 'AES-GCM': + // Fall through + case 'AES-KW': + return lazyRequire('internal/crypto/aes') + .aesImportKey(algorithm, format, keyData, extractable, keyUsages); + case 'HKDF': + // Fall through + case 'NODE-SCRYPT': + // Fall through + case 'PBKDF2': + return importGenericSecretKey( + algorithm, + format, + keyData, + extractable, + keyUsages); + case 'NODE-DSA': + return lazyRequire('internal/crypto/dsa') + .dsaImportKey(format, keyData, algorithm, extractable, keyUsages); + case 'NODE-DH': + return lazyRequire('internal/crypto/diffiehellman') + .dhImportKey(format, keyData, algorithm, extractable, keyUsages); + } + + throw lazyDOMException('Unrecognized name.', 'NotSupportedError'); +} + +// subtle.wrapKey() is essentially a subtle.exportKey() followed +// by a subtle.encrypt(). +async function wrapKey(format, key, wrappingKey, algorithm) { + algorithm = normalizeAlgorithm(algorithm); + let keyData = await exportKey(format, key); + + if (format === 'jwk') { + if (keyData == null || typeof keyData !== 'object') + throw lazyDOMException('Invalid exported JWK key', 'DataError'); + const ec = new TextEncoder(); + const raw = JSONStringify(keyData); + keyData = ec.encode(raw + ' '.repeat(8 - (raw.length % 8))); + } + + return cipherOrWrap( + kWebCryptoCipherEncrypt, + algorithm, + wrappingKey, + keyData, + 'wrapKey'); +} + +// subtle.unwrapKey() is essentially a subtle.decrypt() followed +// by a subtle.importKey(). +async function unwrapKey( + format, + wrappedKey, + unwrappingKey, + unwrapAlgo, + unwrappedKeyAlgo, + extractable, + keyUsages) { + wrappedKey = getArrayBufferOrView(wrappedKey, 'wrappedKey'); + + let keyData = await cipherOrWrap( + kWebCryptoCipherDecrypt, + normalizeAlgorithm(unwrapAlgo), + unwrappingKey, + wrappedKey, + 'unwrapKey'); + + if (format === 'jwk') { + // The fatal: true option is only supported in builds that have ICU. + const options = process.versions.icu !== undefined ? + { fatal: true } : undefined; + const dec = new TextDecoder('utf-8', options); + try { + keyData = JSONParse(dec.decode(keyData)); + } catch { + throw lazyDOMException('Invalid imported JWK key', 'DataError'); + } + } + + return importKey(format, keyData, unwrappedKeyAlgo, extractable, keyUsages); +} + +function signVerify(algorithm, key, data, signature) { + algorithm = normalizeAlgorithm(algorithm); + if (!isCryptoKey(key)) + throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); + data = getArrayBufferOrView(data, 'data'); + let usage = 'sign'; + if (signature !== undefined) { + signature = getArrayBufferOrView(signature, 'signature'); + usage = 'verify'; + } + + if (!ArrayPrototypeIncludes(key.usages, usage) || + algorithm.name !== key.algorithm.name) { + throw lazyDOMException( + `Unable to use this key to ${usage}`, + 'InvalidAccessError'); + } + + switch (algorithm.name) { + case 'RSA-PSS': + // Fall through + case 'RSASSA-PKCS1-V1_5': + return lazyRequire('internal/crypto/rsa') + .rsaSignVerify(key, data, algorithm, signature); + case 'ECDSA': + return lazyRequire('internal/crypto/ec') + .ecdsaSignVerify(key, data, algorithm, signature); + case 'HMAC': + return lazyRequire('internal/crypto/mac') + .hmacSignVerify(key, data, algorithm, signature); + // The following are Node.js specific extensions. They must begin with + // the `NODE-` prefix + case 'NODE-DSA': + return lazyRequire('internal/crypto/dsa') + .dsaSignVerify(key, data, algorithm, signature); + } + throw lazyDOMException('Unrecognized named.', 'NotSupportedError'); +} + +async function sign(algorithm, key, data) { + return signVerify(algorithm, key, data); +} + +async function verify(algorithm, key, signature, data) { + return signVerify(algorithm, key, data, signature); +} + +async function cipherOrWrap(mode, algorithm, key, data, op) { + algorithm = normalizeAlgorithm(algorithm); + // We use a Node.js style error here instead of a DOMException because + // the WebCrypto spec is not specific what kind of error is to be thrown + // in this case. Both Firefox and Chrome throw simple TypeErrors here. + if (!isCryptoKey(key)) + throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); + // The key algorithm and cipher algorithm must match, and the + // key must have the proper usage. + if (key.algorithm.name !== algorithm.name || + !ArrayPrototypeIncludes(key.usages, op)) { + throw lazyDOMException( + 'The requested operation is not valid for the provided key', + 'InvalidAccessError'); + } + + // For the Web Crypto API, the input data can be any ArrayBuffer, + // TypedArray, or DataView. + data = getArrayBufferOrView(data, 'data'); + + // While WebCrypto allows for larger input buffer sizes, we limit + // those to sizes that can fit within uint32_t because of limitations + // in the OpenSSL API. + validateMaxBufferLength(data, 'data'); + + switch (algorithm.name) { + case 'RSA-OAEP': + return lazyRequire('internal/crypto/rsa') + .rsaCipher(mode, key, data, algorithm); + case 'AES-CTR': + // Fall through + case 'AES-CBC': + // Fall through + case 'AES-GCM': + return lazyRequire('internal/crypto/aes') + .aesCipher(mode, key, data, algorithm); + case 'AES-KW': + if (op === 'wrapKey' || op === 'unwrapKey') { + return lazyRequire('internal/crypto/aes') + .aesCipher(mode, key, data, algorithm); + } + } + throw lazyDOMException('Unrecognized name.', 'NotSupportedError'); +} + +async function encrypt(algorithm, key, data) { + return cipherOrWrap(kWebCryptoCipherEncrypt, algorithm, key, data, 'encrypt'); +} + +async function decrypt(algorithm, key, data) { + 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 Crypto {} +const crypto = new Crypto(); + +ObjectDefineProperties( + Crypto.prototype, { + [SymbolToStringTag]: { + enumerable: false, + configurable: true, + writable: false, + value: 'Crypto', + }, + subtle: { + enumerable: true, + configurable: false, + value: subtle, + }, + getRandomValues: { + enumerable: true, + configurable: true, + writable: true, + value: getRandomValues, + }, + CryptoKey: { + enumerable: true, + configurable: true, + writable: true, + value: CryptoKey, + } + }); + +ObjectDefineProperties( + SubtleCrypto.prototype, { + [SymbolToStringTag]: { + enumerable: false, + configurable: true, + writable: false, + value: 'SubtleCrypto', + }, + encrypt: { + enumerable: true, + configurable: true, + writable: true, + value: encrypt, + }, + decrypt: { + enumerable: true, + configurable: true, + writable: true, + value: decrypt, + }, + sign: { + enumerable: true, + configurable: true, + writable: true, + value: sign, + }, + verify: { + enumerable: true, + configurable: true, + writable: true, + value: verify, + }, + digest: { + enumerable: true, + configurable: true, + writable: true, + value: asyncDigest, + }, + generateKey: { + enumerable: true, + configurable: true, + writable: true, + value: generateKey, + }, + deriveKey: { + enumerable: true, + configurable: true, + writable: true, + value: deriveKey, + }, + deriveBits: { + enumerable: true, + configurable: true, + writable: true, + value: deriveBits, + }, + importKey: { + enumerable: true, + configurable: true, + writable: true, + value: importKey, + }, + exportKey: { + enumerable: true, + configurable: true, + writable: true, + value: exportKey, + }, + wrapKey: { + enumerable: true, + configurable: true, + writable: true, + value: wrapKey, + }, + unwrapKey: { + enumerable: true, + configurable: true, + writable: true, + value: unwrapKey, + } + }); + +module.exports = crypto; diff --git a/lib/internal/per_context/primordials.js b/lib/internal/per_context/primordials.js index 8b1b8841c25379..906985c82a3ebd 100644 --- a/lib/internal/per_context/primordials.js +++ b/lib/internal/per_context/primordials.js @@ -123,6 +123,7 @@ primordials.SafePromise = makeSafe( 'BigInt64Array', 'BigUint64Array', 'Boolean', + 'DataView', 'Date', 'Error', 'EvalError', diff --git a/node.gyp b/node.gyp index 59a92e02e00d05..c68af49dab0442 100644 --- a/node.gyp +++ b/node.gyp @@ -120,17 +120,25 @@ 'lib/internal/cluster/worker.js', 'lib/internal/console/constructor.js', 'lib/internal/console/global.js', + 'lib/internal/crypto/aes.js', 'lib/internal/crypto/certificate.js', 'lib/internal/crypto/cipher.js', 'lib/internal/crypto/diffiehellman.js', + 'lib/internal/crypto/dsa.js', + 'lib/internal/crypto/ec.js', 'lib/internal/crypto/hash.js', + 'lib/internal/crypto/hashnames.js', + 'lib/internal/crypto/hkdf.js', 'lib/internal/crypto/keygen.js', 'lib/internal/crypto/keys.js', + 'lib/internal/crypto/mac.js', 'lib/internal/crypto/pbkdf2.js', 'lib/internal/crypto/random.js', + 'lib/internal/crypto/rsa.js', 'lib/internal/crypto/scrypt.js', 'lib/internal/crypto/sig.js', 'lib/internal/crypto/util.js', + 'lib/internal/crypto/webcrypto.js', 'lib/internal/constants.js', 'lib/internal/dgram.js', 'lib/internal/dns/promises.js', @@ -902,14 +910,54 @@ } ], [ 'node_use_openssl=="true"', { 'sources': [ - 'src/crypto/crypto_common.cc', + 'src/crypto/crypto_aes.cc', 'src/crypto/crypto_bio.cc', + 'src/crypto/crypto_common.cc', + 'src/crypto/crypto_dsa.cc', + 'src/crypto/crypto_hkdf.cc', + 'src/crypto/crypto_pbkdf2.cc', + 'src/crypto/crypto_sig.cc', + 'src/crypto/crypto_timing.cc', + 'src/crypto/crypto_cipher.cc', + 'src/crypto/crypto_context.cc', + 'src/crypto/crypto_ecdh.cc', + 'src/crypto/crypto_hmac.cc', + 'src/crypto/crypto_random.cc', + 'src/crypto/crypto_rsa.cc', + 'src/crypto/crypto_spkac.cc', + 'src/crypto/crypto_util.cc', 'src/crypto/crypto_clienthello.cc', - 'src/crypto/crypto_common.h', + 'src/crypto/crypto_dh.cc', + 'src/crypto/crypto_hash.cc', + 'src/crypto/crypto_keys.cc', + 'src/crypto/crypto_keygen.cc', + 'src/crypto/crypto_scrypt.cc', + 'src/crypto/crypto_ssl.cc', + 'src/crypto/crypto_aes.cc', 'src/crypto/crypto_bio.h', - 'src/crypto/crypto_clienthello.h', 'src/crypto/crypto_clienthello-inl.h', + 'src/crypto/crypto_dh.h', 'src/crypto/crypto_groups.h', + 'src/crypto/crypto_hmac.h', + 'src/crypto/crypto_rsa.h', + 'src/crypto/crypto_spkac.h', + 'src/crypto/crypto_util.h', + 'src/crypto/crypto_cipher.h', + 'src/crypto/crypto_common.h', + 'src/crypto/crypto_dsa.h', + 'src/crypto/crypto_hash.h', + 'src/crypto/crypto_keys.h', + 'src/crypto/crypto_keygen.h', + 'src/crypto/crypto_scrypt.h', + 'src/crypto/crypto_ssl.h', + 'src/crypto/crypto_clienthello.h', + 'src/crypto/crypto_context.h', + 'src/crypto/crypto_ecdh.h', + 'src/crypto/crypto_hkdf.h', + 'src/crypto/crypto_pbkdf2.h', + 'src/crypto/crypto_sig.h', + 'src/crypto/crypto_random.h', + 'src/crypto/crypto_timing.h', 'src/node_crypto.cc', 'src/node_crypto.h', 'src/tls_wrap.cc', diff --git a/src/async_wrap.h b/src/async_wrap.h index f0e49c3df28d94..1a82a5bfce846c 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -85,9 +85,16 @@ namespace node { #define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) \ V(PBKDF2REQUEST) \ V(KEYPAIRGENREQUEST) \ + V(KEYGENREQUEST) \ + V(KEYEXPORTREQUEST) \ + V(CIPHERREQUEST) \ + V(DERIVEBITSREQUEST) \ + V(HASHREQUEST) \ V(RANDOMBYTESREQUEST) \ V(SCRYPTREQUEST) \ - V(TLSWRAP) + V(SIGNREQUEST) \ + V(TLSWRAP) \ + V(VERIFYREQUEST) #else #define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) #endif // HAVE_OPENSSL diff --git a/src/base64-inl.h b/src/base64-inl.h index 57c11bde2d7af2..650218a0481085 100644 --- a/src/base64-inl.h +++ b/src/base64-inl.h @@ -119,12 +119,13 @@ size_t base64_decode(char* const dst, const size_t dstlen, inline size_t base64_encode(const char* src, size_t slen, char* dst, - size_t dlen) { + size_t dlen, + Base64Mode mode) { // We know how much we'll write, just make sure that there's space. - CHECK(dlen >= base64_encoded_size(slen) && + CHECK(dlen >= base64_encoded_size(slen, mode) && "not enough space provided for base64 encode"); - dlen = base64_encoded_size(slen); + dlen = base64_encoded_size(slen, mode); unsigned a; unsigned b; @@ -133,9 +134,7 @@ inline size_t base64_encode(const char* src, unsigned k; unsigned n; - static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/"; + const char* table = base64_select_table(mode); i = 0; k = 0; @@ -160,8 +159,10 @@ inline size_t base64_encode(const char* src, a = src[i + 0] & 0xff; dst[k + 0] = table[a >> 2]; dst[k + 1] = table[(a & 3) << 4]; - dst[k + 2] = '='; - dst[k + 3] = '='; + if (mode == Base64Mode::NORMAL) { + dst[k + 2] = '='; + dst[k + 3] = '='; + } break; case 2: a = src[i + 0] & 0xff; @@ -169,7 +170,8 @@ inline size_t base64_encode(const char* src, dst[k + 0] = table[a >> 2]; dst[k + 1] = table[((a & 3) << 4) | (b >> 4)]; dst[k + 2] = table[(b & 0x0f) << 2]; - dst[k + 3] = '='; + if (mode == Base64Mode::NORMAL) + dst[k + 3] = '='; break; } diff --git a/src/base64.h b/src/base64.h index e58baa5f6e4692..cf6e82539a5f91 100644 --- a/src/base64.h +++ b/src/base64.h @@ -5,13 +5,40 @@ #include "util.h" +#include #include #include namespace node { //// Base 64 //// -static inline constexpr size_t base64_encoded_size(size_t size) { - return ((size + 2) / 3 * 4); + +enum class Base64Mode { + NORMAL, + URL +}; + +static constexpr char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +static constexpr char base64_table_url[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789-_"; + +static inline const char* base64_select_table(Base64Mode mode) { + switch (mode) { + case Base64Mode::NORMAL: return base64_table; + case Base64Mode::URL: return base64_table_url; + default: UNREACHABLE(); + } +} + +static inline constexpr size_t base64_encoded_size( + size_t size, + Base64Mode mode = Base64Mode::NORMAL) { + return mode == Base64Mode::NORMAL + ? ((size + 2) / 3 * 4) + : std::ceil(static_cast(size * 4) / 3); } // Doesn't check for padding at the end. Can be 1-2 bytes over. @@ -32,7 +59,8 @@ size_t base64_decode(char* const dst, const size_t dstlen, inline size_t base64_encode(const char* src, size_t slen, char* dst, - size_t dlen); + size_t dlen, + Base64Mode mode = Base64Mode::NORMAL); } // namespace node diff --git a/src/base_object.h b/src/base_object.h index 357a7e40db5ed1..54ebeb52041738 100644 --- a/src/base_object.h +++ b/src/base_object.h @@ -101,7 +101,7 @@ class BaseObject : public MemoryRetainer { // This is a bit of a hack. See the override in async_wrap.cc for details. virtual bool IsDoneInitializing() const; - // Can be used to avoid this object keepling itself alive as a GC root + // Can be used to avoid this object keeping itself alive as a GC root // indefinitely, for example when this object is owned and deleted by another // BaseObject once that is torn down. This can only be called when there is // a BaseObjectPtr to this object. diff --git a/src/crypto/README.md b/src/crypto/README.md new file mode 100644 index 00000000000000..3317334cd71d3c --- /dev/null +++ b/src/crypto/README.md @@ -0,0 +1,349 @@ +# Node.js src/crypto Documentation + +Welcome. You've found your way to the Node.js native crypto subsystem. + +Do not be afraid. + +While crypto may be a dark, mysterious, and forboding subject; and while +this directory may be filled with many `*.h` and `*.cc` files, finding +your way around is not too difficult. And I can promise you that a Gru +will not jump out of the shadows and eat you (well, "promise" may be a +bit too strong, a Gru may jump out of the shadows and eat you if you +live in a place where such things are possible). + +## Finding your way around + +All of the code in this directory is structured into units organized by +function or crypto protocol. + +The following provide generalized utility declarations that are used throughout +the various other crypto files and other parts of Node.js: + +* `crypto_util.h` / `crypto_util.cc` (Core crypto definitions) +* `crypto_common.h` / `crypto_common.h` (Shared TLS utility functions) +* `crypto_bio.c` / `crypto_bio.c` (Custom OpenSSL i/o implementation) +* `crypto_groups.h` (modp group definitions) + +Of these, `crypto_util.h` and `crypto_util.cc` are the most important, as +they provide the core declarations and utility functions used most extensively +throughout the rest of the code. + +The rest of the files are structured by their function, as detailed in the +following table: + +| File (*.h/*.cc) | Description | +| -------------------- | ----------- | +| `crypto_aes` | AES Cipher support. | +| `crypto_cipher` | General Encryption/Decryption utilities. | +| `crypto_clienthello` | TLS/SSL client hello parser implementation. Used during SSL/TLS handshake. | +| `crypto_context` | Implementation of the `SecureContext` object. | +| `crypto_dh` | Diffie-Hellman Key Agreement implementation. | +| `crypto_dsa` | DSA (Digital Signature) Key Generation functions. | +| `crypto_ecdh` | Elliptic-Curve Diffie-Hellman Key Agreement implementation. | +| `crypto_hash` | Basic hash (e.g. SHA-256) functions. | +| `crypto_hkdf` | HKDF (Key derivation) implementation. | +| `crypto_hmac` | HMAC implementations. | +| `crypto_keys` | Utilities for using and generating secret, private, and public keys. | +| `crypto_pbkdf2` | PBKDF2 key / bit generation implementation. | +| `crypto_rsa` | RSA Key Generation functions. | +| `crypto_scrypt` | Scrypt key / bit generation implementation. | +| `crypto_sig` | General digital signature and verification utilities. | +| `crypto_spkac` | Netscape SPKAC certificate utilities. | +| `crypto_ssl` | Implementation of the `SSLWrap` object. | +| `crypto_timing` | Implementation of the TimingSafeEqual. | + +When new crypto protocols are added, they will be added into their own +`crypto_` `*.h` and `*.cc` files. + +## Helpful concepts + +Node.js currently uses OpenSSL to provide it's crypto substructure. +(Some custom Node.js distributions -- such as Electron -- use BoringSSL +instead.) + +This section aims to explain some of the utilities that have been +provided to make working with the OpenSSL APIs a bit easier. + +### Pointer Types + +Most of the key OpenSSL types need to be explicitly freed when they are +no longer needed. Failure to do so introduces memory leaks. To make this +easier (and less error prone), the `crypto_util.h` defines a number of +smart-pointer aliases that should be used: + +```cpp +using X509Pointer = DeleteFnPtr; +using BIOPointer = DeleteFnPtr; +using SSLCtxPointer = DeleteFnPtr; +using SSLSessionPointer = DeleteFnPtr; +using SSLPointer = DeleteFnPtr; +using PKCS8Pointer = DeleteFnPtr; +using EVPKeyPointer = DeleteFnPtr; +using EVPKeyCtxPointer = DeleteFnPtr; +using EVPMDPointer = DeleteFnPtr; +using RSAPointer = DeleteFnPtr; +using ECPointer = DeleteFnPtr; +using BignumPointer = DeleteFnPtr; +using NetscapeSPKIPointer = DeleteFnPtr; +using ECGroupPointer = DeleteFnPtr; +using ECPointPointer = DeleteFnPtr; +using ECKeyPointer = DeleteFnPtr; +using DHPointer = DeleteFnPtr; +using ECDSASigPointer = DeleteFnPtr; +using HMACCtxPointer = DeleteFnPtr; +using CipherCtxPointer = DeleteFnPtr; +``` + +Examples of these being used are pervasive through the `src/crypto` code. + +### `ByteSource` + +The `ByteSource` class is a helper utility representing a *read-only* byte +array. Instances can either wrap external ("foreign") data sources, such as +an `ArrayBuffer` (`v8::BackingStore`) or allocated data. If allocated data +is used, then the allocation is freed automatically when the `ByteSource` is +destroyed. + +### `ArrayBufferOrViewContents` + +The `ArrayBufferOfViewContents` class is a helper utility that abstracts +`ArrayBuffer`, `TypedArray`, or `DataView` inputs and provides access to +their underlying data pointers. It is used extensively through `src/crypto` +to make it easier to deal with inputs that allow any `ArrayBuffer`-backed +object. + +### `AllocatedBuffer` + +The `AllocatedBuffer` utility is defined in `allocated_buffer.h` and is not +specific to `src/crypto`. It is used extensively within `src/crypto` to hold +allocated data that is intended to be output in response to various +crypto functions (generated hash values, or ciphertext, for instance). + +*Currently, we are working to transition away from using `AllocatedBuffer` +to directly using the `v8::BackingStore` API. This will take some time. +New uses of `AllocatedBuffer` should be avoided if possible.* + +### Key Objects + +Most crypto operations involve the use of keys -- cryptographic inputs +that protect data. There are three general types of keys: + +* Secret Keys (Symmetric) +* Public Keys (Asymmetric) +* Private Keys (Asymmetric) + +Secret keys consist of a variable number of bytes. They are "symmetrical" +in that the same key used to encrypt data, or generate a signature, must +be used to decrypt or validate that signature. If two people are exchanging +messages encrypted using a secret key, both of them must have access to the +same secret key data. + +Public and Private keys always come in pairs. When one is used to encrypt +data or generate a signature, the other is used to decrypt or validate the +signature. The Public key is intended to be shared and can be shared openly. +The Private key must be kept secret and known only to the owner of the key. + +The `src/crypto` subsystem uses several objects to represent keys. These +objects are structured in a way to allow key data to be shared across +multiple threads (the Node.js main thread, Worker Threads, and the libuv +threadpool). + +Refer to `crypto_keys.h` and `crypto_keys.cc` for all code relating to the +core key objects. + +#### `ManagedEVPPKey` + +The `ManagedEVPPKey` class is a smart pointer for OpenSSL `EVP_PKEY` +structures. These manage the lifecycle of Public and Private key pairs. + +#### `KeyObjectData` + +`KeyObjectData` is an internal thread-safe structure used to wrap either +a `ManagedEVPPKey` (for Public or Private keys) or a `ByteSource` containing +a Secret key. + +#### `KeyObjectHandle` + +The `KeyObjectHandle` provides the interface between the native C++ code +handling keys and the public JavaScript `KeyObject` API. + +#### `KeyObject` + +A `KeyObject` is the public Node.js-specific API for keys. A single +`KeyObject` wraps exactly one `KeyObjectHandle`. + +#### `CryptoKey` + +A `CryptoKey` is the Web Crypto API's alternative to `KeyObject`. In the +Node.js implementation, `CryptoKey` is a thin wrapper around the +`KeyObject` and it is largely possible to use them interchangeably. + +### `CryptoJob` + +All operations that are not either Stream-based or single-use functions +are built around the `CryptoJob` class. + +A `CryptoJob` encapsulates a single crypto operation that can be +invoked synchronously or asynchronously. + +The `CryptoJob` class itself is a C++ template that takes a single +`CryptoJobTraits` struct as a parameter. The `CryptoJobTraits` +provides the implementation detail of the job. + +There are (currently) four basic `CryptoJob` specializations: + +* `CipherJob` (defined in `src/crypto_cipher.h`) -- Used for + encrypt and decrypt operations. +* `KeyGenJob` (defined in `src/crypto_keygen.h`) -- Used for + secret and key pair generation operations. +* `KeyExportJob` (defined in `src/crypto_keys.h`) -- Used for + key export operations. +* `DeriveBitsJob` (defined in `src/crypto_util.h`) -- Used for + key and byte derivation operations. + +Every `CryptoJobTraits` provides two fundamental operations: + +* Configuration -- Processes input arguments when a + `CryptoJob` instance is created. +* Implementation -- Provides the specific implementation of + the operation. + +The Configuration is typically provided by an `AdditionalConfig()` +method, the signature of which is slightly different for each +of the above `CryptoJob` specializations. Despite the signature +differences, the purpose of the `AdditionalConfig()` function +remains the same: to process input arguments and set the properties +on the `CryptoJob`'s parameters object. + +The parameters object is specific to each `CryptoJob` type, and +is stored with the `CryptoJob`. It holds all of the inputs that +are used by the Implementation. The inputs held by the parameters +must be threadsafe. + +The `AdditionalConfig()` function is always called when the +`CryptoJob` instance is being created. + +The Implementation function is unique to each of the `CryptoJob` +specializations and will either be called synchronously within +the current thread or from within the libuv threadpool. + +Every `CryptoJob` instance exposes a `run()` function to the +JavaScript layer. When called, `run()` with either dispatch the +job to the libuv threadpool or invoke the Implementation +function synchronously. If invoked synchronously, run() will +return a JavaScript array. The first value in the array is +either an `Error` or `undefined`. If the operation was successful, +the second value in the array will contain the result of the +operation. Typically, the result is an `ArrayBuffer`, but +certain `CryptoJob` types can alter the output. + +If the `CryptoJob` is processed asynchronously, then the job +must have an `ondone` property whose value is a function that +is invoked when the operation is complete. This function will +be called with two arguments. The first is either an `Error` +or `undefined`, and the second is the result of the operation +if successful. + +For `CipherJob` types, the output is always an `ArrayBuffer`. + +For `KeyExportJob` types, the output is either an `ArrayBuffer` or +a JavaScript object (for JWK output format); + +For `KeyGenJob` types, the output is either a single KeyObject, +or an array containing a Public/Private key pair represented +either as a `KeyObjectHandle` object or a `Buffer`. + +For `DeriveBitsJob` type output is typically an `ArrayBuffer` but +can be other values (`RandomBytesJob` for instance, fills an +input buffer and always returns `undefined`). + +### Errors + +#### `ThrowCryptoError` and the `THROW_ERR_CRYPTO_*` macros + +The `ThrowCryptoError()` is a legacy utility that will throw a +JavaScript exception containing details collected from OpenSSL +about a failed operation. `ThrowCryptoError()` should only be +used when necessary to report low-level OpenSSL failures. + +In `node_errors.h`, there are a number of `ERR_CRYPTO_*` +macro definitions that define semantically specific errors. +These can be called from within the C++ code as functions, +like `THROW_ERR_CRYPTO_INVALID_IV(env)`. These methods +should be used to throw JavaScript errors when necessary. + +## Crypto API Patterns + +### Operation Mode + +All crypto functions in Node.js operate in one of three +modes: + +* Synchronous single-call +* Asynchronous single-call +* Stream-oriented + +It is often possible to perform various operations across +multiple modes. For instance, cipher and decipher operations +can be performed in any of the three modes. + +Synchronous single-call operations are always blocking. +They perform their actions immediately. + +```js +// Example synchronous single-call operation +const a = new Uint8Array(10); +const b = new Uint8Array(10); +crypto.timingSafeEqual(a, b); +``` + +Asynchronous single-call operations generally perform a +number of synchronous input validation steps, but then +defer the actual crypto-operation work to the libuv threadpool. + +```js +// Example asynchronous single-call operation +const buf = new Uint8Array(10); +crypto.randomFill(buf, (err, buf) => { + console.log(buf); +}); +``` + +For the legacy Node.js crypto API, asynchronous single-call +operations use the traditional Node.js callback pattern, as +illustrated in the previous `randomFill()` example. In the +Web Crypto API (accessible via `require('crypto').webcrypto`), +all asynchronous single-call operations are Promise-based. + +```js +// Example Web Crypto API asynchronous single-call operation +const { subtle } = require('crypto').webcrypto; + +subtle.generateKeys({ name: 'HMAC', length: 256 }, true, ['sign']) + .then((key) => { + console.log(key); + }) + .catch((error) => { + console.error('an error occurred'); + }); +``` + +In nearly every case, asynchronous single-call operations make use +of the libuv threadpool to perform crypto operations off the main +event loop thread. + +Stream-oriented operations use an object to maintain state +over multiple individual synchronous steps. The steps themselves +can be performed over time. + +```js +// Example stream-oriented operation +const hash = crypto.createHash('sha256'); +let updates = 10; +setTimeout(() => { + hash.update('hello world'); + setTimeout(() => { + console.log(hash.digest();) + }, 1000); +}, 1000); +``` diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc new file mode 100644 index 00000000000000..889bb487fa04d5 --- /dev/null +++ b/src/crypto/crypto_aes.cc @@ -0,0 +1,597 @@ +#include "crypto/crypto_aes.h" +#include "crypto/crypto_cipher.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +#include +#include + +#include + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Object; +using v8::Uint32; +using v8::Value; + +namespace crypto { +namespace { +// Implements general AES encryption and decryption for CBC +// The key_data must be a secret key. +// On success, this function sets out to a new AllocatedBuffer +// instance containing the results and returns WebCryptoCipherStatus::ERR_OK. +WebCryptoCipherStatus AES_Cipher( + Environment* env, + KeyObjectData* key_data, + WebCryptoCipherMode cipher_mode, + const AESCipherConfig& params, + const ByteSource& in, + ByteSource* out) { + CHECK_NOT_NULL(key_data); + CHECK_EQ(key_data->GetKeyType(), kKeyTypeSecret); + + const int mode = EVP_CIPHER_mode(params.cipher); + + CipherCtxPointer ctx(EVP_CIPHER_CTX_new()); + EVP_CIPHER_CTX_init(ctx.get()); + if (mode == EVP_CIPH_WRAP_MODE) + EVP_CIPHER_CTX_set_flags(ctx.get(), EVP_CIPHER_CTX_FLAG_WRAP_ALLOW); + + const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; + + if (!EVP_CipherInit_ex( + ctx.get(), + params.cipher, + nullptr, + nullptr, + nullptr, + encrypt)) { + // Cipher init failed + return WebCryptoCipherStatus::ERR_FAILED; + } + + if (mode == EVP_CIPH_GCM_MODE && !EVP_CIPHER_CTX_ctrl( + ctx.get(), + EVP_CTRL_AEAD_SET_IVLEN, + params.iv.size(), + nullptr)) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + if (!EVP_CIPHER_CTX_set_key_length( + ctx.get(), + key_data->GetSymmetricKeySize()) || + !EVP_CipherInit_ex( + ctx.get(), + nullptr, + nullptr, + reinterpret_cast(key_data->GetSymmetricKey()), + params.iv.data(), + encrypt)) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + size_t tag_len = 0; + + if (mode == EVP_CIPH_GCM_MODE) { + switch (cipher_mode) { + case kWebCryptoCipherDecrypt: + // If in decrypt mode, the auth tag must be set in the params.tag. + CHECK(params.tag); + if (!EVP_CIPHER_CTX_ctrl( + ctx.get(), + EVP_CTRL_AEAD_SET_TAG, + params.tag.size(), + const_cast(params.tag.get()))) { + return WebCryptoCipherStatus::ERR_FAILED; + } + break; + case kWebCryptoCipherEncrypt: + // In decrypt mode, we grab the tag length here. We'll use it to + // ensure that that allocated buffer has enough room for both the + // final block and the auth tag. Unlike our other AES-GCM implementation + // in CipherBase, in WebCrypto, the auth tag is concatentated to the end + // of the generated ciphertext and returned in the same ArrayBuffer. + tag_len = params.length; + break; + default: + UNREACHABLE(); + } + } + + size_t total = 0; + int buf_len = in.size() + EVP_CIPHER_CTX_block_size(ctx.get()) + tag_len; + int out_len; + + if (mode == EVP_CIPH_GCM_MODE && + params.additional_data.size() && + !EVP_CipherUpdate( + ctx.get(), + nullptr, + &out_len, + params.additional_data.data(), + params.additional_data.size())) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + char* data = MallocOpenSSL(buf_len); + ByteSource buf = ByteSource::Allocated(data, buf_len); + unsigned char* ptr = reinterpret_cast(data); + + if (!EVP_CipherUpdate( + ctx.get(), + ptr, + &out_len, + in.data(), + in.size())) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + total += out_len; + CHECK_LE(out_len, buf_len); + ptr += out_len; + out_len = EVP_CIPHER_CTX_block_size(ctx.get()); + if (!EVP_CipherFinal_ex(ctx.get(), ptr, &out_len)) { + return WebCryptoCipherStatus::ERR_FAILED; + } + total += out_len; + + // If using AES_GCM, grab the generated auth tag and append + // it to the end of the ciphertext. + if (cipher_mode == kWebCryptoCipherEncrypt && mode == EVP_CIPH_GCM_MODE) { + data += out_len; + if (!EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_GET_TAG, tag_len, ptr)) + return WebCryptoCipherStatus::ERR_FAILED; + total += tag_len; + } + + // It's possible that we haven't used the full allocated space. Size down. + buf.Resize(total); + *out = std::move(buf); + + return WebCryptoCipherStatus::ERR_OK; +} + +// The AES_CTR implementation here takes it's inspiration from the chromium +// implementation here: +// https://github.com/chromium/chromium/blob/7af6cfd/components/webcrypto/algorithms/aes_ctr.cc + +template +T CeilDiv(T a, T b) { + return a == 0 ? 0 : 1 + (a - 1) / b; +} + +BignumPointer GetCounter(const AESCipherConfig& params) { + unsigned int remainder = (params.length % CHAR_BIT); + const unsigned char* data = params.iv.data(); + + if (remainder == 0) { + unsigned int byte_length = params.length / CHAR_BIT; + return BignumPointer(BN_bin2bn( + data + params.iv.size() - byte_length, + byte_length, + nullptr)); + } + + unsigned int byte_length = + CeilDiv(params.length, static_cast(CHAR_BIT)); + + std::vector counter( + data + params.iv.size() - byte_length, + data + params.iv.size()); + counter[0] &= ~(0xFF << remainder); + + return BignumPointer(BN_bin2bn(counter.data(), counter.size(), nullptr)); +} + +std::vector BlockWithZeroedCounter( + const AESCipherConfig& params) { + unsigned int length_bytes = params.length / CHAR_BIT; + unsigned int remainder = params.length % CHAR_BIT; + + const unsigned char* data = params.iv.data(); + + std::vector new_counter_block(data, data + params.iv.size()); + + size_t index = new_counter_block.size() - length_bytes; + memset(&new_counter_block.front() + index, 0, length_bytes); + + if (remainder) + new_counter_block[index - 1] &= 0xFF << remainder; + + return new_counter_block; +} + +WebCryptoCipherStatus AES_CTR_Cipher2( + KeyObjectData* key_data, + WebCryptoCipherMode cipher_mode, + const AESCipherConfig& params, + const ByteSource& in, + unsigned const char* counter, + unsigned char* out) { + CipherCtxPointer ctx(EVP_CIPHER_CTX_new()); + const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; + + if (!EVP_CipherInit_ex( + ctx.get(), + params.cipher, + nullptr, + reinterpret_cast(key_data->GetSymmetricKey()), + counter, + encrypt)) { + // Cipher init failed + return WebCryptoCipherStatus::ERR_FAILED; + } + + int out_len = 0; + int final_len = 0; + if (!EVP_CipherUpdate( + ctx.get(), + out, + &out_len, + in.data(), + in.size())) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + if (!EVP_CipherFinal_ex(ctx.get(), out + out_len, &final_len)) + return WebCryptoCipherStatus::ERR_FAILED; + + out_len += final_len; + if (static_cast(out_len) != in.size()) + return WebCryptoCipherStatus::ERR_FAILED; + + return WebCryptoCipherStatus::ERR_OK; +} + +WebCryptoCipherStatus AES_CTR_Cipher( + Environment* env, + KeyObjectData* key_data, + WebCryptoCipherMode cipher_mode, + const AESCipherConfig& params, + const ByteSource& in, + ByteSource* out) { + BignumPointer num_counters(BN_new()); + if (!BN_lshift(num_counters.get(), BN_value_one(), params.length)) + return WebCryptoCipherStatus::ERR_FAILED; + + BignumPointer current_counter = GetCounter(params); + + BignumPointer num_output(BN_new()); + + if (!BN_set_word(num_output.get(), CeilDiv(in.size(), kAesBlockSize))) + return WebCryptoCipherStatus::ERR_FAILED; + + // Just like in chromium's implementation, if the counter will + // be incremented more than there are counter values, we fail. + if (BN_cmp(num_output.get(), num_counters.get()) > 0) + return WebCryptoCipherStatus::ERR_FAILED; + + BignumPointer remaining_until_reset(BN_new()); + if (!BN_sub(remaining_until_reset.get(), + num_counters.get(), + current_counter.get())) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + // Output size is identical to the input size + char* data = MallocOpenSSL(in.size()); + ByteSource buf = ByteSource::Allocated(data, in.size()); + unsigned char* ptr = reinterpret_cast(data); + + // Also just like in chromium's implementation, if we can process + // the input without wrapping the counter, we'll do it as a single + // call here. If we can't, we'll fallback to the a two-step approach + if (BN_cmp(remaining_until_reset.get(), num_output.get()) >= 0) { + auto status = AES_CTR_Cipher2( + key_data, + cipher_mode, + params, + in, + params.iv.data(), + ptr); + if (status == WebCryptoCipherStatus::ERR_OK) + *out = std::move(buf); + return status; + } + + BN_ULONG blocks_part1 = BN_get_word(remaining_until_reset.get()); + BN_ULONG input_size_part1 = blocks_part1 * kAesBlockSize; + + // Encrypt the first part... + auto status = AES_CTR_Cipher2( + key_data, + cipher_mode, + params, + ByteSource::Foreign(in.get(), input_size_part1), + params.iv.data(), + ptr); + + if (status != WebCryptoCipherStatus::ERR_OK) + return status; + + // Wrap the counter around to zero + std::vector new_counter_block = BlockWithZeroedCounter(params); + + // Encrypt the second part... + status = AES_CTR_Cipher2( + key_data, + cipher_mode, + params, + ByteSource::Foreign( + in.get() + input_size_part1, + in.size() - input_size_part1), + new_counter_block.data(), + ptr + input_size_part1); + + if (status == WebCryptoCipherStatus::ERR_OK) + *out = std::move(buf); + + return status; +} + +bool ValidateIV( + Environment* env, + CryptoJobMode mode, + Local value, + AESCipherConfig* params) { + ArrayBufferOrViewContents iv(value); + if (UNLIKELY(!iv.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "iv is too big"); + return false; + } + params->iv = (mode == kCryptoJobAsync) + ? iv.ToCopy() + : iv.ToByteSource(); + return true; +} + +bool ValidateCounter( + Environment* env, + Local value, + AESCipherConfig* params) { + CHECK(value->IsUint32()); // Length + params->length = value.As()->Value(); + if (params->iv.size() != 16 || + params->length == 0 || + params->length > 128) { + THROW_ERR_CRYPTO_INVALID_COUNTER(env); + return false; + } + return true; +} + +bool ValidateAuthTag( + Environment* env, + CryptoJobMode mode, + WebCryptoCipherMode cipher_mode, + Local value, + AESCipherConfig* params) { + switch (cipher_mode) { + case kWebCryptoCipherDecrypt: { + if (!IsAnyByteSource(value)) { + THROW_ERR_CRYPTO_INVALID_TAG_LENGTH(env); + return false; + } + ArrayBufferOrViewContents tag_contents(value); + if (UNLIKELY(!tag_contents.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "tagLength is too big"); + return false; + } + params->tag = mode == kCryptoJobAsync + ? tag_contents.ToCopy() + : tag_contents.ToByteSource(); + break; + } + case kWebCryptoCipherEncrypt: { + if (!value->IsUint32()) { + THROW_ERR_CRYPTO_INVALID_TAG_LENGTH(env); + return false; + } + params->length = value.As()->Value(); + if (params->length > 128) { + THROW_ERR_CRYPTO_INVALID_TAG_LENGTH(env); + return false; + } + break; + } + default: + UNREACHABLE(); + } + return true; +} + +bool ValidateAdditionalData( + Environment* env, + CryptoJobMode mode, + Local value, + AESCipherConfig* params) { + // Additional Data + if (IsAnyByteSource(value)) { + ArrayBufferOrViewContents additional(value); + if (UNLIKELY(!additional.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "additionalData is too big"); + return false; + } + params->additional_data = mode == kCryptoJobAsync + ? additional.ToCopy() + : additional.ToByteSource(); + } + return true; +} + +void UseDefaultIV(AESCipherConfig* params) { + params->iv = ByteSource::Foreign(kDefaultWrapIV, strlen(kDefaultWrapIV)); +} +} // namespace + +AESCipherConfig::AESCipherConfig(AESCipherConfig&& other) noexcept + : mode(other.mode), + variant(other.variant), + cipher(other.cipher), + length(other.length), + iv(std::move(other.iv)), + additional_data(std::move(other.additional_data)), + tag(std::move(other.tag)) {} + +AESCipherConfig& AESCipherConfig::operator=(AESCipherConfig&& other) noexcept { + if (&other == this) return *this; + this->~AESCipherConfig(); + return *new (this) AESCipherConfig(std::move(other)); +} + +void AESCipherConfig::MemoryInfo(MemoryTracker* tracker) const { + // If mode is sync, then the data in each of these properties + // is not owned by the AESCipherConfig, so we ignore it. + if (mode == kCryptoJobAsync) { + tracker->TrackFieldWithSize("iv", iv.size()); + tracker->TrackFieldWithSize("additional_data", additional_data.size()); + tracker->TrackFieldWithSize("tag", tag.size()); + } +} + +Maybe AESCipherTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + WebCryptoCipherMode cipher_mode, + AESCipherConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->mode = mode; + + CHECK(args[offset]->IsUint32()); // Key Variant + params->variant = + static_cast(args[offset].As()->Value()); + + int cipher_nid; + + switch (params->variant) { + case kKeyVariantAES_CTR_128: + if (!ValidateIV(env, mode, args[offset + 1], params) || + !ValidateCounter(env, args[offset + 2], params)) { + return Nothing(); + } + cipher_nid = NID_aes_128_ctr; + break; + case kKeyVariantAES_CTR_192: + if (!ValidateIV(env, mode, args[offset + 1], params) || + !ValidateCounter(env, args[offset + 2], params)) { + return Nothing(); + } + cipher_nid = NID_aes_192_ctr; + break; + case kKeyVariantAES_CTR_256: + if (!ValidateIV(env, mode, args[offset + 1], params) || + !ValidateCounter(env, args[offset + 2], params)) { + return Nothing(); + } + cipher_nid = NID_aes_256_ctr; + break; + case kKeyVariantAES_CBC_128: + if (!ValidateIV(env, mode, args[offset + 1], params)) + return Nothing(); + cipher_nid = NID_aes_128_cbc; + break; + case kKeyVariantAES_CBC_192: + if (!ValidateIV(env, mode, args[offset + 1], params)) + return Nothing(); + cipher_nid = NID_aes_192_cbc; + break; + case kKeyVariantAES_CBC_256: + if (!ValidateIV(env, mode, args[offset + 1], params)) + return Nothing(); + cipher_nid = NID_aes_256_cbc; + break; + case kKeyVariantAES_KW_128: + UseDefaultIV(params); + cipher_nid = NID_id_aes128_wrap; + break; + case kKeyVariantAES_KW_192: + UseDefaultIV(params); + cipher_nid = NID_id_aes192_wrap; + break; + case kKeyVariantAES_KW_256: + UseDefaultIV(params); + cipher_nid = NID_id_aes256_wrap; + break; + case kKeyVariantAES_GCM_128: + if (!ValidateIV(env, mode, args[offset + 1], params) || + !ValidateAuthTag(env, mode, cipher_mode, args[offset + 2], params) || + !ValidateAdditionalData(env, mode, args[offset + 3], params)) { + return Nothing(); + } + cipher_nid = NID_aes_128_gcm; + break; + case kKeyVariantAES_GCM_192: + if (!ValidateIV(env, mode, args[offset + 1], params) || + !ValidateAuthTag(env, mode, cipher_mode, args[offset + 2], params) || + !ValidateAdditionalData(env, mode, args[offset + 3], params)) { + return Nothing(); + } + cipher_nid = NID_aes_192_gcm; + break; + case kKeyVariantAES_GCM_256: + if (!ValidateIV(env, mode, args[offset + 1], params) || + !ValidateAuthTag(env, mode, cipher_mode, args[offset + 2], params) || + !ValidateAdditionalData(env, mode, args[offset + 3], params)) { + return Nothing(); + } + cipher_nid = NID_aes_256_gcm; + break; + default: + UNREACHABLE(); + } + + params->cipher = EVP_get_cipherbynid(cipher_nid); + CHECK_NOT_NULL(params->cipher); + + if (params->iv.size() < + static_cast(EVP_CIPHER_iv_length(params->cipher))) { + THROW_ERR_CRYPTO_INVALID_IV(env); + return Nothing(); + } + + return Just(true); +} + +WebCryptoCipherStatus AESCipherTraits::DoCipher( + Environment* env, + std::shared_ptr key_data, + WebCryptoCipherMode cipher_mode, + const AESCipherConfig& params, + const ByteSource& in, + ByteSource* out) { +#define V(name, fn) \ + case kKeyVariantAES_ ## name: \ + return fn(env, key_data.get(), cipher_mode, params, in, out); + switch (params.variant) { + VARIANTS(V) + default: + UNREACHABLE(); + } +#undef V +} + +void AES::Initialize(Environment* env, Local target) { + AESCryptoJob::Initialize(env, target); + +#define V(name, _) NODE_DEFINE_CONSTANT(target, kKeyVariantAES_ ## name); + VARIANTS(V) +#undef V +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_aes.h b/src/crypto/crypto_aes.h new file mode 100644 index 00000000000000..a5e37409548e69 --- /dev/null +++ b/src/crypto/crypto_aes.h @@ -0,0 +1,89 @@ +#ifndef SRC_CRYPTO_CRYPTO_AES_H_ +#define SRC_CRYPTO_CRYPTO_AES_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_cipher.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer.h" +#include "env.h" +#include "v8.h" + +namespace node { +namespace crypto { +constexpr size_t kAesBlockSize = 16; +constexpr unsigned kNoAuthTagLength = static_cast(-1); +constexpr const char* kDefaultWrapIV = "\xa6\xa6\xa6\xa6\xa6\xa6\xa6\xa6"; + +#define VARIANTS(V) \ + V(CTR_128, AES_CTR_Cipher) \ + V(CTR_192, AES_CTR_Cipher) \ + V(CTR_256, AES_CTR_Cipher) \ + V(CBC_128, AES_Cipher) \ + V(CBC_192, AES_Cipher) \ + V(CBC_256, AES_Cipher) \ + V(GCM_128, AES_Cipher) \ + V(GCM_192, AES_Cipher) \ + V(GCM_256, AES_Cipher) \ + V(KW_128, AES_Cipher) \ + V(KW_192, AES_Cipher) \ + V(KW_256, AES_Cipher) + +enum AESKeyVariant { +#define V(name, _) kKeyVariantAES_ ## name, + VARIANTS(V) +#undef V +}; + +struct AESCipherConfig final : public MemoryRetainer { + CryptoJobMode mode; + AESKeyVariant variant; + const EVP_CIPHER* cipher; + size_t length; + ByteSource iv; // Used for both iv or counter + ByteSource additional_data; + ByteSource tag; // Used only for authenticated modes (GCM) + + AESCipherConfig() = default; + + AESCipherConfig(AESCipherConfig&& other) noexcept; + + AESCipherConfig& operator=(AESCipherConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(AESCipherConfig); + SET_SELF_SIZE(AESCipherConfig); +}; + +struct AESCipherTraits final { + static constexpr const char* JobName = "AESCipherJob"; + + using AdditionalParameters = AESCipherConfig; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + WebCryptoCipherMode cipher_mode, + AESCipherConfig* config); + + static WebCryptoCipherStatus DoCipher( + Environment* env, + std::shared_ptr key_data, + WebCryptoCipherMode cipher_mode, + const AESCipherConfig& params, + const ByteSource& in, + ByteSource* out); +}; + +using AESCryptoJob = CipherJob; + +namespace AES { +void Initialize(Environment* env, v8::Local target); +} // namespace AES +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_AES_H_ diff --git a/src/crypto/crypto_bio.cc b/src/crypto/crypto_bio.cc index a4d74e4ad78be5..13793aa7f2ed65 100644 --- a/src/crypto/crypto_bio.cc +++ b/src/crypto/crypto_bio.cc @@ -19,12 +19,14 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +#include "crypto/crypto_bio.h" #include "base_object-inl.h" #include "memory_tracker-inl.h" -#include "allocated_buffer-inl.h" // Inlined functions needed by node_crypto.h. -#include "crypto/crypto_bio.h" -#include "openssl/bio.h" +#include "allocated_buffer-inl.h" #include "util-inl.h" + +#include + #include #include diff --git a/src/crypto/crypto_bio.h b/src/crypto/crypto_bio.h index 606ac1d7d43925..b25980ad1fae78 100644 --- a/src/crypto/crypto_bio.h +++ b/src/crypto/crypto_bio.h @@ -34,7 +34,6 @@ namespace node { class Environment; namespace crypto { - // This class represents buffers for OpenSSL I/O, implemented as a singly-linked // list of chunks. It can be used either for writing data from Node to OpenSSL, // or for reading data back, but not both. diff --git a/src/crypto/crypto_cipher.cc b/src/crypto/crypto_cipher.cc new file mode 100644 index 00000000000000..2b895fc2d22e34 --- /dev/null +++ b/src/crypto/crypto_cipher.cc @@ -0,0 +1,848 @@ +#include "crypto/crypto_cipher.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_buffer.h" +#include "node_internals.h" +#include "node_process.h" +#include "v8.h" + +namespace node { + +using v8::Array; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Int32; +using v8::Local; +using v8::Object; +using v8::Uint32; +using v8::Value; + +namespace crypto { +#ifdef OPENSSL_NO_OCB +# define IS_OCB_MODE(mode) false +#else +# define IS_OCB_MODE(mode) ((mode) == EVP_CIPH_OCB_MODE) +#endif + +namespace { +bool IsSupportedAuthenticatedMode(const EVP_CIPHER* cipher) { + const int mode = EVP_CIPHER_mode(cipher); + // Check `chacha20-poly1305` separately, it is also an AEAD cipher, + // but its mode is 0 which doesn't indicate + return EVP_CIPHER_nid(cipher) == NID_chacha20_poly1305 || + mode == EVP_CIPH_CCM_MODE || + mode == EVP_CIPH_GCM_MODE || + IS_OCB_MODE(mode); +} + +bool IsSupportedAuthenticatedMode(const EVP_CIPHER_CTX* ctx) { + const EVP_CIPHER* cipher = EVP_CIPHER_CTX_cipher(ctx); + return IsSupportedAuthenticatedMode(cipher); +} + +bool IsValidGCMTagLength(unsigned int tag_len) { + return tag_len == 4 || tag_len == 8 || (tag_len >= 12 && tag_len <= 16); +} +} // namespace + +void CipherBase::GetSSLCiphers(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + SSLCtxPointer ctx(SSL_CTX_new(TLS_method())); + CHECK(ctx); + + SSLPointer ssl(SSL_new(ctx.get())); + CHECK(ssl); + + STACK_OF(SSL_CIPHER)* ciphers = SSL_get_ciphers(ssl.get()); + + // TLSv1.3 ciphers aren't listed by EVP. There are only 5, we could just + // document them, but since there are only 5, easier to just add them manually + // and not have to explain their absence in the API docs. They are lower-cased + // because the docs say they will be. + static const char* TLS13_CIPHERS[] = { + "tls_aes_256_gcm_sha384", + "tls_chacha20_poly1305_sha256", + "tls_aes_128_gcm_sha256", + "tls_aes_128_ccm_8_sha256", + "tls_aes_128_ccm_sha256" + }; + + const int n = sk_SSL_CIPHER_num(ciphers); + std::vector> arr(n + arraysize(TLS13_CIPHERS)); + + for (int i = 0; i < n; ++i) { + const SSL_CIPHER* cipher = sk_SSL_CIPHER_value(ciphers, i); + arr[i] = OneByteString(env->isolate(), SSL_CIPHER_get_name(cipher)); + } + + for (unsigned i = 0; i < arraysize(TLS13_CIPHERS); ++i) { + const char* name = TLS13_CIPHERS[i]; + arr[n + i] = OneByteString(env->isolate(), name); + } + + args.GetReturnValue().Set(Array::New(env->isolate(), arr.data(), arr.size())); +} + +void CipherBase::GetCiphers(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CipherPushContext ctx(env); + EVP_CIPHER_do_all_sorted(array_push_back, &ctx); + args.GetReturnValue().Set(ctx.ToJSArray()); +} + +CipherBase::CipherBase(Environment* env, + Local wrap, + CipherKind kind) + : BaseObject(env, wrap), + ctx_(nullptr), + kind_(kind), + auth_tag_state_(kAuthTagUnknown), + auth_tag_len_(kNoAuthTagLength), + pending_auth_failed_(false) { + MakeWeak(); +} + +void CipherBase::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("context", ctx_ ? kSizeOf_EVP_CIPHER_CTX : 0); +} + +void CipherBase::Initialize(Environment* env, Local target) { + Local t = env->NewFunctionTemplate(New); + + t->InstanceTemplate()->SetInternalFieldCount( + CipherBase::kInternalFieldCount); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + + env->SetProtoMethod(t, "init", Init); + env->SetProtoMethod(t, "initiv", InitIv); + env->SetProtoMethod(t, "update", Update); + env->SetProtoMethod(t, "final", Final); + env->SetProtoMethod(t, "setAutoPadding", SetAutoPadding); + env->SetProtoMethodNoSideEffect(t, "getAuthTag", GetAuthTag); + env->SetProtoMethod(t, "setAuthTag", SetAuthTag); + env->SetProtoMethod(t, "setAAD", SetAAD); + + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "CipherBase"), + t->GetFunction(env->context()).ToLocalChecked()).Check(); + + env->SetMethodNoSideEffect(target, "getSSLCiphers", GetSSLCiphers); + env->SetMethodNoSideEffect(target, "getCiphers", GetCiphers); + + env->SetMethod(target, "publicEncrypt", + PublicKeyCipher::Cipher); + env->SetMethod(target, "privateDecrypt", + PublicKeyCipher::Cipher); + env->SetMethod(target, "privateEncrypt", + PublicKeyCipher::Cipher); + env->SetMethod(target, "publicDecrypt", + PublicKeyCipher::Cipher); + + NODE_DEFINE_CONSTANT(target, kWebCryptoCipherEncrypt); + NODE_DEFINE_CONSTANT(target, kWebCryptoCipherDecrypt); +} + +void CipherBase::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + new CipherBase(env, args.This(), args[0]->IsTrue() ? kCipher : kDecipher); +} + +void CipherBase::CommonInit(const char* cipher_type, + const EVP_CIPHER* cipher, + const unsigned char* key, + int key_len, + const unsigned char* iv, + int iv_len, + unsigned int auth_tag_len) { + CHECK(!ctx_); + ctx_.reset(EVP_CIPHER_CTX_new()); + + const int mode = EVP_CIPHER_mode(cipher); + if (mode == EVP_CIPH_WRAP_MODE) + EVP_CIPHER_CTX_set_flags(ctx_.get(), EVP_CIPHER_CTX_FLAG_WRAP_ALLOW); + + const bool encrypt = (kind_ == kCipher); + if (1 != EVP_CipherInit_ex(ctx_.get(), cipher, nullptr, + nullptr, nullptr, encrypt)) { + return ThrowCryptoError(env(), ERR_get_error(), + "Failed to initialize cipher"); + } + + if (IsSupportedAuthenticatedMode(cipher)) { + CHECK_GE(iv_len, 0); + if (!InitAuthenticated(cipher_type, iv_len, auth_tag_len)) + return; + } + + if (!EVP_CIPHER_CTX_set_key_length(ctx_.get(), key_len)) { + ctx_.reset(); + return THROW_ERR_CRYPTO_INVALID_KEYLEN(env()); + } + + if (1 != EVP_CipherInit_ex(ctx_.get(), nullptr, nullptr, key, iv, encrypt)) { + return ThrowCryptoError(env(), ERR_get_error(), + "Failed to initialize cipher"); + } +} + +void CipherBase::Init(const char* cipher_type, + const ArrayBufferOrViewContents& key_buf, + unsigned int auth_tag_len) { + HandleScope scope(env()->isolate()); + MarkPopErrorOnReturn mark_pop_error_on_return; + +#ifdef NODE_FIPS_MODE + if (FIPS_mode()) { + return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env(), + "crypto.createCipher() is not supported in FIPS mode."); + } +#endif // NODE_FIPS_MODE + + const EVP_CIPHER* const cipher = EVP_get_cipherbyname(cipher_type); + if (cipher == nullptr) + return THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env()); + + unsigned char key[EVP_MAX_KEY_LENGTH]; + unsigned char iv[EVP_MAX_IV_LENGTH]; + + int key_len = EVP_BytesToKey(cipher, + EVP_md5(), + nullptr, + key_buf.data(), + key_buf.size(), + 1, + key, + iv); + CHECK_NE(key_len, 0); + + const int mode = EVP_CIPHER_mode(cipher); + if (kind_ == kCipher && (mode == EVP_CIPH_CTR_MODE || + mode == EVP_CIPH_GCM_MODE || + mode == EVP_CIPH_CCM_MODE)) { + // Ignore the return value (i.e. possible exception) because we are + // not calling back into JS anyway. + ProcessEmitWarning(env(), + "Use Cipheriv for counter mode of %s", + cipher_type); + } + + CommonInit(cipher_type, cipher, key, key_len, iv, + EVP_CIPHER_iv_length(cipher), auth_tag_len); +} + +void CipherBase::Init(const FunctionCallbackInfo& args) { + CipherBase* cipher; + ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); + Environment* env = Environment::GetCurrent(args); + + CHECK_GE(args.Length(), 3); + + const Utf8Value cipher_type(args.GetIsolate(), args[0]); + ArrayBufferOrViewContents key_buf(args[1]); + if (!key_buf.CheckSizeInt32()) + return THROW_ERR_OUT_OF_RANGE(env, "password is too large"); + + // Don't assign to cipher->auth_tag_len_ directly; the value might not + // represent a valid length at this point. + unsigned int auth_tag_len; + if (args[2]->IsUint32()) { + auth_tag_len = args[2].As()->Value(); + } else { + CHECK(args[2]->IsInt32() && args[2].As()->Value() == -1); + auth_tag_len = kNoAuthTagLength; + } + + cipher->Init(*cipher_type, key_buf, auth_tag_len); +} + +void CipherBase::InitIv(const char* cipher_type, + const ByteSource& key_buf, + const ArrayBufferOrViewContents& iv_buf, + unsigned int auth_tag_len) { + HandleScope scope(env()->isolate()); + MarkPopErrorOnReturn mark_pop_error_on_return; + + const EVP_CIPHER* const cipher = EVP_get_cipherbyname(cipher_type); + if (cipher == nullptr) + return THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env()); + + const int expected_iv_len = EVP_CIPHER_iv_length(cipher); + const bool is_authenticated_mode = IsSupportedAuthenticatedMode(cipher); + const bool has_iv = iv_buf.size() > 0; + + // Throw if no IV was passed and the cipher requires an IV + if (!has_iv && expected_iv_len != 0) + return THROW_ERR_CRYPTO_INVALID_IV(env()); + + // Throw if an IV was passed which does not match the cipher's fixed IV length + // static_cast for the iv_buf.size() is safe because we've verified + // prior that the value is not larger than MAX_INT. + if (!is_authenticated_mode && + has_iv && + static_cast(iv_buf.size()) != expected_iv_len) { + return THROW_ERR_CRYPTO_INVALID_IV(env()); + } + + if (EVP_CIPHER_nid(cipher) == NID_chacha20_poly1305) { + CHECK(has_iv); + // Check for invalid IV lengths, since OpenSSL does not under some + // conditions: + // https://www.openssl.org/news/secadv/20190306.txt. + if (iv_buf.size() > 12) + return THROW_ERR_CRYPTO_INVALID_IV(env()); + } + + CommonInit( + cipher_type, + cipher, + key_buf.data(), + key_buf.size(), + iv_buf.data(), + iv_buf.size(), + auth_tag_len); +} + +void CipherBase::InitIv(const FunctionCallbackInfo& args) { + CipherBase* cipher; + ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); + Environment* env = cipher->env(); + + CHECK_GE(args.Length(), 4); + + const Utf8Value cipher_type(env->isolate(), args[0]); + + // The argument can either be a KeyObjectHandle or a byte source + // (e.g. ArrayBuffer, TypedArray, etc). Whichever it is, grab the + // raw bytes and proceed... + const ByteSource key_buf = ByteSource::FromSecretKeyBytes(env, args[1]); + + if (UNLIKELY(key_buf.size() > INT_MAX)) + return THROW_ERR_OUT_OF_RANGE(env, "key is too big"); + + ArrayBufferOrViewContents iv_buf; + if (!args[2]->IsNull()) + iv_buf = ArrayBufferOrViewContents(args[2]); + + if (UNLIKELY(!iv_buf.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "iv is too big"); + + // Don't assign to cipher->auth_tag_len_ directly; the value might not + // represent a valid length at this point. + unsigned int auth_tag_len; + if (args[3]->IsUint32()) { + auth_tag_len = args[3].As()->Value(); + } else { + CHECK(args[3]->IsInt32() && args[3].As()->Value() == -1); + auth_tag_len = kNoAuthTagLength; + } + + cipher->InitIv(*cipher_type, key_buf, iv_buf, auth_tag_len); +} + +bool CipherBase::InitAuthenticated( + const char* cipher_type, + int iv_len, + unsigned int auth_tag_len) { + CHECK(IsAuthenticatedMode()); + MarkPopErrorOnReturn mark_pop_error_on_return; + + if (!EVP_CIPHER_CTX_ctrl(ctx_.get(), + EVP_CTRL_AEAD_SET_IVLEN, + iv_len, + nullptr)) { + THROW_ERR_CRYPTO_INVALID_IV(env()); + return false; + } + + const int mode = EVP_CIPHER_CTX_mode(ctx_.get()); + if (mode == EVP_CIPH_GCM_MODE) { + if (auth_tag_len != kNoAuthTagLength) { + if (!IsValidGCMTagLength(auth_tag_len)) { + char msg[50]; + snprintf(msg, sizeof(msg), + "Invalid authentication tag length: %u", auth_tag_len); + THROW_ERR_CRYPTO_INVALID_AUTH_TAG(env(), msg); + return false; + } + + // Remember the given authentication tag length for later. + auth_tag_len_ = auth_tag_len; + } + } else { + if (auth_tag_len == kNoAuthTagLength) { + char msg[128]; + snprintf(msg, sizeof(msg), "authTagLength required for %s", cipher_type); + THROW_ERR_CRYPTO_INVALID_AUTH_TAG(env(), msg); + return false; + } + +#ifdef NODE_FIPS_MODE + // TODO(tniessen) Support CCM decryption in FIPS mode + if (mode == EVP_CIPH_CCM_MODE && kind_ == kDecipher && FIPS_mode()) { + THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env(), + "CCM encryption not supported in FIPS mode"); + return false; + } +#endif + + // Tell OpenSSL about the desired length. + if (!EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_SET_TAG, auth_tag_len, + nullptr)) { + THROW_ERR_CRYPTO_INVALID_AUTH_TAG(env()); + return false; + } + + // Remember the given authentication tag length for later. + auth_tag_len_ = auth_tag_len; + + if (mode == EVP_CIPH_CCM_MODE) { + // Restrict the message length to min(INT_MAX, 2^(8*(15-iv_len))-1) bytes. + CHECK(iv_len >= 7 && iv_len <= 13); + max_message_size_ = INT_MAX; + if (iv_len == 12) max_message_size_ = 16777215; + if (iv_len == 13) max_message_size_ = 65535; + } + } + + return true; +} + +bool CipherBase::CheckCCMMessageLength(int message_len) { + CHECK(ctx_); + CHECK(EVP_CIPHER_CTX_mode(ctx_.get()) == EVP_CIPH_CCM_MODE); + + if (message_len > max_message_size_) { + THROW_ERR_CRYPTO_INVALID_MESSAGELEN(env()); + return false; + } + + return true; +} + +bool CipherBase::IsAuthenticatedMode() const { + // Check if this cipher operates in an AEAD mode that we support. + CHECK(ctx_); + return IsSupportedAuthenticatedMode(ctx_.get()); +} + +void CipherBase::GetAuthTag(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CipherBase* cipher; + ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); + + // Only callable after Final and if encrypting. + if (cipher->ctx_ || + cipher->kind_ != kCipher || + cipher->auth_tag_len_ == kNoAuthTagLength) { + return; + } + + args.GetReturnValue().Set( + Buffer::Copy(env, cipher->auth_tag_, cipher->auth_tag_len_) + .ToLocalChecked()); +} + +void CipherBase::SetAuthTag(const FunctionCallbackInfo& args) { + CipherBase* cipher; + ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); + Environment* env = Environment::GetCurrent(args); + + if (!cipher->ctx_ || + !cipher->IsAuthenticatedMode() || + cipher->kind_ != kDecipher || + cipher->auth_tag_state_ != kAuthTagUnknown) { + return args.GetReturnValue().Set(false); + } + + ArrayBufferOrViewContents auth_tag(args[0]); + if (UNLIKELY(!auth_tag.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "buffer is too big"); + + unsigned int tag_len = auth_tag.size(); + + const int mode = EVP_CIPHER_CTX_mode(cipher->ctx_.get()); + bool is_valid; + if (mode == EVP_CIPH_GCM_MODE) { + // Restrict GCM tag lengths according to NIST 800-38d, page 9. + is_valid = (cipher->auth_tag_len_ == kNoAuthTagLength || + cipher->auth_tag_len_ == tag_len) && + IsValidGCMTagLength(tag_len); + } else { + // At this point, the tag length is already known and must match the + // length of the given authentication tag. + CHECK(IsSupportedAuthenticatedMode(cipher->ctx_.get())); + CHECK_NE(cipher->auth_tag_len_, kNoAuthTagLength); + is_valid = cipher->auth_tag_len_ == tag_len; + } + + if (!is_valid) { + char msg[50]; + snprintf(msg, sizeof(msg), + "Invalid authentication tag length: %u", tag_len); + return THROW_ERR_CRYPTO_INVALID_AUTH_TAG(env, msg); + } + + cipher->auth_tag_len_ = tag_len; + cipher->auth_tag_state_ = kAuthTagKnown; + CHECK_LE(cipher->auth_tag_len_, sizeof(cipher->auth_tag_)); + + memset(cipher->auth_tag_, 0, sizeof(cipher->auth_tag_)); + auth_tag.CopyTo(cipher->auth_tag_, cipher->auth_tag_len_); + + args.GetReturnValue().Set(true); +} + +bool CipherBase::MaybePassAuthTagToOpenSSL() { + if (auth_tag_state_ == kAuthTagKnown) { + if (!EVP_CIPHER_CTX_ctrl(ctx_.get(), + EVP_CTRL_AEAD_SET_TAG, + auth_tag_len_, + reinterpret_cast(auth_tag_))) { + return false; + } + auth_tag_state_ = kAuthTagPassedToOpenSSL; + } + return true; +} + +bool CipherBase::SetAAD( + const ArrayBufferOrViewContents& data, + int plaintext_len) { + if (!ctx_ || !IsAuthenticatedMode()) + return false; + MarkPopErrorOnReturn mark_pop_error_on_return; + + int outlen; + const int mode = EVP_CIPHER_CTX_mode(ctx_.get()); + + // When in CCM mode, we need to set the authentication tag and the plaintext + // length in advance. + if (mode == EVP_CIPH_CCM_MODE) { + if (plaintext_len < 0) { + THROW_ERR_MISSING_ARGS(env(), + "options.plaintextLength required for CCM mode with AAD"); + return false; + } + + if (!CheckCCMMessageLength(plaintext_len)) + return false; + + if (kind_ == kDecipher) { + if (!MaybePassAuthTagToOpenSSL()) + return false; + } + + // Specify the plaintext length. + if (!EVP_CipherUpdate(ctx_.get(), nullptr, &outlen, nullptr, plaintext_len)) + return false; + } + + return 1 == EVP_CipherUpdate(ctx_.get(), + nullptr, + &outlen, + data.data(), + data.size()); +} + +void CipherBase::SetAAD(const FunctionCallbackInfo& args) { + CipherBase* cipher; + ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); + Environment* env = Environment::GetCurrent(args); + + CHECK_EQ(args.Length(), 2); + CHECK(args[1]->IsInt32()); + int plaintext_len = args[1].As()->Value(); + ArrayBufferOrViewContents buf(args[0]); + + if (UNLIKELY(!buf.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "buffer is too big"); + args.GetReturnValue().Set(cipher->SetAAD(buf, plaintext_len)); +} + +CipherBase::UpdateResult CipherBase::Update( + const char* data, + size_t len, + AllocatedBuffer* out) { + if (!ctx_ || len > INT_MAX) + return kErrorState; + MarkPopErrorOnReturn mark_pop_error_on_return; + + const int mode = EVP_CIPHER_CTX_mode(ctx_.get()); + + if (mode == EVP_CIPH_CCM_MODE && !CheckCCMMessageLength(len)) + return kErrorMessageSize; + + // Pass the authentication tag to OpenSSL if possible. This will only happen + // once, usually on the first update. + if (kind_ == kDecipher && IsAuthenticatedMode()) + CHECK(MaybePassAuthTagToOpenSSL()); + + int buf_len = len + EVP_CIPHER_CTX_block_size(ctx_.get()); + // For key wrapping algorithms, get output size by calling + // EVP_CipherUpdate() with null output. + if (kind_ == kCipher && mode == EVP_CIPH_WRAP_MODE && + EVP_CipherUpdate(ctx_.get(), + nullptr, + &buf_len, + reinterpret_cast(data), + len) != 1) { + return kErrorState; + } + + *out = AllocatedBuffer::AllocateManaged(env(), buf_len); + int r = EVP_CipherUpdate(ctx_.get(), + reinterpret_cast(out->data()), + &buf_len, + reinterpret_cast(data), + len); + + CHECK_LE(static_cast(buf_len), out->size()); + out->Resize(buf_len); + + // When in CCM mode, EVP_CipherUpdate will fail if the authentication tag is + // invalid. In that case, remember the error and throw in final(). + if (!r && kind_ == kDecipher && mode == EVP_CIPH_CCM_MODE) { + pending_auth_failed_ = true; + return kSuccess; + } + return r == 1 ? kSuccess : kErrorState; +} + +void CipherBase::Update(const FunctionCallbackInfo& args) { + Decode(args, [](CipherBase* cipher, + const FunctionCallbackInfo& args, + const char* data, size_t size) { + AllocatedBuffer out; + Environment* env = Environment::GetCurrent(args); + + if (UNLIKELY(size > INT_MAX)) + return THROW_ERR_OUT_OF_RANGE(env, "data is too long"); + + UpdateResult r = cipher->Update(data, size, &out); + + if (r != kSuccess) { + if (r == kErrorState) { + ThrowCryptoError(env, ERR_get_error(), + "Trying to add data in unsupported state"); + } + return; + } + + CHECK(out.data() != nullptr || out.size() == 0); + args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); + }); +} + +bool CipherBase::SetAutoPadding(bool auto_padding) { + if (!ctx_) + return false; + MarkPopErrorOnReturn mark_pop_error_on_return; + return EVP_CIPHER_CTX_set_padding(ctx_.get(), auto_padding); +} + +void CipherBase::SetAutoPadding(const FunctionCallbackInfo& args) { + CipherBase* cipher; + ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); + + bool b = cipher->SetAutoPadding(args.Length() < 1 || args[0]->IsTrue()); + args.GetReturnValue().Set(b); // Possibly report invalid state failure +} + +bool CipherBase::Final(AllocatedBuffer* out) { + if (!ctx_) + return false; + + const int mode = EVP_CIPHER_CTX_mode(ctx_.get()); + + *out = AllocatedBuffer::AllocateManaged( + env(), + static_cast(EVP_CIPHER_CTX_block_size(ctx_.get()))); + + if (kind_ == kDecipher && IsSupportedAuthenticatedMode(ctx_.get())) { + MaybePassAuthTagToOpenSSL(); + } + + // In CCM mode, final() only checks whether authentication failed in update(). + // EVP_CipherFinal_ex must not be called and will fail. + bool ok; + if (kind_ == kDecipher && mode == EVP_CIPH_CCM_MODE) { + ok = !pending_auth_failed_; + *out = AllocatedBuffer::AllocateManaged(env(), 0); // Empty buffer. + } else { + int out_len = out->size(); + ok = EVP_CipherFinal_ex(ctx_.get(), + reinterpret_cast(out->data()), + &out_len) == 1; + + if (out_len >= 0) + out->Resize(out_len); + else + *out = AllocatedBuffer(); // *out will not be used. + + if (ok && kind_ == kCipher && IsAuthenticatedMode()) { + // In GCM mode, the authentication tag length can be specified in advance, + // but defaults to 16 bytes when encrypting. In CCM and OCB mode, it must + // always be given by the user. + if (auth_tag_len_ == kNoAuthTagLength) { + CHECK(mode == EVP_CIPH_GCM_MODE); + auth_tag_len_ = sizeof(auth_tag_); + } + CHECK_EQ(1, EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_GET_TAG, + auth_tag_len_, + reinterpret_cast(auth_tag_))); + } + } + + ctx_.reset(); + + return ok; +} + +void CipherBase::Final(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CipherBase* cipher; + ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); + if (cipher->ctx_ == nullptr) + return THROW_ERR_CRYPTO_INVALID_STATE(env); + + AllocatedBuffer out; + + // Check IsAuthenticatedMode() first, Final() destroys the EVP_CIPHER_CTX. + const bool is_auth_mode = cipher->IsAuthenticatedMode(); + bool r = cipher->Final(&out); + + if (!r) { + const char* msg = is_auth_mode + ? "Unsupported state or unable to authenticate data" + : "Unsupported state"; + + return ThrowCryptoError(env, ERR_get_error(), msg); + } + + args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); +} + +template +bool PublicKeyCipher::Cipher( + Environment* env, + const ManagedEVPPKey& pkey, + int padding, + const EVP_MD* digest, + const ArrayBufferOrViewContents& oaep_label, + const ArrayBufferOrViewContents& data, + AllocatedBuffer* out) { + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); + if (!ctx) + return false; + if (EVP_PKEY_cipher_init(ctx.get()) <= 0) + return false; + if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), padding) <= 0) + return false; + + if (digest != nullptr) { + if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), digest) <= 0) + return false; + } + + if (oaep_label.size() != 0) { + // OpenSSL takes ownership of the label, so we need to create a copy. + void* label = OPENSSL_memdup(oaep_label.data(), oaep_label.size()); + CHECK_NOT_NULL(label); + if (0 >= EVP_PKEY_CTX_set0_rsa_oaep_label(ctx.get(), + reinterpret_cast(label), + oaep_label.size())) { + OPENSSL_free(label); + return false; + } + } + + size_t out_len = 0; + if (EVP_PKEY_cipher( + ctx.get(), + nullptr, + &out_len, + data.data(), + data.size()) <= 0) { + return false; + } + + *out = AllocatedBuffer::AllocateManaged(env, out_len); + + if (EVP_PKEY_cipher( + ctx.get(), + reinterpret_cast(out->data()), + &out_len, + data.data(), + data.size()) <= 0) { + return false; + } + + out->Resize(out_len); + return true; +} + +template +void PublicKeyCipher::Cipher(const FunctionCallbackInfo& args) { + MarkPopErrorOnReturn mark_pop_error_on_return; + Environment* env = Environment::GetCurrent(args); + + unsigned int offset = 0; + ManagedEVPPKey pkey = + ManagedEVPPKey::GetPublicOrPrivateKeyFromJs(args, &offset); + if (!pkey) + return; + + ArrayBufferOrViewContents buf(args[offset]); + if (UNLIKELY(!buf.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "buffer is too long"); + + uint32_t padding; + if (!args[offset + 1]->Uint32Value(env->context()).To(&padding)) return; + + const EVP_MD* digest = nullptr; + if (args[offset + 2]->IsString()) { + const Utf8Value oaep_str(env->isolate(), args[offset + 2]); + digest = EVP_get_digestbyname(*oaep_str); + if (digest == nullptr) + return THROW_ERR_OSSL_EVP_INVALID_DIGEST(env); + } + + ArrayBufferOrViewContents oaep_label; + if (!args[offset + 3]->IsUndefined()) { + oaep_label = ArrayBufferOrViewContents(args[offset + 3]); + if (UNLIKELY(!oaep_label.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "oaep_label is too big"); + } + + AllocatedBuffer out; + if (!Cipher( + env, pkey, padding, digest, oaep_label, buf, &out)) { + return ThrowCryptoError(env, ERR_get_error()); + } + + Local result; + if (out.ToBuffer().ToLocal(&result)) + args.GetReturnValue().Set(result); +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_cipher.h b/src/crypto/crypto_cipher.h new file mode 100644 index 00000000000000..725807d509525d --- /dev/null +++ b/src/crypto/crypto_cipher.h @@ -0,0 +1,278 @@ +#ifndef SRC_CRYPTO_CRYPTO_CIPHER_H_ +#define SRC_CRYPTO_CRYPTO_CIPHER_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +#include + +namespace node { +namespace crypto { +class CipherBase : public BaseObject { + public: + static void GetSSLCiphers(const v8::FunctionCallbackInfo& args); + static void GetCiphers(const v8::FunctionCallbackInfo& args); + + static void Initialize(Environment* env, v8::Local target); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CipherBase) + SET_SELF_SIZE(CipherBase) + + protected: + enum CipherKind { + kCipher, + kDecipher + }; + enum UpdateResult { + kSuccess, + kErrorMessageSize, + kErrorState + }; + enum AuthTagState { + kAuthTagUnknown, + kAuthTagKnown, + kAuthTagPassedToOpenSSL + }; + static const unsigned kNoAuthTagLength = static_cast(-1); + + void CommonInit(const char* cipher_type, + const EVP_CIPHER* cipher, + const unsigned char* key, + int key_len, + const unsigned char* iv, + int iv_len, + unsigned int auth_tag_len); + void Init(const char* cipher_type, + const ArrayBufferOrViewContents& key_buf, + unsigned int auth_tag_len); + void InitIv(const char* cipher_type, + const ByteSource& key_buf, + const ArrayBufferOrViewContents& iv_buf, + unsigned int auth_tag_len); + bool InitAuthenticated(const char* cipher_type, int iv_len, + unsigned int auth_tag_len); + bool CheckCCMMessageLength(int message_len); + UpdateResult Update(const char* data, size_t len, AllocatedBuffer* out); + bool Final(AllocatedBuffer* out); + bool SetAutoPadding(bool auto_padding); + + bool IsAuthenticatedMode() const; + bool SetAAD( + const ArrayBufferOrViewContents& data, + int plaintext_len); + bool MaybePassAuthTagToOpenSSL(); + + static void New(const v8::FunctionCallbackInfo& args); + static void Init(const v8::FunctionCallbackInfo& args); + static void InitIv(const v8::FunctionCallbackInfo& args); + static void Update(const v8::FunctionCallbackInfo& args); + static void Final(const v8::FunctionCallbackInfo& args); + static void SetAutoPadding(const v8::FunctionCallbackInfo& args); + + static void GetAuthTag(const v8::FunctionCallbackInfo& args); + static void SetAuthTag(const v8::FunctionCallbackInfo& args); + static void SetAAD(const v8::FunctionCallbackInfo& args); + + CipherBase(Environment* env, v8::Local wrap, CipherKind kind); + + private: + DeleteFnPtr ctx_; + const CipherKind kind_; + AuthTagState auth_tag_state_; + unsigned int auth_tag_len_; + char auth_tag_[EVP_GCM_TLS_TAG_LEN]; + bool pending_auth_failed_; + int max_message_size_; +}; + +class PublicKeyCipher { + public: + typedef int (*EVP_PKEY_cipher_init_t)(EVP_PKEY_CTX* ctx); + typedef int (*EVP_PKEY_cipher_t)(EVP_PKEY_CTX* ctx, + unsigned char* out, size_t* outlen, + const unsigned char* in, size_t inlen); + + enum Operation { + kPublic, + kPrivate + }; + + template + static bool Cipher(Environment* env, + const ManagedEVPPKey& pkey, + int padding, + const EVP_MD* digest, + const ArrayBufferOrViewContents& oaep_label, + const ArrayBufferOrViewContents& data, + AllocatedBuffer* out); + + template + static void Cipher(const v8::FunctionCallbackInfo& args); +}; + +enum WebCryptoCipherMode { + kWebCryptoCipherEncrypt, + kWebCryptoCipherDecrypt +}; + +enum class WebCryptoCipherStatus { + ERR_OK, + ERR_INVALID_KEY_TYPE, + ERR_FAILED +}; + +// CipherJob is a base implementation class for implementations of +// one-shot sync and async ciphers. It has been added primarily to +// support the AES and RSA ciphers underlying the WebCrypt API. +// +// See the crypto_aes and crypto_rsa headers for examples of how to +// use CipherJob. +template +class CipherJob final : public CryptoJob { + public: + using AdditionalParams = typename CipherTraits::AdditionalParameters; + + static void New(const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + + CryptoJobMode mode = GetCryptoJobMode(args[0]); + + CHECK(args[1]->IsUint32()); // Cipher Mode + + uint32_t cmode = args[1].As()->Value(); + CHECK_LE(cmode, WebCryptoCipherMode::kWebCryptoCipherDecrypt); + WebCryptoCipherMode cipher_mode = static_cast(cmode); + + CHECK(args[2]->IsObject()); // KeyObject + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[2]); + CHECK_NOT_NULL(key); + + ArrayBufferOrViewContents data(args[3]); // data to operate on + if (!data.CheckSizeInt32()) + return THROW_ERR_OUT_OF_RANGE(env, "data is too large"); + + AdditionalParams params; + if (CipherTraits::AdditionalConfig(mode, args, 4, cipher_mode, ¶ms) + .IsNothing()) { + // The CipherTraits::AdditionalConfig is responsible for + // calling an appropriate THROW_CRYPTO_* variant reporting + // whatever error caused initialization to fail. + return; + } + + new CipherJob( + env, + args.This(), + mode, + key, + cipher_mode, + data, + std::move(params)); + } + + static void Initialize( + Environment* env, + v8::Local target) { + CryptoJob::Initialize(New, env, target); + } + + CipherJob( + Environment* env, + v8::Local object, + CryptoJobMode mode, + KeyObjectHandle* key, + WebCryptoCipherMode cipher_mode, + const ArrayBufferOrViewContents& data, + AdditionalParams&& params) + : CryptoJob( + env, + object, + AsyncWrap::PROVIDER_CIPHERREQUEST, + mode, + std::move(params)), + key_(key->Data()), + cipher_mode_(cipher_mode), + in_(mode == kCryptoJobAsync + ? data.ToCopy() + : data.ToByteSource()) {} + + std::shared_ptr key() const { return key_; } + + WebCryptoCipherMode cipher_mode() const { return cipher_mode_; } + + void DoThreadPoolWork() override { + switch (CipherTraits::DoCipher( + AsyncWrap::env(), + key(), + cipher_mode_, + *CryptoJob::params(), + in_, + &out_)) { + case WebCryptoCipherStatus::ERR_OK: + // Success! + break; + case WebCryptoCipherStatus::ERR_INVALID_KEY_TYPE: + // Fall through + // TODO(@jasnell): Separate error for this + case WebCryptoCipherStatus::ERR_FAILED: { + CryptoErrorVector* errors = CryptoJob::errors(); + errors->Capture(); + if (errors->empty()) + errors->push_back(std::string("Cipher job failed.")); + } + } + } + + v8::Maybe ToResult( + v8::Local* err, + v8::Local* result) override { + Environment* env = AsyncWrap::env(); + CryptoErrorVector* errors = CryptoJob::errors(); + if (out_.size() > 0) { + CHECK(errors->empty()); + *err = v8::Undefined(env->isolate()); + *result = out_.ToArrayBuffer(env); + return v8::Just(!result->IsEmpty()); + } + + if (errors->empty()) + errors->Capture(); + CHECK(!errors->empty()); + *result = v8::Undefined(env->isolate()); + return v8::Just(errors->ToException(env).ToLocal(err)); + } + + SET_SELF_SIZE(CipherJob) + void MemoryInfo(MemoryTracker* tracker) const override { + if (CryptoJob::mode() == kCryptoJobAsync) + tracker->TrackFieldWithSize("in", in_.size()); + tracker->TrackFieldWithSize("out", out_.size()); + CryptoJob::MemoryInfo(tracker); + } + + private: + std::shared_ptr key_; + WebCryptoCipherMode cipher_mode_; + ByteSource in_; + ByteSource out_; +}; + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_CIPHER_H_ diff --git a/src/crypto/crypto_clienthello-inl.h b/src/crypto/crypto_clienthello-inl.h index 555920b2f0830d..4f133e83832d1e 100644 --- a/src/crypto/crypto_clienthello-inl.h +++ b/src/crypto/crypto_clienthello-inl.h @@ -29,7 +29,6 @@ namespace node { namespace crypto { - inline ClientHelloParser::ClientHelloParser() : state_(kEnded), onhello_cb_(nullptr), diff --git a/src/crypto/crypto_clienthello.cc b/src/crypto/crypto_clienthello.cc index f4dc6c5c572f94..5a0be70bc11e5f 100644 --- a/src/crypto/crypto_clienthello.cc +++ b/src/crypto/crypto_clienthello.cc @@ -24,7 +24,6 @@ namespace node { namespace crypto { - void ClientHelloParser::Parse(const uint8_t* data, size_t avail) { switch (state_) { case kWaiting: diff --git a/src/crypto/crypto_clienthello.h b/src/crypto/crypto_clienthello.h index b79210c0b47d1e..0d3bf60f96bc69 100644 --- a/src/crypto/crypto_clienthello.h +++ b/src/crypto/crypto_clienthello.h @@ -29,7 +29,6 @@ namespace node { namespace crypto { - // Parse the client hello so we can do async session resumption. OpenSSL's // session resumption uses synchronous callbacks, see SSL_CTX_sess_set_get_cb // and get_session_cb. diff --git a/src/crypto/crypto_common.cc b/src/crypto/crypto_common.cc index 8c54ac89c969a1..ae9cbbc79589a2 100644 --- a/src/crypto/crypto_common.cc +++ b/src/crypto/crypto_common.cc @@ -8,6 +8,7 @@ #include "node_internals.h" #include "node_url.h" #include "string_bytes.h" +#include "memory_tracker-inl.h" #include "v8.h" #include @@ -39,7 +40,6 @@ using v8::Undefined; using v8::Value; namespace crypto { - static constexpr int X509_NAME_FLAGS = ASN1_STRFLGS_ESC_CTRL | ASN1_STRFLGS_UTF8_CONVERT | diff --git a/src/crypto/crypto_common.h b/src/crypto/crypto_common.h index fdbc268e251535..e738eac07eb885 100644 --- a/src/crypto/crypto_common.h +++ b/src/crypto/crypto_common.h @@ -13,7 +13,6 @@ namespace node { namespace crypto { - // OPENSSL_free is a macro, so we need a wrapper function. struct OpenSSLBufferDeleter { void operator()(char* pointer) const { OPENSSL_free(pointer); } diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc new file mode 100644 index 00000000000000..16a14ffcc89591 --- /dev/null +++ b/src/crypto/crypto_context.cc @@ -0,0 +1,1284 @@ +#include "crypto/crypto_context.h" +#include "crypto/crypto_bio.h" +#include "crypto/crypto_common.h" +#include "crypto/crypto_util.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node.h" +#include "node_buffer.h" +#include "node_options.h" +#include "util.h" +#include "v8.h" + +#include +#include +#ifndef OPENSSL_NO_ENGINE +#include +#endif // !OPENSSL_NO_ENGINE + +namespace node { + +using v8::Array; +using v8::ArrayBufferView; +using v8::Boolean; +using v8::Context; +using v8::DontDelete; +using v8::Exception; +using v8::External; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Int32; +using v8::Integer; +using v8::Local; +using v8::Object; +using v8::PropertyAttribute; +using v8::ReadOnly; +using v8::Signature; +using v8::String; +using v8::Value; + +namespace crypto { +static const char* const root_certs[] = { +#include "node_root_certs.h" // NOLINT(build/include_order) +}; + +static const char system_cert_path[] = NODE_OPENSSL_SYSTEM_CERT_PATH; + +static X509_STORE* root_cert_store; + +static bool extra_root_certs_loaded = false; + +namespace { +// Takes a string or buffer and loads it into a BIO. +// Caller responsible for BIO_free_all-ing the returned object. +BIOPointer LoadBIO(Environment* env, Local v) { + HandleScope scope(env->isolate()); + + if (v->IsString()) { + const node::Utf8Value s(env->isolate(), v); + return NodeBIO::NewFixed(*s, s.length()); + } + + if (v->IsArrayBufferView()) { + ArrayBufferViewContents buf(v.As()); + return NodeBIO::NewFixed(buf.data(), buf.length()); + } + + return nullptr; +} + +int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, + X509Pointer&& x, + STACK_OF(X509)* extra_certs, + X509Pointer* cert, + X509Pointer* issuer_) { + CHECK(!*issuer_); + CHECK(!*cert); + X509* issuer = nullptr; + + int ret = SSL_CTX_use_certificate(ctx, x.get()); + + if (ret) { + // If we could set up our certificate, now proceed to + // the CA certificates. + SSL_CTX_clear_extra_chain_certs(ctx); + + for (int i = 0; i < sk_X509_num(extra_certs); i++) { + X509* ca = sk_X509_value(extra_certs, i); + + // NOTE: Increments reference count on `ca` + if (!SSL_CTX_add1_chain_cert(ctx, ca)) { + ret = 0; + issuer = nullptr; + break; + } + // Note that we must not free r if it was successfully + // added to the chain (while we must free the main + // certificate, since its reference count is increased + // by SSL_CTX_use_certificate). + + // Find issuer + if (issuer != nullptr || X509_check_issued(ca, x.get()) != X509_V_OK) + continue; + + issuer = ca; + } + } + + // Try getting issuer from a cert store + if (ret) { + if (issuer == nullptr) { + ret = SSL_CTX_get_issuer(ctx, x.get(), &issuer); + ret = ret < 0 ? 0 : 1; + // NOTE: get_cert_store doesn't increment reference count, + // no need to free `store` + } else { + // Increment issuer reference count + issuer = X509_dup(issuer); + if (issuer == nullptr) { + ret = 0; + } + } + } + + issuer_->reset(issuer); + + if (ret && x != nullptr) { + cert->reset(X509_dup(x.get())); + if (!*cert) + ret = 0; + } + return ret; +} + +// Read a file that contains our certificate in "PEM" format, +// possibly followed by a sequence of CA certificates that should be +// sent to the peer in the Certificate message. +// +// Taken from OpenSSL - edited for style. +int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, + BIOPointer&& in, + X509Pointer* cert, + X509Pointer* issuer) { + // Just to ensure that `ERR_peek_last_error` below will return only errors + // that we are interested in + ERR_clear_error(); + + X509Pointer x( + PEM_read_bio_X509_AUX(in.get(), nullptr, NoPasswordCallback, nullptr)); + + if (!x) + return 0; + + unsigned long err = 0; // NOLINT(runtime/int) + + StackOfX509 extra_certs(sk_X509_new_null()); + if (!extra_certs) + return 0; + + while (X509Pointer extra {PEM_read_bio_X509(in.get(), + nullptr, + NoPasswordCallback, + nullptr)}) { + if (sk_X509_push(extra_certs.get(), extra.get())) { + extra.release(); + continue; + } + + return 0; + } + + // When the while loop ends, it's usually just EOF. + err = ERR_peek_last_error(); + if (ERR_GET_LIB(err) == ERR_LIB_PEM && + ERR_GET_REASON(err) == PEM_R_NO_START_LINE) { + ERR_clear_error(); + } else { + // some real error + return 0; + } + + return SSL_CTX_use_certificate_chain(ctx, + std::move(x), + extra_certs.get(), + cert, + issuer); +} + +static X509_STORE* NewRootCertStore() { + static std::vector root_certs_vector; + static Mutex root_certs_vector_mutex; + Mutex::ScopedLock lock(root_certs_vector_mutex); + + if (root_certs_vector.empty()) { + for (size_t i = 0; i < arraysize(root_certs); i++) { + X509* x509 = + PEM_read_bio_X509(NodeBIO::NewFixed(root_certs[i], + strlen(root_certs[i])).get(), + nullptr, // no re-use of X509 structure + NoPasswordCallback, + nullptr); // no callback data + + // Parse errors from the built-in roots are fatal. + CHECK_NOT_NULL(x509); + + root_certs_vector.push_back(x509); + } + } + + X509_STORE* store = X509_STORE_new(); + if (*system_cert_path != '\0') { + X509_STORE_load_locations(store, system_cert_path, nullptr); + } + + Mutex::ScopedLock cli_lock(node::per_process::cli_options_mutex); + if (per_process::cli_options->ssl_openssl_cert_store) { + X509_STORE_set_default_paths(store); + } else { + for (X509* cert : root_certs_vector) { + X509_up_ref(cert); + X509_STORE_add_cert(store, cert); + } + } + + return store; +} +} // namespace + +void GetRootCertificates(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local result[arraysize(root_certs)]; + + for (size_t i = 0; i < arraysize(root_certs); i++) { + if (!String::NewFromOneByte( + env->isolate(), + reinterpret_cast(root_certs[i])) + .ToLocal(&result[i])) { + return; + } + } + + args.GetReturnValue().Set( + Array::New(env->isolate(), result, arraysize(root_certs))); +} + +void SecureContext::Initialize(Environment* env, Local target) { + Local t = env->NewFunctionTemplate(New); + t->InstanceTemplate()->SetInternalFieldCount( + SecureContext::kInternalFieldCount); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + Local secureContextString = + FIXED_ONE_BYTE_STRING(env->isolate(), "SecureContext"); + t->SetClassName(secureContextString); + + env->SetProtoMethod(t, "init", Init); + env->SetProtoMethod(t, "setKey", SetKey); +#ifndef OPENSSL_NO_ENGINE + env->SetProtoMethod(t, "setEngineKey", SetEngineKey); +#endif // !OPENSSL_NO_ENGINE + env->SetProtoMethod(t, "setCert", SetCert); + env->SetProtoMethod(t, "addCACert", AddCACert); + env->SetProtoMethod(t, "addCRL", AddCRL); + env->SetProtoMethod(t, "addRootCerts", AddRootCerts); + env->SetProtoMethod(t, "setCipherSuites", SetCipherSuites); + env->SetProtoMethod(t, "setCiphers", SetCiphers); + env->SetProtoMethod(t, "setSigalgs", SetSigalgs); + env->SetProtoMethod(t, "setECDHCurve", SetECDHCurve); + env->SetProtoMethod(t, "setDHParam", SetDHParam); + env->SetProtoMethod(t, "setMaxProto", SetMaxProto); + env->SetProtoMethod(t, "setMinProto", SetMinProto); + env->SetProtoMethod(t, "getMaxProto", GetMaxProto); + env->SetProtoMethod(t, "getMinProto", GetMinProto); + env->SetProtoMethod(t, "setOptions", SetOptions); + env->SetProtoMethod(t, "setSessionIdContext", SetSessionIdContext); + env->SetProtoMethod(t, "setSessionTimeout", SetSessionTimeout); + env->SetProtoMethod(t, "close", Close); + env->SetProtoMethod(t, "loadPKCS12", LoadPKCS12); +#ifndef OPENSSL_NO_ENGINE + env->SetProtoMethod(t, "setClientCertEngine", SetClientCertEngine); +#endif // !OPENSSL_NO_ENGINE + env->SetProtoMethodNoSideEffect(t, "getTicketKeys", GetTicketKeys); + env->SetProtoMethod(t, "setTicketKeys", SetTicketKeys); + env->SetProtoMethod(t, "setFreeListLength", SetFreeListLength); + env->SetProtoMethod(t, "enableTicketKeyCallback", EnableTicketKeyCallback); + env->SetProtoMethodNoSideEffect(t, "getCertificate", GetCertificate); + env->SetProtoMethodNoSideEffect(t, "getIssuer", GetCertificate); + +#define SET_INTEGER_CONSTANTS(name, value) \ + t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), name), \ + Integer::NewFromUnsigned(env->isolate(), value)); + SET_INTEGER_CONSTANTS("kTicketKeyReturnIndex", kTicketKeyReturnIndex); + SET_INTEGER_CONSTANTS("kTicketKeyHMACIndex", kTicketKeyHMACIndex); + SET_INTEGER_CONSTANTS("kTicketKeyAESIndex", kTicketKeyAESIndex); + SET_INTEGER_CONSTANTS("kTicketKeyNameIndex", kTicketKeyNameIndex); + SET_INTEGER_CONSTANTS("kTicketKeyIVIndex", kTicketKeyIVIndex); + +#undef SET_INTEGER_CONSTANTS + + Local ctx_getter_templ = + FunctionTemplate::New(env->isolate(), + CtxGetter, + Local(), + Signature::New(env->isolate(), t)); + + + t->PrototypeTemplate()->SetAccessorProperty( + FIXED_ONE_BYTE_STRING(env->isolate(), "_external"), + ctx_getter_templ, + Local(), + static_cast(ReadOnly | DontDelete)); + + target->Set(env->context(), secureContextString, + t->GetFunction(env->context()).ToLocalChecked()).Check(); + env->set_secure_context_constructor_template(t); + + env->SetMethodNoSideEffect(target, "getRootCertificates", + GetRootCertificates); + // Exposed for testing purposes only. + env->SetMethodNoSideEffect(target, "isExtraRootCertsFileLoaded", + IsExtraRootCertsFileLoaded); +} + +SecureContext::SecureContext(Environment* env, Local wrap) + : BaseObject(env, wrap) { + MakeWeak(); + env->isolate()->AdjustAmountOfExternalAllocatedMemory(kExternalSize); +} + +inline void SecureContext::Reset() { + if (ctx_ != nullptr) { + env()->isolate()->AdjustAmountOfExternalAllocatedMemory(-kExternalSize); + } + ctx_.reset(); + cert_.reset(); + issuer_.reset(); +} + +SecureContext::~SecureContext() { + Reset(); +} + +void SecureContext::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + new SecureContext(env, args.This()); +} + +void SecureContext::Init(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + Environment* env = sc->env(); + + CHECK_EQ(args.Length(), 3); + CHECK(args[1]->IsInt32()); + CHECK(args[2]->IsInt32()); + + int min_version = args[1].As()->Value(); + int max_version = args[2].As()->Value(); + const SSL_METHOD* method = TLS_method(); + + if (max_version == 0) + max_version = kMaxSupportedVersion; + + if (args[0]->IsString()) { + const node::Utf8Value sslmethod(env->isolate(), args[0]); + + // Note that SSLv2 and SSLv3 are disallowed but SSLv23_method and friends + // are still accepted. They are OpenSSL's way of saying that all known + // protocols below TLS 1.3 are supported unless explicitly disabled (which + // we do below for SSLv2 and SSLv3.) + if (sslmethod == "SSLv2_method" || + sslmethod == "SSLv2_server_method" || + sslmethod == "SSLv2_client_method") { + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv2 methods disabled"); + return; + } else if (sslmethod == "SSLv3_method" || + sslmethod == "SSLv3_server_method" || + sslmethod == "SSLv3_client_method") { + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv3 methods disabled"); + return; + } else if (sslmethod == "SSLv23_method") { + max_version = TLS1_2_VERSION; + } else if (sslmethod == "SSLv23_server_method") { + max_version = TLS1_2_VERSION; + method = TLS_server_method(); + } else if (sslmethod == "SSLv23_client_method") { + max_version = TLS1_2_VERSION; + method = TLS_client_method(); + } else if (sslmethod == "TLS_method") { + min_version = 0; + max_version = kMaxSupportedVersion; + } else if (sslmethod == "TLS_server_method") { + min_version = 0; + max_version = kMaxSupportedVersion; + method = TLS_server_method(); + } else if (sslmethod == "TLS_client_method") { + min_version = 0; + max_version = kMaxSupportedVersion; + method = TLS_client_method(); + } else if (sslmethod == "TLSv1_method") { + min_version = TLS1_VERSION; + max_version = TLS1_VERSION; + } else if (sslmethod == "TLSv1_server_method") { + min_version = TLS1_VERSION; + max_version = TLS1_VERSION; + method = TLS_server_method(); + } else if (sslmethod == "TLSv1_client_method") { + min_version = TLS1_VERSION; + max_version = TLS1_VERSION; + method = TLS_client_method(); + } else if (sslmethod == "TLSv1_1_method") { + min_version = TLS1_1_VERSION; + max_version = TLS1_1_VERSION; + } else if (sslmethod == "TLSv1_1_server_method") { + min_version = TLS1_1_VERSION; + max_version = TLS1_1_VERSION; + method = TLS_server_method(); + } else if (sslmethod == "TLSv1_1_client_method") { + min_version = TLS1_1_VERSION; + max_version = TLS1_1_VERSION; + method = TLS_client_method(); + } else if (sslmethod == "TLSv1_2_method") { + min_version = TLS1_2_VERSION; + max_version = TLS1_2_VERSION; + } else if (sslmethod == "TLSv1_2_server_method") { + min_version = TLS1_2_VERSION; + max_version = TLS1_2_VERSION; + method = TLS_server_method(); + } else if (sslmethod == "TLSv1_2_client_method") { + min_version = TLS1_2_VERSION; + max_version = TLS1_2_VERSION; + method = TLS_client_method(); + } else { + const std::string msg("Unknown method: "); + THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, (msg + * sslmethod).c_str()); + return; + } + } + + sc->ctx_.reset(SSL_CTX_new(method)); + SSL_CTX_set_app_data(sc->ctx_.get(), sc); + + // Disable SSLv2 in the case when method == TLS_method() and the + // cipher list contains SSLv2 ciphers (not the default, should be rare.) + // The bundled OpenSSL doesn't have SSLv2 support but the system OpenSSL may. + // SSLv3 is disabled because it's susceptible to downgrade attacks (POODLE.) + SSL_CTX_set_options(sc->ctx_.get(), SSL_OP_NO_SSLv2); + SSL_CTX_set_options(sc->ctx_.get(), SSL_OP_NO_SSLv3); + + // Enable automatic cert chaining. This is enabled by default in OpenSSL, but + // disabled by default in BoringSSL. Enable it explicitly to make the + // behavior match when Node is built with BoringSSL. + SSL_CTX_clear_mode(sc->ctx_.get(), SSL_MODE_NO_AUTO_CHAIN); + + // SSL session cache configuration + SSL_CTX_set_session_cache_mode(sc->ctx_.get(), + SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_SERVER | + SSL_SESS_CACHE_NO_INTERNAL | + SSL_SESS_CACHE_NO_AUTO_CLEAR); + + SSL_CTX_set_min_proto_version(sc->ctx_.get(), min_version); + SSL_CTX_set_max_proto_version(sc->ctx_.get(), max_version); + + // OpenSSL 1.1.0 changed the ticket key size, but the OpenSSL 1.0.x size was + // exposed in the public API. To retain compatibility, install a callback + // which restores the old algorithm. + if (RAND_bytes(sc->ticket_key_name_, sizeof(sc->ticket_key_name_)) <= 0 || + RAND_bytes(sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_)) <= 0 || + RAND_bytes(sc->ticket_key_aes_, sizeof(sc->ticket_key_aes_)) <= 0) { + return env->ThrowError("Error generating ticket keys"); + } + SSL_CTX_set_tlsext_ticket_key_cb(sc->ctx_.get(), TicketCompatibilityCallback); +} + +void SecureContext::SetKey(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + unsigned int len = args.Length(); + if (len < 1) { + return THROW_ERR_MISSING_ARGS(env, "Private key argument is mandatory"); + } + + if (len > 2) { + return env->ThrowError("Only private key and pass phrase are expected"); + } + + if (len == 2) { + if (args[1]->IsUndefined() || args[1]->IsNull()) + len = 1; + else + THROW_AND_RETURN_IF_NOT_STRING(env, args[1], "Pass phrase"); + } + + BIOPointer bio(LoadBIO(env, args[0])); + if (!bio) + return; + + node::Utf8Value passphrase(env->isolate(), args[1]); + + EVPKeyPointer key( + PEM_read_bio_PrivateKey(bio.get(), + nullptr, + PasswordCallback, + *passphrase)); + + if (!key) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + return ThrowCryptoError(env, err, "PEM_read_bio_PrivateKey"); + } + + int rv = SSL_CTX_use_PrivateKey(sc->ctx_.get(), key.get()); + + if (!rv) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + return ThrowCryptoError(env, err, "SSL_CTX_use_PrivateKey"); + } +} + +void SecureContext::SetSigalgs(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + Environment* env = sc->env(); + ClearErrorOnReturn clear_error_on_return; + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsString()); + + const node::Utf8Value sigalgs(env->isolate(), args[0]); + + int rv = SSL_CTX_set1_sigalgs_list(sc->ctx_.get(), *sigalgs); + + if (rv == 0) { + return ThrowCryptoError(env, ERR_get_error()); + } +} + +#ifndef OPENSSL_NO_ENGINE +void SecureContext::SetEngineKey(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + CHECK_EQ(args.Length(), 2); + + CryptoErrorVector errors; + const Utf8Value engine_id(env->isolate(), args[1]); + EnginePointer engine = LoadEngineById(*engine_id, &errors); + if (!engine) { + env->isolate()->ThrowException(errors.ToException(env).ToLocalChecked()); + return; + } + + if (!ENGINE_init(engine.get())) { + return env->ThrowError("ENGINE_init"); + } + + engine.finish_on_exit = true; + + const Utf8Value key_name(env->isolate(), args[0]); + EVPKeyPointer key(ENGINE_load_private_key(engine.get(), *key_name, + nullptr, nullptr)); + + if (!key) { + return ThrowCryptoError(env, ERR_get_error(), "ENGINE_load_private_key"); + } + + int rv = SSL_CTX_use_PrivateKey(sc->ctx_.get(), key.get()); + + if (rv == 0) { + return ThrowCryptoError(env, ERR_get_error(), "SSL_CTX_use_PrivateKey"); + } + + sc->private_key_engine_ = std::move(engine); +} +#endif // !OPENSSL_NO_ENGINE + +void SecureContext::SetCert(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + if (args.Length() != 1) { + return THROW_ERR_MISSING_ARGS(env, "Certificate argument is mandatory"); + } + + BIOPointer bio(LoadBIO(env, args[0])); + if (!bio) + return; + + sc->cert_.reset(); + sc->issuer_.reset(); + + int rv = SSL_CTX_use_certificate_chain(sc->ctx_.get(), + std::move(bio), + &sc->cert_, + &sc->issuer_); + + if (!rv) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + return ThrowCryptoError(env, err, "SSL_CTX_use_certificate_chain"); + } +} + +void SecureContext::AddCACert(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + ClearErrorOnReturn clear_error_on_return; + + if (args.Length() != 1) { + return THROW_ERR_MISSING_ARGS(env, "CA certificate argument is mandatory"); + } + + BIOPointer bio(LoadBIO(env, args[0])); + if (!bio) + return; + + X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get()); + while (X509* x509 = PEM_read_bio_X509_AUX( + bio.get(), nullptr, NoPasswordCallback, nullptr)) { + if (cert_store == root_cert_store) { + cert_store = NewRootCertStore(); + SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store); + } + X509_STORE_add_cert(cert_store, x509); + SSL_CTX_add_client_CA(sc->ctx_.get(), x509); + X509_free(x509); + } +} + +void SecureContext::AddCRL(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + if (args.Length() != 1) { + return THROW_ERR_MISSING_ARGS(env, "CRL argument is mandatory"); + } + + ClearErrorOnReturn clear_error_on_return; + + BIOPointer bio(LoadBIO(env, args[0])); + if (!bio) + return; + + DeleteFnPtr crl( + PEM_read_bio_X509_CRL(bio.get(), nullptr, NoPasswordCallback, nullptr)); + + if (!crl) + return env->ThrowError("Failed to parse CRL"); + + X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get()); + if (cert_store == root_cert_store) { + cert_store = NewRootCertStore(); + SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store); + } + + X509_STORE_add_crl(cert_store, crl.get()); + X509_STORE_set_flags(cert_store, + X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); +} + +static unsigned long AddCertsFromFile( // NOLINT(runtime/int) + X509_STORE* store, + const char* file) { + ERR_clear_error(); + MarkPopErrorOnReturn mark_pop_error_on_return; + + BIOPointer bio(BIO_new_file(file, "r")); + if (!bio) + return ERR_get_error(); + + while (X509* x509 = + PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr)) { + X509_STORE_add_cert(store, x509); + X509_free(x509); + } + + unsigned long err = ERR_peek_error(); // NOLINT(runtime/int) + // Ignore error if its EOF/no start line found. + if (ERR_GET_LIB(err) == ERR_LIB_PEM && + ERR_GET_REASON(err) == PEM_R_NO_START_LINE) { + return 0; + } + + return err; +} + +void UseExtraCaCerts(const std::string& file) { + ClearErrorOnReturn clear_error_on_return; + + if (root_cert_store == nullptr) { + root_cert_store = NewRootCertStore(); + + if (!file.empty()) { + unsigned long err = AddCertsFromFile( // NOLINT(runtime/int) + root_cert_store, + file.c_str()); + if (err) { + fprintf(stderr, + "Warning: Ignoring extra certs from `%s`, load failed: %s\n", + file.c_str(), + ERR_error_string(err, nullptr)); + } else { + extra_root_certs_loaded = true; + } + } + } +} + +void IsExtraRootCertsFileLoaded( + const FunctionCallbackInfo& args) { + return args.GetReturnValue().Set(extra_root_certs_loaded); +} + +void SecureContext::AddRootCerts(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + ClearErrorOnReturn clear_error_on_return; + + if (root_cert_store == nullptr) { + root_cert_store = NewRootCertStore(); + } + + // Increment reference count so global store is not deleted along with CTX. + X509_STORE_up_ref(root_cert_store); + SSL_CTX_set_cert_store(sc->ctx_.get(), root_cert_store); +} + +void SecureContext::SetCipherSuites(const FunctionCallbackInfo& args) { + // BoringSSL doesn't allow API config of TLS1.3 cipher suites. +#ifndef OPENSSL_IS_BORINGSSL + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + Environment* env = sc->env(); + ClearErrorOnReturn clear_error_on_return; + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsString()); + + const node::Utf8Value ciphers(args.GetIsolate(), args[0]); + if (!SSL_CTX_set_ciphersuites(sc->ctx_.get(), *ciphers)) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + return ThrowCryptoError(env, err, "Failed to set ciphers"); + } +#endif +} + +void SecureContext::SetCiphers(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + Environment* env = sc->env(); + ClearErrorOnReturn clear_error_on_return; + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsString()); + + const node::Utf8Value ciphers(args.GetIsolate(), args[0]); + if (!SSL_CTX_set_cipher_list(sc->ctx_.get(), *ciphers)) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + + if (strlen(*ciphers) == 0 && ERR_GET_REASON(err) == SSL_R_NO_CIPHER_MATCH) { + // TLS1.2 ciphers were deliberately cleared, so don't consider + // SSL_R_NO_CIPHER_MATCH to be an error (this is how _set_cipher_suites() + // works). If the user actually sets a value (like "no-such-cipher"), then + // that's actually an error. + return; + } + return ThrowCryptoError(env, err, "Failed to set ciphers"); + } +} + +void SecureContext::SetECDHCurve(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + Environment* env = sc->env(); + + if (args.Length() != 1) + return THROW_ERR_MISSING_ARGS(env, "ECDH curve name argument is mandatory"); + + THROW_AND_RETURN_IF_NOT_STRING(env, args[0], "ECDH curve name"); + + node::Utf8Value curve(env->isolate(), args[0]); + + if (strcmp(*curve, "auto") == 0) + return; + + if (!SSL_CTX_set1_curves_list(sc->ctx_.get(), *curve)) + return env->ThrowError("Failed to set ECDH curve"); +} + +void SecureContext::SetDHParam(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.This()); + Environment* env = sc->env(); + ClearErrorOnReturn clear_error_on_return; + + // Auto DH is not supported in openssl 1.0.1, so dhparam needs + // to be specified explicitly + if (args.Length() != 1) + return THROW_ERR_MISSING_ARGS(env, "DH argument is mandatory"); + + DHPointer dh; + { + BIOPointer bio(LoadBIO(env, args[0])); + if (!bio) + return; + + dh.reset(PEM_read_bio_DHparams(bio.get(), nullptr, nullptr, nullptr)); + } + + // Invalid dhparam is silently discarded and DHE is no longer used. + if (!dh) + return; + + const BIGNUM* p; + DH_get0_pqg(dh.get(), &p, nullptr, nullptr); + const int size = BN_num_bits(p); + if (size < 1024) { + return THROW_ERR_INVALID_ARG_VALUE( + env, "DH parameter is less than 1024 bits"); + } else if (size < 2048) { + args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING( + env->isolate(), "DH parameter is less than 2048 bits")); + } + + SSL_CTX_set_options(sc->ctx_.get(), SSL_OP_SINGLE_DH_USE); + int r = SSL_CTX_set_tmp_dh(sc->ctx_.get(), dh.get()); + + if (!r) + return env->ThrowTypeError("Error setting temp DH parameter"); +} + +void SecureContext::SetMinProto(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsInt32()); + + int version = args[0].As()->Value(); + + CHECK(SSL_CTX_set_min_proto_version(sc->ctx_.get(), version)); +} + +void SecureContext::SetMaxProto(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsInt32()); + + int version = args[0].As()->Value(); + + CHECK(SSL_CTX_set_max_proto_version(sc->ctx_.get(), version)); +} + +void SecureContext::GetMinProto(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + CHECK_EQ(args.Length(), 0); + + long version = // NOLINT(runtime/int) + SSL_CTX_get_min_proto_version(sc->ctx_.get()); + args.GetReturnValue().Set(static_cast(version)); +} + +void SecureContext::GetMaxProto(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + CHECK_EQ(args.Length(), 0); + + long version = // NOLINT(runtime/int) + SSL_CTX_get_max_proto_version(sc->ctx_.get()); + args.GetReturnValue().Set(static_cast(version)); +} + +void SecureContext::SetOptions(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + int64_t val; + + if (args.Length() != 1 || + !args[0]->IntegerValue(args.GetIsolate()->GetCurrentContext()).To(&val)) { + return THROW_ERR_INVALID_ARG_TYPE( + sc->env(), "Options must be an integer value"); + } + + SSL_CTX_set_options(sc->ctx_.get(), + static_cast(val)); // NOLINT(runtime/int) +} + +void SecureContext::SetSessionIdContext( + const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + Environment* env = sc->env(); + + if (args.Length() != 1) { + return THROW_ERR_MISSING_ARGS( + env, "Session ID context argument is mandatory"); + } + + THROW_AND_RETURN_IF_NOT_STRING(env, args[0], "Session ID context"); + + const node::Utf8Value sessionIdContext(args.GetIsolate(), args[0]); + const unsigned char* sid_ctx = + reinterpret_cast(*sessionIdContext); + unsigned int sid_ctx_len = sessionIdContext.length(); + + int r = SSL_CTX_set_session_id_context(sc->ctx_.get(), sid_ctx, sid_ctx_len); + if (r == 1) + return; + + BUF_MEM* mem; + Local message; + + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) { + message = FIXED_ONE_BYTE_STRING(args.GetIsolate(), + "SSL_CTX_set_session_id_context error"); + } else { + ERR_print_errors(bio.get()); + BIO_get_mem_ptr(bio.get(), &mem); + message = OneByteString(args.GetIsolate(), mem->data, mem->length); + } + + args.GetIsolate()->ThrowException(Exception::TypeError(message)); +} + +void SecureContext::SetSessionTimeout(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + if (args.Length() != 1 || !args[0]->IsInt32()) { + return THROW_ERR_INVALID_ARG_TYPE( + sc->env(), "Session timeout must be a 32-bit integer"); + } + + int32_t sessionTimeout = args[0].As()->Value(); + SSL_CTX_set_timeout(sc->ctx_.get(), sessionTimeout); +} + +void SecureContext::Close(const FunctionCallbackInfo& args) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + sc->Reset(); +} + +// Takes .pfx or .p12 and password in string or buffer format +void SecureContext::LoadPKCS12(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + std::vector pass; + bool ret = false; + + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + ClearErrorOnReturn clear_error_on_return; + + if (args.Length() < 1) { + return THROW_ERR_MISSING_ARGS(env, "PFX certificate argument is mandatory"); + } + + BIOPointer in(LoadBIO(env, args[0])); + if (!in) + return env->ThrowError("Unable to load BIO"); + + if (args.Length() >= 2) { + THROW_AND_RETURN_IF_NOT_BUFFER(env, args[1], "Pass phrase"); + Local abv = args[1].As(); + size_t passlen = abv->ByteLength(); + pass.resize(passlen + 1); + abv->CopyContents(pass.data(), passlen); + pass[passlen] = '\0'; + } + + // Free previous certs + sc->issuer_.reset(); + sc->cert_.reset(); + + X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get()); + + DeleteFnPtr p12; + EVPKeyPointer pkey; + X509Pointer cert; + StackOfX509 extra_certs; + + PKCS12* p12_ptr = nullptr; + EVP_PKEY* pkey_ptr = nullptr; + X509* cert_ptr = nullptr; + STACK_OF(X509)* extra_certs_ptr = nullptr; + if (d2i_PKCS12_bio(in.get(), &p12_ptr) && + (p12.reset(p12_ptr), true) && // Move ownership to the smart pointer. + PKCS12_parse(p12.get(), pass.data(), + &pkey_ptr, + &cert_ptr, + &extra_certs_ptr) && + (pkey.reset(pkey_ptr), cert.reset(cert_ptr), + extra_certs.reset(extra_certs_ptr), true) && // Move ownership. + SSL_CTX_use_certificate_chain(sc->ctx_.get(), + std::move(cert), + extra_certs.get(), + &sc->cert_, + &sc->issuer_) && + SSL_CTX_use_PrivateKey(sc->ctx_.get(), pkey.get())) { + // Add CA certs too + for (int i = 0; i < sk_X509_num(extra_certs.get()); i++) { + X509* ca = sk_X509_value(extra_certs.get(), i); + + if (cert_store == root_cert_store) { + cert_store = NewRootCertStore(); + SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store); + } + X509_STORE_add_cert(cert_store, ca); + SSL_CTX_add_client_CA(sc->ctx_.get(), ca); + } + ret = true; + } + + if (!ret) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + const char* str = ERR_reason_error_string(err); + return env->ThrowError(str); + } +} + +#ifndef OPENSSL_NO_ENGINE +void SecureContext::SetClientCertEngine( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsString()); + + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); + + MarkPopErrorOnReturn mark_pop_error_on_return; + + // SSL_CTX_set_client_cert_engine does not itself support multiple + // calls by cleaning up before overwriting the client_cert_engine + // internal context variable. + // Instead of trying to fix up this problem we in turn also do not + // support multiple calls to SetClientCertEngine. + if (sc->client_cert_engine_provided_) { + return env->ThrowError( + "Multiple calls to SetClientCertEngine are not allowed"); + } + + CryptoErrorVector errors; + const node::Utf8Value engine_id(env->isolate(), args[0]); + EnginePointer engine = LoadEngineById(*engine_id, &errors); + if (!engine) { + env->isolate()->ThrowException(errors.ToException(env).ToLocalChecked()); + return; + } + + // Note that this takes another reference to `engine`. + int r = SSL_CTX_set_client_cert_engine(sc->ctx_.get(), engine.get()); + if (r == 0) + return ThrowCryptoError(env, ERR_get_error()); + sc->client_cert_engine_provided_ = true; +} +#endif // !OPENSSL_NO_ENGINE + +void SecureContext::GetTicketKeys(const FunctionCallbackInfo& args) { +#if !defined(OPENSSL_NO_TLSEXT) && defined(SSL_CTX_get_tlsext_ticket_keys) + + SecureContext* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + + Local buff = Buffer::New(wrap->env(), 48).ToLocalChecked(); + memcpy(Buffer::Data(buff), wrap->ticket_key_name_, 16); + memcpy(Buffer::Data(buff) + 16, wrap->ticket_key_hmac_, 16); + memcpy(Buffer::Data(buff) + 32, wrap->ticket_key_aes_, 16); + + args.GetReturnValue().Set(buff); +#endif // !def(OPENSSL_NO_TLSEXT) && def(SSL_CTX_get_tlsext_ticket_keys) +} + +void SecureContext::SetTicketKeys(const FunctionCallbackInfo& args) { +#if !defined(OPENSSL_NO_TLSEXT) && defined(SSL_CTX_get_tlsext_ticket_keys) + SecureContext* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + Environment* env = wrap->env(); + + // TODO(@sam-github) Move type and len check to js, and CHECK() in C++. + if (args.Length() < 1) { + return THROW_ERR_MISSING_ARGS(env, "Ticket keys argument is mandatory"); + } + + THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "Ticket keys"); + ArrayBufferViewContents buf(args[0].As()); + + if (buf.length() != 48) { + return THROW_ERR_INVALID_ARG_VALUE( + env, "Ticket keys length must be 48 bytes"); + } + + memcpy(wrap->ticket_key_name_, buf.data(), 16); + memcpy(wrap->ticket_key_hmac_, buf.data() + 16, 16); + memcpy(wrap->ticket_key_aes_, buf.data() + 32, 16); + + args.GetReturnValue().Set(true); +#endif // !def(OPENSSL_NO_TLSEXT) && def(SSL_CTX_get_tlsext_ticket_keys) +} + +void SecureContext::SetFreeListLength(const FunctionCallbackInfo& args) { +} + +// Currently, EnableTicketKeyCallback and TicketKeyCallback are only present for +// the regression test in test/parallel/test-https-resume-after-renew.js. +void SecureContext::EnableTicketKeyCallback( + const FunctionCallbackInfo& args) { + SecureContext* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + + SSL_CTX_set_tlsext_ticket_key_cb(wrap->ctx_.get(), TicketKeyCallback); +} + +int SecureContext::TicketKeyCallback(SSL* ssl, + unsigned char* name, + unsigned char* iv, + EVP_CIPHER_CTX* ectx, + HMAC_CTX* hctx, + int enc) { + static const int kTicketPartSize = 16; + + SecureContext* sc = static_cast( + SSL_CTX_get_app_data(SSL_get_SSL_CTX(ssl))); + + Environment* env = sc->env(); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local argv[] = { + Buffer::Copy(env, + reinterpret_cast(name), + kTicketPartSize).ToLocalChecked(), + Buffer::Copy(env, + reinterpret_cast(iv), + kTicketPartSize).ToLocalChecked(), + Boolean::New(env->isolate(), enc != 0) + }; + + Local ret = node::MakeCallback(env->isolate(), + sc->object(), + env->ticketkeycallback_string(), + arraysize(argv), + argv, + {0, 0}).ToLocalChecked(); + Local arr = ret.As(); + + int r = + arr->Get(env->context(), + kTicketKeyReturnIndex).ToLocalChecked() + ->Int32Value(env->context()).FromJust(); + if (r < 0) + return r; + + Local hmac = arr->Get(env->context(), + kTicketKeyHMACIndex).ToLocalChecked(); + Local aes = arr->Get(env->context(), + kTicketKeyAESIndex).ToLocalChecked(); + if (Buffer::Length(aes) != kTicketPartSize) + return -1; + + if (enc) { + Local name_val = arr->Get(env->context(), + kTicketKeyNameIndex).ToLocalChecked(); + Local iv_val = arr->Get(env->context(), + kTicketKeyIVIndex).ToLocalChecked(); + + if (Buffer::Length(name_val) != kTicketPartSize || + Buffer::Length(iv_val) != kTicketPartSize) { + return -1; + } + + name_val.As()->CopyContents(name, kTicketPartSize); + iv_val.As()->CopyContents(iv, kTicketPartSize); + } + + ArrayBufferViewContents hmac_buf(hmac); + HMAC_Init_ex(hctx, + hmac_buf.data(), + hmac_buf.length(), + EVP_sha256(), + nullptr); + + ArrayBufferViewContents aes_key(aes.As()); + if (enc) { + EVP_EncryptInit_ex(ectx, + EVP_aes_128_cbc(), + nullptr, + aes_key.data(), + iv); + } else { + EVP_DecryptInit_ex(ectx, + EVP_aes_128_cbc(), + nullptr, + aes_key.data(), + iv); + } + + return r; +} + +int SecureContext::TicketCompatibilityCallback(SSL* ssl, + unsigned char* name, + unsigned char* iv, + EVP_CIPHER_CTX* ectx, + HMAC_CTX* hctx, + int enc) { + SecureContext* sc = static_cast( + SSL_CTX_get_app_data(SSL_get_SSL_CTX(ssl))); + + if (enc) { + memcpy(name, sc->ticket_key_name_, sizeof(sc->ticket_key_name_)); + if (RAND_bytes(iv, 16) <= 0 || + EVP_EncryptInit_ex(ectx, EVP_aes_128_cbc(), nullptr, + sc->ticket_key_aes_, iv) <= 0 || + HMAC_Init_ex(hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_), + EVP_sha256(), nullptr) <= 0) { + return -1; + } + return 1; + } + + if (memcmp(name, sc->ticket_key_name_, sizeof(sc->ticket_key_name_)) != 0) { + // The ticket key name does not match. Discard the ticket. + return 0; + } + + if (EVP_DecryptInit_ex(ectx, EVP_aes_128_cbc(), nullptr, sc->ticket_key_aes_, + iv) <= 0 || + HMAC_Init_ex(hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_), + EVP_sha256(), nullptr) <= 0) { + return -1; + } + return 1; +} + +void SecureContext::CtxGetter(const FunctionCallbackInfo& info) { + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, info.This()); + Local ext = External::New(info.GetIsolate(), sc->ctx_.get()); + info.GetReturnValue().Set(ext); +} + +template +void SecureContext::GetCertificate(const FunctionCallbackInfo& args) { + SecureContext* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + Environment* env = wrap->env(); + X509* cert; + + if (primary) + cert = wrap->cert_.get(); + else + cert = wrap->issuer_.get(); + if (cert == nullptr) + return args.GetReturnValue().SetNull(); + + int size = i2d_X509(cert, nullptr); + Local buff = Buffer::New(env, size).ToLocalChecked(); + unsigned char* serialized = reinterpret_cast( + Buffer::Data(buff)); + i2d_X509(cert, &serialized); + + args.GetReturnValue().Set(buff); +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_context.h b/src/crypto/crypto_context.h new file mode 100644 index 00000000000000..f6c911f1fad03f --- /dev/null +++ b/src/crypto/crypto_context.h @@ -0,0 +1,125 @@ +#ifndef SRC_CRYPTO_CRYPTO_CONTEXT_H_ +#define SRC_CRYPTO_CRYPTO_CONTEXT_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_util.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +namespace node { +namespace crypto { +// A maxVersion of 0 means "any", but OpenSSL may support TLS versions that +// Node.js doesn't, so pin the max to what we do support. +constexpr int kMaxSupportedVersion = TLS1_3_VERSION; + +void GetRootCertificates( + const v8::FunctionCallbackInfo& args); + +void IsExtraRootCertsFileLoaded( + const v8::FunctionCallbackInfo& args); + +class SecureContext final : public BaseObject { + public: + ~SecureContext() override; + + static void Initialize(Environment* env, v8::Local target); + + SSL_CTX* operator*() const { return ctx_.get(); } + + // TODO(joyeecheung): track the memory used by OpenSSL types + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(SecureContext) + SET_SELF_SIZE(SecureContext) + + SSLCtxPointer ctx_; + X509Pointer cert_; + X509Pointer issuer_; +#ifndef OPENSSL_NO_ENGINE + bool client_cert_engine_provided_ = false; + EnginePointer private_key_engine_; +#endif // !OPENSSL_NO_ENGINE + + static const int kMaxSessionSize = 10 * 1024; + + // See TicketKeyCallback + static const int kTicketKeyReturnIndex = 0; + static const int kTicketKeyHMACIndex = 1; + static const int kTicketKeyAESIndex = 2; + static const int kTicketKeyNameIndex = 3; + static const int kTicketKeyIVIndex = 4; + + unsigned char ticket_key_name_[16]; + unsigned char ticket_key_aes_[16]; + unsigned char ticket_key_hmac_[16]; + + protected: + // OpenSSL structures are opaque. This is sizeof(SSL_CTX) for OpenSSL 1.1.1b: + static const int64_t kExternalSize = 1024; + + static void New(const v8::FunctionCallbackInfo& args); + static void Init(const v8::FunctionCallbackInfo& args); + static void SetKey(const v8::FunctionCallbackInfo& args); +#ifndef OPENSSL_NO_ENGINE + static void SetEngineKey(const v8::FunctionCallbackInfo& args); +#endif // !OPENSSL_NO_ENGINE + static void SetCert(const v8::FunctionCallbackInfo& args); + static void AddCACert(const v8::FunctionCallbackInfo& args); + static void AddCRL(const v8::FunctionCallbackInfo& args); + static void AddRootCerts(const v8::FunctionCallbackInfo& args); + static void SetCipherSuites(const v8::FunctionCallbackInfo& args); + static void SetCiphers(const v8::FunctionCallbackInfo& args); + static void SetSigalgs(const v8::FunctionCallbackInfo& args); + static void SetECDHCurve(const v8::FunctionCallbackInfo& args); + static void SetDHParam(const v8::FunctionCallbackInfo& args); + static void SetOptions(const v8::FunctionCallbackInfo& args); + static void SetSessionIdContext( + const v8::FunctionCallbackInfo& args); + static void SetSessionTimeout( + const v8::FunctionCallbackInfo& args); + static void SetMinProto(const v8::FunctionCallbackInfo& args); + static void SetMaxProto(const v8::FunctionCallbackInfo& args); + static void GetMinProto(const v8::FunctionCallbackInfo& args); + static void GetMaxProto(const v8::FunctionCallbackInfo& args); + static void Close(const v8::FunctionCallbackInfo& args); + static void LoadPKCS12(const v8::FunctionCallbackInfo& args); +#ifndef OPENSSL_NO_ENGINE + static void SetClientCertEngine( + const v8::FunctionCallbackInfo& args); +#endif // !OPENSSL_NO_ENGINE + static void GetTicketKeys(const v8::FunctionCallbackInfo& args); + static void SetTicketKeys(const v8::FunctionCallbackInfo& args); + static void SetFreeListLength( + const v8::FunctionCallbackInfo& args); + static void EnableTicketKeyCallback( + const v8::FunctionCallbackInfo& args); + static void CtxGetter(const v8::FunctionCallbackInfo& info); + + template + static void GetCertificate(const v8::FunctionCallbackInfo& args); + + static int TicketKeyCallback(SSL* ssl, + unsigned char* name, + unsigned char* iv, + EVP_CIPHER_CTX* ectx, + HMAC_CTX* hctx, + int enc); + + static int TicketCompatibilityCallback(SSL* ssl, + unsigned char* name, + unsigned char* iv, + EVP_CIPHER_CTX* ectx, + HMAC_CTX* hctx, + int enc); + + SecureContext(Environment* env, v8::Local wrap); + void Reset(); +}; + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_CONTEXT_H_ diff --git a/src/crypto/crypto_dh.cc b/src/crypto/crypto_dh.cc new file mode 100644 index 00000000000000..b907e7b7a4c5e4 --- /dev/null +++ b/src/crypto/crypto_dh.cc @@ -0,0 +1,677 @@ +#include "crypto/crypto_dh.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_groups.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +namespace node { + +using v8::ConstructorBehavior; +using v8::DontDelete; +using v8::FunctionCallback; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Int32; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Object; +using v8::PropertyAttribute; +using v8::ReadOnly; +using v8::SideEffectType; +using v8::Signature; +using v8::String; +using v8::Value; + +namespace crypto { +namespace { +static void ZeroPadDiffieHellmanSecret(size_t remainder_size, + char* data, + size_t length) { + // DH_size returns number of bytes in a prime number. + // DH_compute_key returns number of bytes in a remainder of exponent, which + // may have less bytes than a prime number. Therefore add 0-padding to the + // allocated buffer. + const size_t prime_size = length; + if (remainder_size != prime_size) { + CHECK_LT(remainder_size, prime_size); + const size_t padding = prime_size - remainder_size; + memmove(data + padding, data, remainder_size); + memset(data, 0, padding); + } +} +static void ZeroPadDiffieHellmanSecret(size_t remainder_size, + AllocatedBuffer* ret) { + ZeroPadDiffieHellmanSecret(remainder_size, ret->data(), ret->size()); +} +} // namespace + +DiffieHellman::DiffieHellman(Environment* env, Local wrap) + : BaseObject(env, wrap), verifyError_(0) { + MakeWeak(); +} + +void DiffieHellman::Initialize(Environment* env, Local target) { + auto make = [&] (Local name, FunctionCallback callback) { + Local t = env->NewFunctionTemplate(callback); + + const PropertyAttribute attributes = + static_cast(ReadOnly | DontDelete); + + t->InstanceTemplate()->SetInternalFieldCount( + DiffieHellman::kInternalFieldCount); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + + env->SetProtoMethod(t, "generateKeys", GenerateKeys); + env->SetProtoMethod(t, "computeSecret", ComputeSecret); + env->SetProtoMethodNoSideEffect(t, "getPrime", GetPrime); + env->SetProtoMethodNoSideEffect(t, "getGenerator", GetGenerator); + env->SetProtoMethodNoSideEffect(t, "getPublicKey", GetPublicKey); + env->SetProtoMethodNoSideEffect(t, "getPrivateKey", GetPrivateKey); + env->SetProtoMethod(t, "setPublicKey", SetPublicKey); + env->SetProtoMethod(t, "setPrivateKey", SetPrivateKey); + + Local verify_error_getter_templ = + FunctionTemplate::New(env->isolate(), + DiffieHellman::VerifyErrorGetter, + Local(), + Signature::New(env->isolate(), t), + /* length */ 0, + ConstructorBehavior::kThrow, + SideEffectType::kHasNoSideEffect); + + t->InstanceTemplate()->SetAccessorProperty( + env->verify_error_string(), + verify_error_getter_templ, + Local(), + attributes); + + target->Set(env->context(), + name, + t->GetFunction(env->context()).ToLocalChecked()).Check(); + }; + + make(FIXED_ONE_BYTE_STRING(env->isolate(), "DiffieHellman"), New); + make(FIXED_ONE_BYTE_STRING(env->isolate(), "DiffieHellmanGroup"), + DiffieHellmanGroup); + + env->SetMethodNoSideEffect(target, "statelessDH", DiffieHellman::Stateless); + DHKeyPairGenJob::Initialize(env, target); + DHKeyExportJob::Initialize(env, target); + DHBitsJob::Initialize(env, target); +} + +bool DiffieHellman::Init(int primeLength, int g) { + dh_.reset(DH_new()); + if (!DH_generate_parameters_ex(dh_.get(), primeLength, g, nullptr)) + return false; + return VerifyContext(); +} + +void DiffieHellman::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("dh", dh_ ? kSizeOf_DH : 0); +} + +bool DiffieHellman::Init(const char* p, int p_len, int g) { + dh_.reset(DH_new()); + if (p_len <= 0) { + BNerr(BN_F_BN_GENERATE_PRIME_EX, BN_R_BITS_TOO_SMALL); + return false; + } + if (g <= 1) { + DHerr(DH_F_DH_BUILTIN_GENPARAMS, DH_R_BAD_GENERATOR); + return false; + } + BIGNUM* bn_p = + BN_bin2bn(reinterpret_cast(p), p_len, nullptr); + BIGNUM* bn_g = BN_new(); + if (!BN_set_word(bn_g, g) || + !DH_set0_pqg(dh_.get(), bn_p, nullptr, bn_g)) { + BN_free(bn_p); + BN_free(bn_g); + return false; + } + return VerifyContext(); +} + +bool DiffieHellman::Init(const char* p, int p_len, const char* g, int g_len) { + dh_.reset(DH_new()); + if (p_len <= 0) { + BNerr(BN_F_BN_GENERATE_PRIME_EX, BN_R_BITS_TOO_SMALL); + return false; + } + if (g_len <= 0) { + DHerr(DH_F_DH_BUILTIN_GENPARAMS, DH_R_BAD_GENERATOR); + return false; + } + BIGNUM* bn_g = + BN_bin2bn(reinterpret_cast(g), g_len, nullptr); + if (BN_is_zero(bn_g) || BN_is_one(bn_g)) { + BN_free(bn_g); + DHerr(DH_F_DH_BUILTIN_GENPARAMS, DH_R_BAD_GENERATOR); + return false; + } + BIGNUM* bn_p = + BN_bin2bn(reinterpret_cast(p), p_len, nullptr); + if (!DH_set0_pqg(dh_.get(), bn_p, nullptr, bn_g)) { + BN_free(bn_p); + BN_free(bn_g); + return false; + } + return VerifyContext(); +} + +inline const modp_group* FindDiffieHellmanGroup(const char* name) { + for (const modp_group& group : modp_groups) { + if (StringEqualNoCase(name, group.name)) + return &group; + } + return nullptr; +} + +void DiffieHellman::DiffieHellmanGroup( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + DiffieHellman* diffieHellman = new DiffieHellman(env, args.This()); + + CHECK_EQ(args.Length(), 1); + THROW_AND_RETURN_IF_NOT_STRING(env, args[0], "Group name"); + + bool initialized = false; + + const node::Utf8Value group_name(env->isolate(), args[0]); + const modp_group* group = FindDiffieHellmanGroup(*group_name); + if (group == nullptr) + return THROW_ERR_CRYPTO_UNKNOWN_DH_GROUP(env); + + initialized = diffieHellman->Init(group->prime, + group->prime_size, + group->gen); + if (!initialized) + THROW_ERR_CRYPTO_INITIALIZATION_FAILED(env); +} + + +void DiffieHellman::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + DiffieHellman* diffieHellman = + new DiffieHellman(env, args.This()); + bool initialized = false; + + if (args.Length() == 2) { + if (args[0]->IsInt32()) { + if (args[1]->IsInt32()) { + initialized = diffieHellman->Init(args[0].As()->Value(), + args[1].As()->Value()); + } + } else { + ArrayBufferOrViewContents arg0(args[0]); + if (UNLIKELY(!arg0.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "prime is too big"); + if (args[1]->IsInt32()) { + initialized = diffieHellman->Init(arg0.data(), + arg0.size(), + args[1].As()->Value()); + } else { + ArrayBufferOrViewContents arg1(args[1]); + if (UNLIKELY(!arg1.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "generator is too big"); + initialized = diffieHellman->Init(arg0.data(), arg0.size(), + arg1.data(), arg1.size()); + } + } + } + + if (!initialized) { + return ThrowCryptoError(env, ERR_get_error(), "Initialization failed"); + } +} + + +void DiffieHellman::GenerateKeys(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + DiffieHellman* diffieHellman; + ASSIGN_OR_RETURN_UNWRAP(&diffieHellman, args.Holder()); + + if (!DH_generate_key(diffieHellman->dh_.get())) { + return ThrowCryptoError(env, ERR_get_error(), "Key generation failed"); + } + + const BIGNUM* pub_key; + DH_get0_key(diffieHellman->dh_.get(), &pub_key, nullptr); + const int size = BN_num_bytes(pub_key); + CHECK_GE(size, 0); + AllocatedBuffer data = AllocatedBuffer::AllocateManaged(env, size); + CHECK_EQ(size, + BN_bn2binpad( + pub_key, reinterpret_cast(data.data()), size)); + args.GetReturnValue().Set(data.ToBuffer().ToLocalChecked()); +} + + +void DiffieHellman::GetField(const FunctionCallbackInfo& args, + const BIGNUM* (*get_field)(const DH*), + const char* err_if_null) { + Environment* env = Environment::GetCurrent(args); + + DiffieHellman* dh; + ASSIGN_OR_RETURN_UNWRAP(&dh, args.Holder()); + + const BIGNUM* num = get_field(dh->dh_.get()); + if (num == nullptr) + return THROW_ERR_CRYPTO_INVALID_STATE(env, err_if_null); + + const int size = BN_num_bytes(num); + CHECK_GE(size, 0); + AllocatedBuffer data = AllocatedBuffer::AllocateManaged(env, size); + CHECK_EQ( + size, + BN_bn2binpad(num, reinterpret_cast(data.data()), size)); + args.GetReturnValue().Set(data.ToBuffer().ToLocalChecked()); +} + +void DiffieHellman::GetPrime(const FunctionCallbackInfo& args) { + GetField(args, [](const DH* dh) -> const BIGNUM* { + const BIGNUM* p; + DH_get0_pqg(dh, &p, nullptr, nullptr); + return p; + }, "p is null"); +} + +void DiffieHellman::GetGenerator(const FunctionCallbackInfo& args) { + GetField(args, [](const DH* dh) -> const BIGNUM* { + const BIGNUM* g; + DH_get0_pqg(dh, nullptr, nullptr, &g); + return g; + }, "g is null"); +} + +void DiffieHellman::GetPublicKey(const FunctionCallbackInfo& args) { + GetField(args, [](const DH* dh) -> const BIGNUM* { + const BIGNUM* pub_key; + DH_get0_key(dh, &pub_key, nullptr); + return pub_key; + }, "No public key - did you forget to generate one?"); +} + +void DiffieHellman::GetPrivateKey(const FunctionCallbackInfo& args) { + GetField(args, [](const DH* dh) -> const BIGNUM* { + const BIGNUM* priv_key; + DH_get0_key(dh, nullptr, &priv_key); + return priv_key; + }, "No private key - did you forget to generate one?"); +} + +void DiffieHellman::ComputeSecret(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + DiffieHellman* diffieHellman; + ASSIGN_OR_RETURN_UNWRAP(&diffieHellman, args.Holder()); + + ClearErrorOnReturn clear_error_on_return; + + CHECK_EQ(args.Length(), 1); + ArrayBufferOrViewContents key_buf(args[0]); + if (UNLIKELY(!key_buf.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "secret is too big"); + BignumPointer key(BN_bin2bn(key_buf.data(), key_buf.size(), nullptr)); + + AllocatedBuffer ret = + AllocatedBuffer::AllocateManaged(env, DH_size(diffieHellman->dh_.get())); + + int size = DH_compute_key(reinterpret_cast(ret.data()), + key.get(), + diffieHellman->dh_.get()); + + if (size == -1) { + int checkResult; + int checked; + + checked = DH_check_pub_key(diffieHellman->dh_.get(), + key.get(), + &checkResult); + + if (!checked) { + return ThrowCryptoError(env, ERR_get_error(), "Invalid Key"); + } else if (checkResult) { + if (checkResult & DH_CHECK_PUBKEY_TOO_SMALL) { + return THROW_ERR_CRYPTO_INVALID_KEYLEN(env, + "Supplied key is too small"); + } else if (checkResult & DH_CHECK_PUBKEY_TOO_LARGE) { + return THROW_ERR_CRYPTO_INVALID_KEYLEN(env, + "Supplied key is too large"); + } + } + + return THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); + } + + CHECK_GE(size, 0); + ZeroPadDiffieHellmanSecret(static_cast(size), &ret); + + args.GetReturnValue().Set(ret.ToBuffer().ToLocalChecked()); +} + +void DiffieHellman::SetKey(const FunctionCallbackInfo& args, + int (*set_field)(DH*, BIGNUM*), const char* what) { + Environment* env = Environment::GetCurrent(args); + DiffieHellman* dh; + ASSIGN_OR_RETURN_UNWRAP(&dh, args.Holder()); + CHECK_EQ(args.Length(), 1); + ArrayBufferOrViewContents buf(args[0]); + if (UNLIKELY(!buf.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "buf is too big"); + BIGNUM* num = BN_bin2bn(buf.data(), buf.size(), nullptr); + CHECK_NOT_NULL(num); + CHECK_EQ(1, set_field(dh->dh_.get(), num)); +} + +void DiffieHellman::SetPublicKey(const FunctionCallbackInfo& args) { + SetKey(args, + [](DH* dh, BIGNUM* num) { return DH_set0_key(dh, num, nullptr); }, + "Public key"); +} + +void DiffieHellman::SetPrivateKey(const FunctionCallbackInfo& args) { + SetKey(args, + [](DH* dh, BIGNUM* num) { return DH_set0_key(dh, nullptr, num); }, + "Private key"); +} + +void DiffieHellman::VerifyErrorGetter(const FunctionCallbackInfo& args) { + HandleScope scope(args.GetIsolate()); + + DiffieHellman* diffieHellman; + ASSIGN_OR_RETURN_UNWRAP(&diffieHellman, args.Holder()); + + args.GetReturnValue().Set(diffieHellman->verifyError_); +} + +bool DiffieHellman::VerifyContext() { + int codes; + if (!DH_check(dh_.get(), &codes)) + return false; + verifyError_ = codes; + return true; +} + +// The input arguments to DhKeyPairGenJob can vary +// 1. CryptoJobMode +// and either +// 2. Group name (as a string) +// or +// 2. Prime or Prime Length +// 3. Generator +// Followed by the public and private key encoding parameters: +// * Public format +// * Public type +// * Private format +// * Private type +// * Cipher +// * Passphrase +Maybe DhKeyGenTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int* offset, + DhKeyPairGenConfig* params) { + Environment* env = Environment::GetCurrent(args); + + if (args[*offset]->IsString()) { + Utf8Value group_name(env->isolate(), args[*offset]); + const modp_group* group = FindDiffieHellmanGroup(*group_name); + if (group == nullptr) { + THROW_ERR_CRYPTO_UNKNOWN_DH_GROUP(env); + return Nothing(); + } + + params->params.prime_fixed_value = BignumPointer( + BN_bin2bn(reinterpret_cast(group->prime), + group->prime_size, nullptr)); + params->params.generator = group->gen; + *offset += 1; + } else { + if (args[*offset]->IsInt32()) { + int size = args[*offset].As()->Value(); + if (size < 0) { + THROW_ERR_OUT_OF_RANGE(env, "Invalid prime size"); + return Nothing(); + } + params->params.prime_size = size; + } else { + ArrayBufferOrViewContents input(args[*offset]); + if (UNLIKELY(!input.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "prime is too big"); + return Nothing(); + } + params->params.prime_fixed_value = BignumPointer( + BN_bin2bn(input.data(), input.size(), nullptr)); + } + + CHECK(args[*offset + 1]->IsInt32()); + params->params.generator = args[*offset + 1].As()->Value(); + *offset += 2; + } + + return Just(true); +} + +EVPKeyCtxPointer DhKeyGenTraits::Setup(DhKeyPairGenConfig* params) { + EVPKeyPointer key_params; + if (params->params.prime_fixed_value) { + DHPointer dh(DH_new()); + if (!dh) + return EVPKeyCtxPointer(); + + BIGNUM* prime = params->params.prime_fixed_value.get(); + BignumPointer bn_g(BN_new()); + if (!BN_set_word(bn_g.get(), params->params.generator) || + !DH_set0_pqg(dh.get(), prime, nullptr, bn_g.get())) + return EVPKeyCtxPointer(); + + params->params.prime_fixed_value.release(); + bn_g.release(); + + key_params = EVPKeyPointer(EVP_PKEY_new()); + CHECK(key_params); + EVP_PKEY_assign_DH(key_params.get(), dh.release()); + } else { + EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_DH, nullptr)); + EVP_PKEY* raw_params = nullptr; + if (!param_ctx || + EVP_PKEY_paramgen_init(param_ctx.get()) <= 0 || + EVP_PKEY_CTX_set_dh_paramgen_prime_len( + param_ctx.get(), + params->params.prime_size) <= 0 || + EVP_PKEY_CTX_set_dh_paramgen_generator( + param_ctx.get(), + params->params.generator) <= 0 || + EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) { + return EVPKeyCtxPointer(); + } + + key_params = EVPKeyPointer(raw_params); + } + + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(key_params.get(), nullptr)); + if (!ctx || EVP_PKEY_keygen_init(ctx.get()) <= 0) + return EVPKeyCtxPointer(); + + return ctx; +} + +Maybe DHKeyExportTraits::AdditionalConfig( + const FunctionCallbackInfo& args, + unsigned int offset, + DHKeyExportConfig* params) { + return Just(true); +} + +WebCryptoKeyExportStatus DHKeyExportTraits::DoExport( + std::shared_ptr key_data, + WebCryptoKeyFormat format, + const DHKeyExportConfig& params, + ByteSource* out) { + CHECK_NE(key_data->GetKeyType(), kKeyTypeSecret); + + switch (format) { + case kWebCryptoKeyFormatPKCS8: + if (key_data->GetKeyType() != kKeyTypePrivate) + return WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE; + return PKEY_PKCS8_Export(key_data.get(), out); + case kWebCryptoKeyFormatSPKI: + if (key_data->GetKeyType() != kKeyTypePublic) + return WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE; + return PKEY_SPKI_Export(key_data.get(), out); + default: + UNREACHABLE(); + } +} + +namespace { +AllocatedBuffer StatelessDiffieHellman( + Environment* env, + ManagedEVPPKey our_key, + ManagedEVPPKey their_key) { + size_t out_size; + + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(our_key.get(), nullptr)); + if (!ctx || + EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_derive_set_peer(ctx.get(), their_key.get()) <= 0 || + EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0) + return AllocatedBuffer(); + + AllocatedBuffer result = AllocatedBuffer::AllocateManaged(env, out_size); + CHECK_NOT_NULL(result.data()); + + unsigned char* data = reinterpret_cast(result.data()); + if (EVP_PKEY_derive(ctx.get(), data, &out_size) <= 0) + return AllocatedBuffer(); + + ZeroPadDiffieHellmanSecret(out_size, &result); + return result; +} + +// The version of StatelessDiffieHellman that returns an AllocatedBuffer +// is not threadsafe because of the AllocatedBuffer allocation of a +// v8::BackingStore (it'll cause much crashing if we call it from a +// libuv worker thread). This version allocates a ByteSource instead, +// which we can convert into a v8::BackingStore later. +// TODO(@jasnell): Eliminate the code duplication between these two +// versions of the function. +ByteSource StatelessDiffieHellmanThreadsafe( + Environment* env, + ManagedEVPPKey our_key, + ManagedEVPPKey their_key) { + size_t out_size; + + EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(our_key.get(), nullptr)); + if (!ctx || + EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_derive_set_peer(ctx.get(), their_key.get()) <= 0 || + EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0) + return ByteSource(); + + char* buf = MallocOpenSSL(out_size); + ByteSource out = ByteSource::Allocated(buf, out_size); + + if (EVP_PKEY_derive( + ctx.get(), + reinterpret_cast(buf), + &out_size) <= 0) { + return ByteSource(); + } + + ZeroPadDiffieHellmanSecret(out_size, buf, out.size()); + return out; +} +} // namespace + +void DiffieHellman::Stateless(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args[0]->IsObject() && args[1]->IsObject()); + KeyObjectHandle* our_key_object; + ASSIGN_OR_RETURN_UNWRAP(&our_key_object, args[0].As()); + CHECK_EQ(our_key_object->Data()->GetKeyType(), kKeyTypePrivate); + KeyObjectHandle* their_key_object; + ASSIGN_OR_RETURN_UNWRAP(&their_key_object, args[1].As()); + CHECK_NE(their_key_object->Data()->GetKeyType(), kKeyTypeSecret); + + ManagedEVPPKey our_key = our_key_object->Data()->GetAsymmetricKey(); + ManagedEVPPKey their_key = their_key_object->Data()->GetAsymmetricKey(); + + AllocatedBuffer out = StatelessDiffieHellman(env, our_key, their_key); + if (out.size() == 0) + return ThrowCryptoError(env, ERR_get_error(), "diffieHellman failed"); + + args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); +} + +Maybe DHBitsTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + DHBitsConfig* params) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args[offset]->IsObject()); // public key + CHECK(args[offset + 1]->IsObject()); // private key + + KeyObjectHandle* private_key; + KeyObjectHandle* public_key; + + ASSIGN_OR_RETURN_UNWRAP(&public_key, args[offset], Nothing()); + ASSIGN_OR_RETURN_UNWRAP(&private_key, args[offset + 1], Nothing()); + + if (private_key->Data()->GetKeyType() != kKeyTypePrivate || + public_key->Data()->GetKeyType() != kKeyTypePublic) { + THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); + return Nothing(); + } + + params->public_key = public_key->Data(); + params->private_key = private_key->Data(); + + return Just(true); +} + +Maybe DHBitsTraits::EncodeOutput( + Environment* env, + const DHBitsConfig& params, + ByteSource* out, + v8::Local* result) { + *result = out->ToArrayBuffer(env); + return Just(!result->IsEmpty()); +} + +bool DHBitsTraits::DeriveBits( + Environment* env, + const DHBitsConfig& params, + ByteSource* out) { + *out = StatelessDiffieHellmanThreadsafe( + env, + params.private_key->GetAsymmetricKey(), + params.public_key->GetAsymmetricKey()); + return true; +} + +Maybe GetDhKeyDetail( + Environment* env, + std::shared_ptr key, + Local target) { + ManagedEVPPKey pkey = key->GetAsymmetricKey(); + CHECK_EQ(EVP_PKEY_id(pkey.get()), EVP_PKEY_DH); + return Just(true); +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_dh.h b/src/crypto/crypto_dh.h new file mode 100644 index 00000000000000..7c69afb0a46ab4 --- /dev/null +++ b/src/crypto/crypto_dh.h @@ -0,0 +1,157 @@ +#ifndef SRC_CRYPTO_CRYPTO_DH_H_ +#define SRC_CRYPTO_CRYPTO_DH_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "crypto/crypto_keygen.h" +#include "crypto/crypto_util.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +#include + +namespace node { +namespace crypto { +class DiffieHellman : public BaseObject { + public: + static void Initialize(Environment* env, v8::Local target); + + bool Init(int primeLength, int g); + bool Init(const char* p, int p_len, int g); + bool Init(const char* p, int p_len, const char* g, int g_len); + + static void Stateless(const v8::FunctionCallbackInfo& args); + + protected: + static void DiffieHellmanGroup( + const v8::FunctionCallbackInfo& args); + static void New(const v8::FunctionCallbackInfo& args); + static void GenerateKeys(const v8::FunctionCallbackInfo& args); + static void GetPrime(const v8::FunctionCallbackInfo& args); + static void GetGenerator(const v8::FunctionCallbackInfo& args); + static void GetPublicKey(const v8::FunctionCallbackInfo& args); + static void GetPrivateKey(const v8::FunctionCallbackInfo& args); + static void ComputeSecret(const v8::FunctionCallbackInfo& args); + static void SetPublicKey(const v8::FunctionCallbackInfo& args); + static void SetPrivateKey(const v8::FunctionCallbackInfo& args); + static void VerifyErrorGetter( + const v8::FunctionCallbackInfo& args); + + DiffieHellman(Environment* env, v8::Local wrap); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(DiffieHellman) + SET_SELF_SIZE(DiffieHellman) + + private: + static void GetField(const v8::FunctionCallbackInfo& args, + const BIGNUM* (*get_field)(const DH*), + const char* err_if_null); + static void SetKey(const v8::FunctionCallbackInfo& args, + int (*set_field)(DH*, BIGNUM*), const char* what); + bool VerifyContext(); + + int verifyError_; + DHPointer dh_; +}; + +struct DhKeyPairParams final : public MemoryRetainer { + // TODO(tniessen): Use std::variant instead. + // Diffie-Hellman can either generate keys using a fixed prime, or by first + // generating a random prime of a given size (in bits). Only one of both + // options may be specified. + BignumPointer prime_fixed_value; + unsigned int prime_size; + unsigned int generator; + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(DhKeyPairParams) + SET_SELF_SIZE(DhKeyPairParams) +}; + +using DhKeyPairGenConfig = KeyPairGenConfig; + +struct DhKeyGenTraits final { + using AdditionalParameters = DhKeyPairGenConfig; + static constexpr const char* JobName = "DhKeyPairGenJob"; + + static EVPKeyCtxPointer Setup(DhKeyPairGenConfig* params); + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + DhKeyPairGenConfig* params); +}; + +using DHKeyPairGenJob = KeyGenJob>; + +struct DHKeyExportConfig final : public MemoryRetainer { + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(DHKeyExportConfig) + SET_SELF_SIZE(DHKeyExportConfig) +}; + +struct DHKeyExportTraits final { + static constexpr const char* JobName = "DHKeyExportJob"; + using AdditionalParameters = DHKeyExportConfig; + + static v8::Maybe AdditionalConfig( + const v8::FunctionCallbackInfo& args, + unsigned int offset, + DHKeyExportConfig* config); + + static WebCryptoKeyExportStatus DoExport( + std::shared_ptr key_data, + WebCryptoKeyFormat format, + const DHKeyExportConfig& params, + ByteSource* out); +}; + +using DHKeyExportJob = KeyExportJob; + +struct DHBitsConfig final : public MemoryRetainer { + std::shared_ptr private_key; + std::shared_ptr public_key; + SET_NO_MEMORY_INFO(); + SET_MEMORY_INFO_NAME(DHBitsConfig); + SET_SELF_SIZE(DHBitsConfig); +}; + +struct DHBitsTraits final { + using AdditionalParameters = DHBitsConfig; + static constexpr const char* JobName = "DHBitsJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_DERIVEBITSREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + DHBitsConfig* params); + + static bool DeriveBits( + Environment* env, + const DHBitsConfig& params, + ByteSource* out_); + + static v8::Maybe EncodeOutput( + Environment* env, + const DHBitsConfig& params, + ByteSource* out, + v8::Local* result); +}; + +using DHBitsJob = DeriveBitsJob; + +v8::Maybe GetDhKeyDetail( + Environment* env, + std::shared_ptr key, + v8::Local target); + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_DH_H_ diff --git a/src/crypto/crypto_dsa.cc b/src/crypto/crypto_dsa.cc new file mode 100644 index 00000000000000..1dc0a86fb4b10c --- /dev/null +++ b/src/crypto/crypto_dsa.cc @@ -0,0 +1,272 @@ +#include "crypto/crypto_dsa.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "async_wrap-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +#include +#include + +#include + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Int32; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Number; +using v8::Object; +using v8::String; +using v8::Uint32; +using v8::Value; + +namespace crypto { +EVPKeyCtxPointer DsaKeyGenTraits::Setup(DsaKeyPairGenConfig* params) { + EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_DSA, nullptr)); + EVP_PKEY* raw_params = nullptr; + + if (!param_ctx || + EVP_PKEY_paramgen_init(param_ctx.get()) <= 0 || + EVP_PKEY_CTX_set_dsa_paramgen_bits( + param_ctx.get(), + params->params.modulus_bits) <= 0) { + return EVPKeyCtxPointer(); + } + + if (params->params.divisor_bits != -1) { + if (EVP_PKEY_CTX_ctrl( + param_ctx.get(), + EVP_PKEY_DSA, + EVP_PKEY_OP_PARAMGEN, + EVP_PKEY_CTRL_DSA_PARAMGEN_Q_BITS, + params->params.divisor_bits, + nullptr) <= 0) { + return EVPKeyCtxPointer(); + } + } + + if (EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) + return EVPKeyCtxPointer(); + + EVPKeyPointer key_params(raw_params); + EVPKeyCtxPointer key_ctx(EVP_PKEY_CTX_new(key_params.get(), nullptr)); + + if (!key_ctx || EVP_PKEY_keygen_init(key_ctx.get()) <= 0) + return EVPKeyCtxPointer(); + + return key_ctx; +} + +// Input arguments for DsaKeyPairGenJob +// 1. CryptoJobMode +// 2. Modulus Bits +// 3. Divisor Bits +// 4. Public Format +// 5. Public Type +// 6. Private Format +// 7. Private Type +// 8. Cipher +// 9. Passphrase +Maybe DsaKeyGenTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int* offset, + DsaKeyPairGenConfig* params) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[*offset]->IsUint32()); // modulus bits + CHECK(args[*offset + 1]->IsInt32()); // divisor bits + + params->params.modulus_bits = args[*offset].As()->Value(); + params->params.divisor_bits = args[*offset + 1].As()->Value(); + if (params->params.divisor_bits < -1) { + char msg[1024]; + snprintf(msg, sizeof(msg), "invalid value for divisor_bits"); + THROW_ERR_OUT_OF_RANGE(env, msg); + return Nothing(); + } + + *offset += 2; + + return Just(true); +} + +Maybe DSAKeyExportTraits::AdditionalConfig( + const FunctionCallbackInfo& args, + unsigned int offset, + DSAKeyExportConfig* params) { + return Just(true); +} + +WebCryptoKeyExportStatus DSAKeyExportTraits::DoExport( + std::shared_ptr key_data, + WebCryptoKeyFormat format, + const DSAKeyExportConfig& params, + ByteSource* out) { + CHECK_NE(key_data->GetKeyType(), kKeyTypeSecret); + + switch (format) { + case kWebCryptoKeyFormatRaw: + // Not supported for RSA keys of either type + return WebCryptoKeyExportStatus::ERR_FAILED; + case kWebCryptoKeyFormatPKCS8: + if (key_data->GetKeyType() != kKeyTypePrivate) + return WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE; + return PKEY_PKCS8_Export(key_data.get(), out); + case kWebCryptoKeyFormatSPKI: + if (key_data->GetKeyType() != kKeyTypePublic) + return WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE; + return PKEY_SPKI_Export(key_data.get(), out); + default: + UNREACHABLE(); + } +} + +Maybe ExportJWKDsaKey( + Environment* env, + std::shared_ptr key, + Local target) { + ManagedEVPPKey pkey = key->GetAsymmetricKey(); + CHECK_EQ(EVP_PKEY_id(pkey.get()), EVP_PKEY_DSA); + + DSA* dsa = EVP_PKEY_get0_DSA(pkey.get()); + CHECK_NOT_NULL(dsa); + + const BIGNUM* y; + const BIGNUM* x; + const BIGNUM* p; + const BIGNUM* q; + const BIGNUM* g; + + DSA_get0_key(dsa, &y, &x); + DSA_get0_pqg(dsa, &p, &q, &g); + + if (target->Set( + env->context(), + env->jwk_kty_string(), + env->jwk_dsa_string()).IsNothing()) { + return Nothing(); + } + + if (SetEncodedValue(env, target, env->jwk_y_string(), y).IsNothing() || + SetEncodedValue(env, target, env->jwk_p_string(), p).IsNothing() || + SetEncodedValue(env, target, env->jwk_q_string(), q).IsNothing() || + SetEncodedValue(env, target, env->jwk_g_string(), g).IsNothing()) { + return Nothing(); + } + + if (key->GetKeyType() == kKeyTypePrivate && + SetEncodedValue(env, target, env->jwk_x_string(), x).IsNothing()) { + return Nothing(); + } + + return Just(true); +} + +std::shared_ptr ImportJWKDsaKey( + Environment* env, + Local jwk, + const FunctionCallbackInfo& args, + unsigned int offset) { + Local y_value; + Local p_value; + Local q_value; + Local g_value; + Local x_value; + + if (!jwk->Get(env->context(), env->jwk_y_string()).ToLocal(&y_value) || + !jwk->Get(env->context(), env->jwk_p_string()).ToLocal(&p_value) || + !jwk->Get(env->context(), env->jwk_q_string()).ToLocal(&q_value) || + !jwk->Get(env->context(), env->jwk_g_string()).ToLocal(&g_value) || + !jwk->Get(env->context(), env->jwk_x_string()).ToLocal(&x_value)) { + return std::shared_ptr(); + } + + if (!y_value->IsString() || + !p_value->IsString() || + !q_value->IsString() || + !q_value->IsString() || + (!x_value->IsUndefined() && !x_value->IsString())) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK DSA key"); + return std::shared_ptr(); + } + + KeyType type = x_value->IsString() ? kKeyTypePrivate : kKeyTypePublic; + + DsaPointer dsa(DSA_new()); + + ByteSource y = ByteSource::FromEncodedString(env, y_value.As()); + ByteSource p = ByteSource::FromEncodedString(env, p_value.As()); + ByteSource q = ByteSource::FromEncodedString(env, q_value.As()); + ByteSource g = ByteSource::FromEncodedString(env, g_value.As()); + + if (!DSA_set0_key(dsa.get(), y.ToBN().release(), nullptr) || + !DSA_set0_pqg(dsa.get(), + p.ToBN().release(), + q.ToBN().release(), + g.ToBN().release())) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK DSA key"); + return std::shared_ptr(); + } + + if (type == kKeyTypePrivate) { + ByteSource x = ByteSource::FromEncodedString(env, x_value.As()); + if (!DSA_set0_key(dsa.get(), nullptr, x.ToBN().release())) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK DSA key"); + return std::shared_ptr(); + } + } + + EVPKeyPointer pkey(EVP_PKEY_new()); + CHECK_EQ(EVP_PKEY_set1_DSA(pkey.get(), dsa.get()), 1); + + return KeyObjectData::CreateAsymmetric(type, ManagedEVPPKey(std::move(pkey))); +} + +Maybe GetDsaKeyDetail( + Environment* env, + std::shared_ptr key, + Local target) { + const BIGNUM* p; // Modulus length + const BIGNUM* q; // Divisor length + + ManagedEVPPKey pkey = key->GetAsymmetricKey(); + int type = EVP_PKEY_id(pkey.get()); + CHECK(type == EVP_PKEY_DSA); + + DSA* dsa = EVP_PKEY_get0_DSA(pkey.get()); + CHECK_NOT_NULL(dsa); + + DSA_get0_pqg(dsa, &p, &q, nullptr); + + size_t modulus_length = BN_num_bytes(p) * CHAR_BIT; + size_t divisor_length = BN_num_bytes(q) * CHAR_BIT; + + if (target->Set( + env->context(), + env->modulus_length_string(), + Number::New(env->isolate(), modulus_length)).IsNothing() || + target->Set( + env->context(), + env->divisor_length_string(), + Number::New(env->isolate(), divisor_length)).IsNothing()) { + return Nothing(); + } + + return Just(true); +} + +namespace DSAAlg { +void Initialize(Environment* env, Local target) { + DsaKeyPairGenJob::Initialize(env, target); + DSAKeyExportJob::Initialize(env, target); +} +} // namespace DSAAlg +} // namespace crypto +} // namespace node + diff --git a/src/crypto/crypto_dsa.h b/src/crypto/crypto_dsa.h new file mode 100644 index 00000000000000..b685d6a88f9412 --- /dev/null +++ b/src/crypto/crypto_dsa.h @@ -0,0 +1,87 @@ +#ifndef SRC_CRYPTO_CRYPTO_DSA_H_ +#define SRC_CRYPTO_CRYPTO_DSA_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "crypto/crypto_keygen.h" +#include "crypto/crypto_util.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +namespace node { +namespace crypto { +struct DsaKeyPairParams final : public MemoryRetainer { + unsigned int modulus_bits; + int divisor_bits; + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(DsaKeyPairParams); + SET_SELF_SIZE(DsaKeyPairParams); +}; + +using DsaKeyPairGenConfig = KeyPairGenConfig; + +struct DsaKeyGenTraits final { + using AdditionalParameters = DsaKeyPairGenConfig; + static constexpr const char* JobName = "DsaKeyPairGenJob"; + + static EVPKeyCtxPointer Setup(DsaKeyPairGenConfig* params); + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + DsaKeyPairGenConfig* params); +}; + +using DsaKeyPairGenJob = KeyGenJob>; + +struct DSAKeyExportConfig final : public MemoryRetainer { + SET_NO_MEMORY_INFO(); + SET_MEMORY_INFO_NAME(DSAKeyExportConfig); + SET_SELF_SIZE(DSAKeyExportConfig); +}; + +struct DSAKeyExportTraits final { + static constexpr const char* JobName = "DSAKeyExportJob"; + using AdditionalParameters = DSAKeyExportConfig; + + static v8::Maybe AdditionalConfig( + const v8::FunctionCallbackInfo& args, + unsigned int offset, + DSAKeyExportConfig* config); + + static WebCryptoKeyExportStatus DoExport( + std::shared_ptr key_data, + WebCryptoKeyFormat format, + const DSAKeyExportConfig& params, + ByteSource* out); +}; + +using DSAKeyExportJob = KeyExportJob; + +v8::Maybe ExportJWKDsaKey( + Environment* env, + std::shared_ptr key, + v8::Local target); + +std::shared_ptr ImportJWKDsaKey( + Environment* env, + v8::Local jwk, + const v8::FunctionCallbackInfo& args, + unsigned int offset); + +v8::Maybe GetDsaKeyDetail( + Environment* env, + std::shared_ptr key, + v8::Local target); + +namespace DSAAlg { +void Initialize(Environment* env, v8::Local target); +} // namespace DSAAlg +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_DSA_H_ diff --git a/src/crypto/crypto_ecdh.cc b/src/crypto/crypto_ecdh.cc new file mode 100644 index 00000000000000..07ce1c21480e24 --- /dev/null +++ b/src/crypto/crypto_ecdh.cc @@ -0,0 +1,814 @@ +#include "crypto/crypto_ecdh.h" +#include "crypto/crypto_common.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_buffer.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +#include +#include +#include + +namespace node { + +using v8::Array; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Int32; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Object; +using v8::String; +using v8::Uint32; +using v8::Value; + +namespace crypto { +namespace { +int GetCurveFromName(const char* name) { + int nid = EC_curve_nist2nid(name); + if (nid == NID_undef) + nid = OBJ_sn2nid(name); + return nid; +} +} // namespace + +void ECDH::Initialize(Environment* env, Local target) { + Local t = env->NewFunctionTemplate(New); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + + t->InstanceTemplate()->SetInternalFieldCount(ECDH::kInternalFieldCount); + + env->SetProtoMethod(t, "generateKeys", GenerateKeys); + env->SetProtoMethod(t, "computeSecret", ComputeSecret); + env->SetProtoMethodNoSideEffect(t, "getPublicKey", GetPublicKey); + env->SetProtoMethodNoSideEffect(t, "getPrivateKey", GetPrivateKey); + env->SetProtoMethod(t, "setPublicKey", SetPublicKey); + env->SetProtoMethod(t, "setPrivateKey", SetPrivateKey); + + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "ECDH"), + t->GetFunction(env->context()).ToLocalChecked()).Check(); + + env->SetMethodNoSideEffect(target, "ECDHConvertKey", ECDH::ConvertKey); + env->SetMethodNoSideEffect(target, "getCurves", ECDH::GetCurves); + + ECDHBitsJob::Initialize(env, target); + ECKeyPairGenJob::Initialize(env, target); + ECKeyExportJob::Initialize(env, target); + + NODE_DEFINE_CONSTANT(target, OPENSSL_EC_NAMED_CURVE); + NODE_DEFINE_CONSTANT(target, OPENSSL_EC_EXPLICIT_CURVE); +} + +void ECDH::GetCurves(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + const size_t num_curves = EC_get_builtin_curves(nullptr, 0); + + if (num_curves) { + std::vector curves(num_curves); + + if (EC_get_builtin_curves(curves.data(), num_curves)) { + std::vector> arr(num_curves); + + for (size_t i = 0; i < num_curves; i++) + arr[i] = OneByteString(env->isolate(), OBJ_nid2sn(curves[i].nid)); + + args.GetReturnValue().Set( + Array::New(env->isolate(), arr.data(), arr.size())); + return; + } + } + + args.GetReturnValue().Set(Array::New(env->isolate())); +} + +ECDH::ECDH(Environment* env, Local wrap, ECKeyPointer&& key) + : BaseObject(env, wrap), + key_(std::move(key)), + group_(EC_KEY_get0_group(key_.get())) { + MakeWeak(); + CHECK_NOT_NULL(group_); +} + +void ECDH::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("key", key_ ? kSizeOf_EC_KEY : 0); +} + +ECDH::~ECDH() {} + +void ECDH::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + MarkPopErrorOnReturn mark_pop_error_on_return; + + // TODO(indutny): Support raw curves? + CHECK(args[0]->IsString()); + node::Utf8Value curve(env->isolate(), args[0]); + + int nid = OBJ_sn2nid(*curve); + if (nid == NID_undef) + return THROW_ERR_CRYPTO_INVALID_CURVE(env); + + ECKeyPointer key(EC_KEY_new_by_curve_name(nid)); + if (!key) + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to create key using named curve"); + + new ECDH(env, args.This(), std::move(key)); +} + +void ECDH::GenerateKeys(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + ECDH* ecdh; + ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); + + if (!EC_KEY_generate_key(ecdh->key_.get())) + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to generate key"); +} + +ECPointPointer ECDH::BufferToPoint(Environment* env, + const EC_GROUP* group, + Local buf) { + int r; + + ECPointPointer pub(EC_POINT_new(group)); + if (!pub) { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to allocate EC_POINT for a public key"); + return pub; + } + + ArrayBufferOrViewContents input(buf); + if (UNLIKELY(!input.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "buffer is too big"); + return ECPointPointer(); + } + r = EC_POINT_oct2point( + group, + pub.get(), + input.data(), + input.size(), + nullptr); + if (!r) + return ECPointPointer(); + + return pub; +} + +void ECDH::ComputeSecret(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK(IsAnyByteSource(args[0])); + + ECDH* ecdh; + ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); + + MarkPopErrorOnReturn mark_pop_error_on_return; + + if (!ecdh->IsKeyPairValid()) + return THROW_ERR_CRYPTO_INVALID_KEYPAIR(env); + + ECPointPointer pub( + ECDH::BufferToPoint(env, + ecdh->group_, + args[0])); + if (!pub) { + args.GetReturnValue().Set( + FIXED_ONE_BYTE_STRING(env->isolate(), + "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY")); + return; + } + + // NOTE: field_size is in bits + int field_size = EC_GROUP_get_degree(ecdh->group_); + size_t out_len = (field_size + 7) / 8; + AllocatedBuffer out = AllocatedBuffer::AllocateManaged(env, out_len); + + int r = ECDH_compute_key( + out.data(), out_len, pub.get(), ecdh->key_.get(), nullptr); + if (!r) + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to compute ECDH key"); + + Local buf = out.ToBuffer().ToLocalChecked(); + args.GetReturnValue().Set(buf); +} + +void ECDH::GetPublicKey(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + // Conversion form + CHECK_EQ(args.Length(), 1); + + ECDH* ecdh; + ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); + + const EC_GROUP* group = EC_KEY_get0_group(ecdh->key_.get()); + const EC_POINT* pub = EC_KEY_get0_public_key(ecdh->key_.get()); + if (pub == nullptr) + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to get ECDH public key"); + + CHECK(args[0]->IsUint32()); + uint32_t val = args[0].As()->Value(); + point_conversion_form_t form = static_cast(val); + + const char* error; + Local buf; + if (!ECPointToBuffer(env, group, pub, form, &error).ToLocal(&buf)) + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, error); + args.GetReturnValue().Set(buf); +} + +void ECDH::GetPrivateKey(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + ECDH* ecdh; + ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); + + const BIGNUM* b = EC_KEY_get0_private_key(ecdh->key_.get()); + if (b == nullptr) + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to get ECDH private key"); + + const int size = BN_num_bytes(b); + AllocatedBuffer out = AllocatedBuffer::AllocateManaged(env, size); + CHECK_EQ(size, BN_bn2binpad(b, + reinterpret_cast(out.data()), + size)); + + Local buf = out.ToBuffer().ToLocalChecked(); + args.GetReturnValue().Set(buf); +} + +void ECDH::SetPrivateKey(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + ECDH* ecdh; + ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); + + ArrayBufferOrViewContents priv_buffer(args[0]); + if (UNLIKELY(!priv_buffer.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "key is too big"); + + BignumPointer priv(BN_bin2bn( + priv_buffer.data(), priv_buffer.size(), nullptr)); + if (!priv) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to convert Buffer to BN"); + } + + if (!ecdh->IsKeyValidForCurve(priv)) { + return THROW_ERR_CRYPTO_INVALID_KEYTYPE(env, + "Private key is not valid for specified curve."); + } + + ECKeyPointer new_key(EC_KEY_dup(ecdh->key_.get())); + CHECK(new_key); + + int result = EC_KEY_set_private_key(new_key.get(), priv.get()); + priv.reset(); + + if (!result) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to convert BN to a private key"); + } + + MarkPopErrorOnReturn mark_pop_error_on_return; + USE(&mark_pop_error_on_return); + + const BIGNUM* priv_key = EC_KEY_get0_private_key(new_key.get()); + CHECK_NOT_NULL(priv_key); + + ECPointPointer pub(EC_POINT_new(ecdh->group_)); + CHECK(pub); + + if (!EC_POINT_mul(ecdh->group_, pub.get(), priv_key, + nullptr, nullptr, nullptr)) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to generate ECDH public key"); + } + + if (!EC_KEY_set_public_key(new_key.get(), pub.get())) + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to set generated public key"); + + EC_KEY_copy(ecdh->key_.get(), new_key.get()); + ecdh->group_ = EC_KEY_get0_group(ecdh->key_.get()); +} + +void ECDH::SetPublicKey(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + ECDH* ecdh; + ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); + + CHECK(IsAnyByteSource(args[0])); + + MarkPopErrorOnReturn mark_pop_error_on_return; + + ECPointPointer pub( + ECDH::BufferToPoint(env, + ecdh->group_, + args[0])); + if (!pub) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to convert Buffer to EC_POINT"); + } + + int r = EC_KEY_set_public_key(ecdh->key_.get(), pub.get()); + if (!r) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to set EC_POINT as the public key"); + } +} + +bool ECDH::IsKeyValidForCurve(const BignumPointer& private_key) { + CHECK(group_); + CHECK(private_key); + // Private keys must be in the range [1, n-1]. + // Ref: Section 3.2.1 - http://www.secg.org/sec1-v2.pdf + if (BN_cmp(private_key.get(), BN_value_one()) < 0) { + return false; + } + BignumPointer order(BN_new()); + CHECK(order); + return EC_GROUP_get_order(group_, order.get(), nullptr) && + BN_cmp(private_key.get(), order.get()) < 0; +} + +bool ECDH::IsKeyPairValid() { + MarkPopErrorOnReturn mark_pop_error_on_return; + USE(&mark_pop_error_on_return); + return 1 == EC_KEY_check_key(key_.get()); +} + +// Convert the input public key to compressed, uncompressed, or hybrid formats. +void ECDH::ConvertKey(const FunctionCallbackInfo& args) { + MarkPopErrorOnReturn mark_pop_error_on_return; + Environment* env = Environment::GetCurrent(args); + + CHECK_EQ(args.Length(), 3); + CHECK(IsAnyByteSource(args[0])); + + ArrayBufferOrViewContents args0(args[0]); + if (UNLIKELY(!args0.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "key is too big"); + if (args0.size() == 0) + return args.GetReturnValue().SetEmptyString(); + + node::Utf8Value curve(env->isolate(), args[1]); + + int nid = OBJ_sn2nid(*curve); + if (nid == NID_undef) + return THROW_ERR_CRYPTO_INVALID_CURVE(env); + + ECGroupPointer group( + EC_GROUP_new_by_curve_name(nid)); + if (group == nullptr) + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get EC_GROUP"); + + ECPointPointer pub( + ECDH::BufferToPoint(env, + group.get(), + args[0])); + + if (pub == nullptr) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to convert Buffer to EC_POINT"); + } + + CHECK(args[2]->IsUint32()); + uint32_t val = args[2].As()->Value(); + point_conversion_form_t form = static_cast(val); + + const char* error; + Local buf; + if (!ECPointToBuffer(env, group.get(), pub.get(), form, &error).ToLocal(&buf)) + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, error); + args.GetReturnValue().Set(buf); +} + +Maybe ECDHBitsTraits::EncodeOutput( + Environment* env, + const ECDHBitsConfig& params, + ByteSource* out, + v8::Local* result) { + *result = out->ToArrayBuffer(env); + return Just(!result->IsEmpty()); +} + +Maybe ECDHBitsTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + ECDHBitsConfig* params) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args[offset]->IsString()); // curve name + CHECK(args[offset + 1]->IsObject()); // public key + CHECK(args[offset + 2]->IsObject()); // private key + + KeyObjectHandle* private_key; + KeyObjectHandle* public_key; + + Utf8Value name(env->isolate(), args[offset]); + ASSIGN_OR_RETURN_UNWRAP(&public_key, args[offset + 1], Nothing()); + ASSIGN_OR_RETURN_UNWRAP(&private_key, args[offset + 2], Nothing()); + + if (private_key->Data()->GetKeyType() != kKeyTypePrivate || + public_key->Data()->GetKeyType() != kKeyTypePublic) { + THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); + return Nothing(); + } + + params->private_key = ECKeyPointer( + EC_KEY_dup( + EVP_PKEY_get1_EC_KEY(private_key->Data()->GetAsymmetricKey().get()))); + if (!params->private_key) { + THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); + return Nothing(); + } + + params->public_key = ECKeyPointer( + EC_KEY_dup( + EVP_PKEY_get1_EC_KEY(public_key->Data()->GetAsymmetricKey().get()))); + if (!params->public_key) { + THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); + return Nothing(); + } + + params->group = EC_KEY_get0_group(params->private_key.get()); + + return Just(true); +} + +bool ECDHBitsTraits::DeriveBits( + Environment* env, + const ECDHBitsConfig& params, + ByteSource* out) { + if (params.group == nullptr) + return false; + CHECK_EQ(EC_KEY_check_key(params.private_key.get()), 1); + CHECK_EQ(EC_KEY_check_key(params.public_key.get()), 1); + const EC_POINT* pub = EC_KEY_get0_public_key(params.public_key.get()); + int field_size = EC_GROUP_get_degree(params.group); + size_t len = (field_size + 7) / 8; + char* data = MallocOpenSSL(len); + ByteSource buf = ByteSource::Allocated(data, len); + if (ECDH_compute_key( + data, + len, + pub, + params.private_key.get(), + nullptr) <= 0) { + return false; + } + *out = std::move(buf); + return true; +} + +EVPKeyCtxPointer EcKeyGenTraits::Setup(EcKeyPairGenConfig* params) { + EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr)); + EVP_PKEY* raw_params = nullptr; + if (!param_ctx || + EVP_PKEY_paramgen_init(param_ctx.get()) <= 0 || + EVP_PKEY_CTX_set_ec_paramgen_curve_nid( + param_ctx.get(), params->params.curve_nid) <= 0 || + EVP_PKEY_CTX_set_ec_param_enc( + param_ctx.get(), params->params.param_encoding) <= 0 || + EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) { + return EVPKeyCtxPointer(); + } + EVPKeyPointer key_params(raw_params); + EVPKeyCtxPointer key_ctx(EVP_PKEY_CTX_new(key_params.get(), nullptr)); + + if (!key_ctx || EVP_PKEY_keygen_init(key_ctx.get()) <= 0) + return EVPKeyCtxPointer(); + + return key_ctx; +} + +// EcKeyPairGenJob input arguments +// 1. CryptoJobMode +// 2. Curve Name +// 3. Param Encoding +// 4. Public Format +// 5. Public Type +// 6. Private Format +// 7. Private Type +// 8. Cipher +// 9. Passphrase +Maybe EcKeyGenTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int* offset, + EcKeyPairGenConfig* params) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[*offset]->IsString()); // curve name + CHECK(args[*offset + 1]->IsInt32()); // param encoding + + Utf8Value curve_name(env->isolate(), args[*offset]); + params->params.curve_nid = GetCurveFromName(*curve_name); + if (params->params.curve_nid == NID_undef) { + THROW_ERR_CRYPTO_INVALID_CURVE(env); + return Nothing(); + } + + params->params.param_encoding = args[*offset + 1].As()->Value(); + if (params->params.param_encoding != OPENSSL_EC_NAMED_CURVE && + params->params.param_encoding != OPENSSL_EC_EXPLICIT_CURVE) { + THROW_ERR_OUT_OF_RANGE(env, "Invalid param_encoding specified"); + return Nothing(); + } + + *offset += 2; + + return Just(true); +} + +namespace { +WebCryptoKeyExportStatus EC_Raw_Export( + KeyObjectData* key_data, + const ECKeyExportConfig& params, + ByteSource* out) { + CHECK(key_data->GetAsymmetricKey()); + + EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(key_data->GetAsymmetricKey().get()); + CHECK_NOT_NULL(ec_key); + + const EC_GROUP* group = EC_KEY_get0_group(ec_key); + const EC_POINT* point = EC_KEY_get0_public_key(ec_key); + point_conversion_form_t form = POINT_CONVERSION_UNCOMPRESSED; + + // Get the allocated data size... + size_t len = EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr); + if (len == 0) + return WebCryptoKeyExportStatus::ERR_FAILED; + + unsigned char* data = MallocOpenSSL(len); + size_t check_len = EC_POINT_point2oct(group, point, form, data, len, nullptr); + if (check_len == 0) + return WebCryptoKeyExportStatus::ERR_FAILED; + + CHECK_EQ(len, check_len); + + *out = ByteSource::Allocated(reinterpret_cast(data), len); + + return WebCryptoKeyExportStatus::ERR_OK; +} +} // namespace + +Maybe ECKeyExportTraits::AdditionalConfig( + const FunctionCallbackInfo& args, + unsigned int offset, + ECKeyExportConfig* params) { + return Just(true); +} + +WebCryptoKeyExportStatus ECKeyExportTraits::DoExport( + std::shared_ptr key_data, + WebCryptoKeyFormat format, + const ECKeyExportConfig& params, + ByteSource* out) { + CHECK_NE(key_data->GetKeyType(), kKeyTypeSecret); + + switch (format) { + case kWebCryptoKeyFormatRaw: + if (key_data->GetKeyType() != kKeyTypePublic) + return WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE; + return EC_Raw_Export(key_data.get(), params, out); + case kWebCryptoKeyFormatPKCS8: + if (key_data->GetKeyType() != kKeyTypePrivate) + return WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE; + return PKEY_PKCS8_Export(key_data.get(), out); + case kWebCryptoKeyFormatSPKI: + if (key_data->GetKeyType() != kKeyTypePublic) + return WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE; + return PKEY_SPKI_Export(key_data.get(), out); + default: + UNREACHABLE(); + } +} + +Maybe ExportJWKEcKey( + Environment* env, + std::shared_ptr key, + Local target) { + ManagedEVPPKey pkey = key->GetAsymmetricKey(); + CHECK_EQ(EVP_PKEY_id(pkey.get()), EVP_PKEY_EC); + + EC_KEY* ec = EVP_PKEY_get0_EC_KEY(pkey.get()); + CHECK_NOT_NULL(ec); + + const EC_POINT* pub = EC_KEY_get0_public_key(ec); + const EC_GROUP* group = EC_KEY_get0_group(ec); + + int degree_bits = EC_GROUP_get_degree(group); + int degree_bytes = + (degree_bits / CHAR_BIT) + (7 + (degree_bits % CHAR_BIT)) / 8; + + BignumPointer x(BN_new()); + BignumPointer y(BN_new()); + + EC_POINT_get_affine_coordinates(group, pub, x.get(), y.get(), nullptr); + + if (target->Set( + env->context(), + env->jwk_kty_string(), + env->jwk_ec_string()).IsNothing()) { + return Nothing(); + } + + if (SetEncodedValue( + env, + target, + env->jwk_x_string(), + x.get(), + degree_bytes).IsNothing() || + SetEncodedValue( + env, + target, + env->jwk_y_string(), + y.get(), + degree_bytes).IsNothing()) { + return Nothing(); + } + + if (key->GetKeyType() == kKeyTypePrivate) { + const BIGNUM* pvt = EC_KEY_get0_private_key(ec); + return SetEncodedValue( + env, + target, + env->jwk_d_string(), + pvt, + degree_bytes); + } + + return Just(true); +} + +std::shared_ptr ImportJWKEcKey( + Environment* env, + Local jwk, + const FunctionCallbackInfo& args, + unsigned int offset) { + CHECK(args[offset]->IsString()); // curve name + Utf8Value curve(env->isolate(), args[offset].As()); + + int nid = GetCurveFromName(*curve); + if (nid == NID_undef) { // Unknown curve + THROW_ERR_CRYPTO_INVALID_CURVE(env); + return std::shared_ptr(); + } + + Local x_value; + Local y_value; + Local d_value; + + if (!jwk->Get(env->context(), env->jwk_x_string()).ToLocal(&x_value) || + !jwk->Get(env->context(), env->jwk_y_string()).ToLocal(&y_value) || + !jwk->Get(env->context(), env->jwk_d_string()).ToLocal(&d_value)) { + return std::shared_ptr(); + } + + if (!x_value->IsString() || + !y_value->IsString() || + (!d_value->IsUndefined() && !d_value->IsString())) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key"); + return std::shared_ptr(); + } + + KeyType type = d_value->IsString() ? kKeyTypePrivate : kKeyTypePublic; + + ECKeyPointer ec(EC_KEY_new_by_curve_name(nid)); + if (!ec) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key"); + return std::shared_ptr(); + } + + ByteSource x = ByteSource::FromEncodedString(env, x_value.As()); + ByteSource y = ByteSource::FromEncodedString(env, y_value.As()); + + if (!EC_KEY_set_public_key_affine_coordinates( + ec.get(), + x.ToBN().get(), + y.ToBN().get())) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key"); + return std::shared_ptr(); + } + + if (type == kKeyTypePrivate) { + ByteSource d = ByteSource::FromEncodedString(env, d_value.As()); + if (!EC_KEY_set_private_key(ec.get(), d.ToBN().get())) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key"); + return std::shared_ptr(); + } + } + + EVPKeyPointer pkey(EVP_PKEY_new()); + CHECK_EQ(EVP_PKEY_set1_EC_KEY(pkey.get(), ec.get()), 1); + + return KeyObjectData::CreateAsymmetric(type, ManagedEVPPKey(std::move(pkey))); +} + +Maybe GetEcKeyDetail( + Environment* env, + std::shared_ptr key, + Local target) { + ManagedEVPPKey pkey = key->GetAsymmetricKey(); + CHECK_EQ(EVP_PKEY_id(pkey.get()), EVP_PKEY_EC); + + EC_KEY* ec = EVP_PKEY_get0_EC_KEY(pkey.get()); + CHECK_NOT_NULL(ec); + + const EC_GROUP* group = EC_KEY_get0_group(ec); + int nid = EC_GROUP_get_curve_name(group); + + return target->Set( + env->context(), + env->named_curve_string(), + OneByteString(env->isolate(), OBJ_nid2sn(nid))); +} + +// WebCrypto requires a different format for ECDSA signatures than +// what OpenSSL produces, so we need to convert between them. The +// implementation here is a adapted from Chromium's impl here: +// https://github.com/chromium/chromium/blob/7af6cfd/components/webcrypto/algorithms/ecdsa.cc + +size_t GroupOrderSize(ManagedEVPPKey key) { + EC_KEY* ec = EVP_PKEY_get0_EC_KEY(key.get()); + CHECK_NOT_NULL(ec); + const EC_GROUP* group = EC_KEY_get0_group(ec); + BignumPointer order(BN_new()); + CHECK(EC_GROUP_get_order(group, order.get(), nullptr)); + return BN_num_bytes(order.get()); +} + +ByteSource ConvertToWebCryptoSignature( + ManagedEVPPKey key, + const ByteSource& signature) { + const unsigned char* data = + reinterpret_cast(signature.get()); + EcdsaSigPointer ecsig(d2i_ECDSA_SIG(nullptr, &data, signature.size())); + + if (!ecsig) + return ByteSource(); + + size_t order_size_bytes = GroupOrderSize(key); + char* outdata = MallocOpenSSL(order_size_bytes * 2); + ByteSource out = ByteSource::Allocated(outdata, order_size_bytes * 2); + unsigned char* ptr = reinterpret_cast(outdata); + + const BIGNUM* pr; + const BIGNUM* ps; + ECDSA_SIG_get0(ecsig.get(), &pr, &ps); + + if (!BN_bn2binpad(pr, ptr, order_size_bytes) || + !BN_bn2binpad(ps, ptr + order_size_bytes, order_size_bytes)) { + return ByteSource(); + } + return out; +} + +ByteSource ConvertFromWebCryptoSignature( + ManagedEVPPKey key, + const ByteSource& signature) { + size_t order_size_bytes = GroupOrderSize(key); + + // If the size of the signature is incorrect, verification + // will fail. + if (signature.size() != 2 * order_size_bytes) + return ByteSource(); // Empty! + + EcdsaSigPointer ecsig(ECDSA_SIG_new()); + if (!ecsig) + return ByteSource(); + + BignumPointer r(BN_new()); + BignumPointer s(BN_new()); + + const unsigned char* sig = signature.data(); + + if (!BN_bin2bn(sig, order_size_bytes, r.get()) || + !BN_bin2bn(sig + order_size_bytes, order_size_bytes, s.get()) || + !ECDSA_SIG_set0(ecsig.get(), r.release(), s.release())) { + return ByteSource(); + } + + int size = i2d_ECDSA_SIG(ecsig.get(), nullptr); + char* data = MallocOpenSSL(size); + unsigned char* ptr = reinterpret_cast(data); + CHECK_EQ(i2d_ECDSA_SIG(ecsig.get(), &ptr), size); + return ByteSource::Allocated(data, size); +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_ecdh.h b/src/crypto/crypto_ecdh.h new file mode 100644 index 00000000000000..c83818d16be0fb --- /dev/null +++ b/src/crypto/crypto_ecdh.h @@ -0,0 +1,170 @@ +#ifndef SRC_CRYPTO_CRYPTO_ECDH_H_ +#define SRC_CRYPTO_CRYPTO_ECDH_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "crypto/crypto_keygen.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer.h" +#include "async_wrap.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" +#include "node_internals.h" +#include "v8.h" + +namespace node { +namespace crypto { +class ECDH final : public BaseObject { + public: + ~ECDH() override; + + static void Initialize(Environment* env, v8::Local target); + static ECPointPointer BufferToPoint(Environment* env, + const EC_GROUP* group, + v8::Local buf); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(ECDH) + SET_SELF_SIZE(ECDH) + + static void ConvertKey(const v8::FunctionCallbackInfo& args); + + static void GetCurves(const v8::FunctionCallbackInfo& args); + + protected: + ECDH(Environment* env, v8::Local wrap, ECKeyPointer&& key); + + static void New(const v8::FunctionCallbackInfo& args); + static void GenerateKeys(const v8::FunctionCallbackInfo& args); + static void ComputeSecret(const v8::FunctionCallbackInfo& args); + static void GetPrivateKey(const v8::FunctionCallbackInfo& args); + static void SetPrivateKey(const v8::FunctionCallbackInfo& args); + static void GetPublicKey(const v8::FunctionCallbackInfo& args); + static void SetPublicKey(const v8::FunctionCallbackInfo& args); + + bool IsKeyPairValid(); + bool IsKeyValidForCurve(const BignumPointer& private_key); + + ECKeyPointer key_; + const EC_GROUP* group_; +}; + +struct ECDHBitsConfig final : public MemoryRetainer { + ECKeyPointer private_key; + ECKeyPointer public_key; + const EC_GROUP* group = nullptr; + + SET_NO_MEMORY_INFO(); + SET_MEMORY_INFO_NAME(ECDHBitsConfig); + SET_SELF_SIZE(ECDHBitsConfig); +}; + +struct ECDHBitsTraits final { + using AdditionalParameters = ECDHBitsConfig; + static constexpr const char* JobName = "ECDHBitsJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_DERIVEBITSREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + ECDHBitsConfig* params); + + static bool DeriveBits( + Environment* env, + const ECDHBitsConfig& params, + ByteSource* out_); + + static v8::Maybe EncodeOutput( + Environment* env, + const ECDHBitsConfig& params, + ByteSource* out, + v8::Local* result); +}; + +using ECDHBitsJob = DeriveBitsJob; + +struct EcKeyPairParams final : public MemoryRetainer { + int curve_nid; + int param_encoding; + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(EcKeyPairParams) + SET_SELF_SIZE(EcKeyPairParams) +}; + +using EcKeyPairGenConfig = KeyPairGenConfig; + +struct EcKeyGenTraits final { + using AdditionalParameters = EcKeyPairGenConfig; + static constexpr const char* JobName = "EcKeyPairGenJob"; + + static EVPKeyCtxPointer Setup(EcKeyPairGenConfig* params); + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + EcKeyPairGenConfig* params); +}; + +using ECKeyPairGenJob = KeyGenJob>; + +// There is currently no additional information that the +// ECKeyExport needs to collect, but we need to provide +// the base struct anyway. +struct ECKeyExportConfig final : public MemoryRetainer { + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(ECKeyExportConfig) + SET_SELF_SIZE(ECKeyExportConfig) +}; + +struct ECKeyExportTraits final { + static constexpr const char* JobName = "ECKeyExportJob"; + using AdditionalParameters = ECKeyExportConfig; + + static v8::Maybe AdditionalConfig( + const v8::FunctionCallbackInfo& args, + unsigned int offset, + ECKeyExportConfig* config); + + static WebCryptoKeyExportStatus DoExport( + std::shared_ptr key_data, + WebCryptoKeyFormat format, + const ECKeyExportConfig& params, + ByteSource* out); +}; + +using ECKeyExportJob = KeyExportJob; + +v8::Maybe ExportJWKEcKey( + Environment* env, + std::shared_ptr key, + v8::Local target); + +std::shared_ptr ImportJWKEcKey( + Environment* env, + v8::Local jwk, + const v8::FunctionCallbackInfo& args, + unsigned int offset); + +v8::Maybe GetEcKeyDetail( + Environment* env, + std::shared_ptr key, + v8::Local target); + +ByteSource ConvertToWebCryptoSignature( + ManagedEVPPKey key, + const ByteSource& signature); + +ByteSource ConvertFromWebCryptoSignature( + ManagedEVPPKey key, + const ByteSource& signature); + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_ECDH_H_ diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc new file mode 100644 index 00000000000000..e3cda069c594ab --- /dev/null +++ b/src/crypto/crypto_hash.cc @@ -0,0 +1,314 @@ +#include "crypto/crypto_hash.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "string_bytes.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +#include + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::Uint32; +using v8::Value; + +namespace crypto { +Hash::Hash(Environment* env, Local wrap) + : BaseObject(env, wrap), + mdctx_(nullptr), + has_md_(false), + md_value_(nullptr) { + MakeWeak(); +} + +void Hash::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("mdctx", mdctx_ ? kSizeOf_EVP_MD_CTX : 0); + tracker->TrackFieldWithSize("md", md_len_); +} + +void Hash::GetHashes(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CipherPushContext ctx(env); + EVP_MD_do_all_sorted(array_push_back, &ctx); + args.GetReturnValue().Set(ctx.ToJSArray()); +} + +void Hash::Initialize(Environment* env, Local target) { + Local t = env->NewFunctionTemplate(New); + + t->InstanceTemplate()->SetInternalFieldCount( + Hash::kInternalFieldCount); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + + env->SetProtoMethod(t, "update", HashUpdate); + env->SetProtoMethod(t, "digest", HashDigest); + + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "Hash"), + t->GetFunction(env->context()).ToLocalChecked()).Check(); + + env->SetMethodNoSideEffect(target, "getHashes", GetHashes); + + HashJob::Initialize(env, target); +} + +Hash::~Hash() { + if (md_value_ != nullptr) + OPENSSL_clear_free(md_value_, md_len_); +} + +void Hash::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + const Hash* orig = nullptr; + const EVP_MD* md = nullptr; + + if (args[0]->IsObject()) { + ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As()); + md = EVP_MD_CTX_md(orig->mdctx_.get()); + } else { + const Utf8Value hash_type(env->isolate(), args[0]); + md = EVP_get_digestbyname(*hash_type); + } + + Maybe xof_md_len = Nothing(); + if (!args[1]->IsUndefined()) { + CHECK(args[1]->IsUint32()); + xof_md_len = Just(args[1].As()->Value()); + } + + Hash* hash = new Hash(env, args.This()); + if (md == nullptr || !hash->HashInit(md, xof_md_len)) { + return ThrowCryptoError(env, ERR_get_error(), + "Digest method not supported"); + } + + if (orig != nullptr && + 0 >= EVP_MD_CTX_copy(hash->mdctx_.get(), orig->mdctx_.get())) { + return ThrowCryptoError(env, ERR_get_error(), "Digest copy error"); + } +} + +bool Hash::HashInit(const EVP_MD* md, Maybe xof_md_len) { + mdctx_.reset(EVP_MD_CTX_new()); + if (!mdctx_ || EVP_DigestInit_ex(mdctx_.get(), md, nullptr) <= 0) { + mdctx_.reset(); + return false; + } + + md_len_ = EVP_MD_size(md); + if (xof_md_len.IsJust() && xof_md_len.FromJust() != md_len_) { + // This is a little hack to cause createHash to fail when an incorrect + // hashSize option was passed for a non-XOF hash function. + if ((EVP_MD_flags(md) & EVP_MD_FLAG_XOF) == 0) { + EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH); + return false; + } + md_len_ = xof_md_len.FromJust(); + } + + return true; +} + +bool Hash::HashUpdate(const char* data, size_t len) { + if (!mdctx_) + return false; + EVP_DigestUpdate(mdctx_.get(), data, len); + return true; +} + +void Hash::HashUpdate(const FunctionCallbackInfo& args) { + Decode(args, [](Hash* hash, const FunctionCallbackInfo& args, + const char* data, size_t size) { + Environment* env = Environment::GetCurrent(args); + if (UNLIKELY(size > INT_MAX)) + return THROW_ERR_OUT_OF_RANGE(env, "data is too long"); + bool r = hash->HashUpdate(data, size); + args.GetReturnValue().Set(r); + }); +} + +void Hash::HashDigest(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Hash* hash; + ASSIGN_OR_RETURN_UNWRAP(&hash, args.Holder()); + + enum encoding encoding = BUFFER; + if (args.Length() >= 1) { + encoding = ParseEncoding(env->isolate(), args[0], BUFFER); + } + + // TODO(tniessen): SHA3_squeeze does not work for zero-length outputs on all + // platforms and will cause a segmentation fault if called. This workaround + // causes hash.digest() to correctly return an empty buffer / string. + // See https://github.com/openssl/openssl/issues/9431. + if (!hash->has_md_ && hash->md_len_ == 0) { + hash->has_md_ = true; + } + + if (!hash->has_md_) { + // Some hash algorithms such as SHA3 do not support calling + // EVP_DigestFinal_ex more than once, however, Hash._flush + // and Hash.digest can both be used to retrieve the digest, + // so we need to cache it. + // See https://github.com/nodejs/node/issues/28245. + + hash->md_value_ = MallocOpenSSL(hash->md_len_); + + size_t default_len = EVP_MD_CTX_size(hash->mdctx_.get()); + int ret; + if (hash->md_len_ == default_len) { + ret = EVP_DigestFinal_ex(hash->mdctx_.get(), hash->md_value_, + &hash->md_len_); + } else { + ret = EVP_DigestFinalXOF(hash->mdctx_.get(), hash->md_value_, + hash->md_len_); + } + + if (ret != 1) { + OPENSSL_free(hash->md_value_); + hash->md_value_ = nullptr; + return ThrowCryptoError(env, ERR_get_error()); + } + + hash->has_md_ = true; + } + + Local error; + MaybeLocal rc = + StringBytes::Encode(env->isolate(), + reinterpret_cast(hash->md_value_), + hash->md_len_, + encoding, + &error); + if (rc.IsEmpty()) { + CHECK(!error.IsEmpty()); + env->isolate()->ThrowException(error); + return; + } + args.GetReturnValue().Set(rc.ToLocalChecked()); +} + +HashConfig::HashConfig(HashConfig&& other) noexcept + : mode(other.mode), + in(std::move(other.in)), + digest(other.digest), + length(other.length) {} + +HashConfig& HashConfig::operator=(HashConfig&& other) noexcept { + if (&other == this) return *this; + this->~HashConfig(); + return *new (this) HashConfig(std::move(other)); +} + +void HashConfig::MemoryInfo(MemoryTracker* tracker) const { + // If the Job is sync, then the HashConfig does not own the data. + if (mode == kCryptoJobAsync) + tracker->TrackFieldWithSize("in", in.size()); +} + +Maybe HashTraits::EncodeOutput( + Environment* env, + const HashConfig& params, + ByteSource* out, + v8::Local* result) { + *result = out->ToArrayBuffer(env); + return Just(!result->IsEmpty()); +} + +Maybe HashTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + HashConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->mode = mode; + + CHECK(args[offset]->IsString()); // Hash algorithm + Utf8Value digest(env->isolate(), args[offset]); + params->digest = EVP_get_digestbyname(*digest); + if (UNLIKELY(params->digest == nullptr)) { + char msg[1024]; + snprintf(msg, sizeof(msg), "Invalid digest: %s", *digest); + THROW_ERR_CRYPTO_INVALID_DIGEST(env); + return Nothing(); + } + + ArrayBufferOrViewContents data(args[offset + 1]); + if (UNLIKELY(!data.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + return Nothing(); + } + params->in = mode == kCryptoJobAsync + ? data.ToCopy() + : data.ToByteSource(); + + unsigned int expected = EVP_MD_size(params->digest); + params->length = expected; + if (UNLIKELY(args[offset + 2]->IsUint32())) { + // length is expressed in terms of bits + params->length = + static_cast(args[offset + 2] + .As()->Value()) / CHAR_BIT; + if (params->length != expected) { + if ((EVP_MD_flags(params->digest) & EVP_MD_FLAG_XOF) == 0) { + THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Digest method not supported"); + return Nothing(); + } + } + } + + return Just(true); +} + +bool HashTraits::DeriveBits( + Environment* env, + const HashConfig& params, + ByteSource* out) { + EVPMDPointer ctx(EVP_MD_CTX_new()); + + if (UNLIKELY(!ctx || + EVP_DigestInit_ex(ctx.get(), params.digest, nullptr) <= 0 || + EVP_DigestUpdate( + ctx.get(), + params.in.get(), + params.in.size()) <= 0)) { + return false; + } + + if (LIKELY(params.length > 0)) { + unsigned int length = params.length; + char* data = MallocOpenSSL(length); + ByteSource buf = ByteSource::Allocated(data, length); + unsigned char* ptr = reinterpret_cast(data); + + size_t expected = EVP_MD_CTX_size(ctx.get()); + + int ret = (length == expected) + ? EVP_DigestFinal_ex(ctx.get(), ptr, &length) + : EVP_DigestFinalXOF(ctx.get(), ptr, length); + + if (UNLIKELY(ret != 1)) + return false; + + *out = std::move(buf); + } + + return true; +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h new file mode 100644 index 00000000000000..15e83d0cd11b82 --- /dev/null +++ b/src/crypto/crypto_hash.h @@ -0,0 +1,92 @@ +#ifndef SRC_CRYPTO_CRYPTO_HASH_H_ +#define SRC_CRYPTO_CRYPTO_HASH_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +namespace node { +namespace crypto { +class Hash final : public BaseObject { + public: + ~Hash() override; + + static void Initialize(Environment* env, v8::Local target); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Hash) + SET_SELF_SIZE(Hash) + + bool HashInit(const EVP_MD* md, v8::Maybe xof_md_len); + bool HashUpdate(const char* data, size_t len); + + static void GetHashes(const v8::FunctionCallbackInfo& args); + + protected: + static void New(const v8::FunctionCallbackInfo& args); + static void HashUpdate(const v8::FunctionCallbackInfo& args); + static void HashDigest(const v8::FunctionCallbackInfo& args); + + Hash(Environment* env, v8::Local wrap); + + private: + EVPMDPointer mdctx_; + bool has_md_; + unsigned int md_len_; + unsigned char* md_value_; +}; + +struct HashConfig final : public MemoryRetainer { + CryptoJobMode mode; + ByteSource in; + const EVP_MD* digest; + unsigned int length; + + HashConfig() = default; + + explicit HashConfig(HashConfig&& other) noexcept; + + HashConfig& operator=(HashConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(HashConfig); + SET_SELF_SIZE(HashConfig); +}; + +struct HashTraits final { + using AdditionalParameters = HashConfig; + static constexpr const char* JobName = "HashJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_HASHREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + HashConfig* params); + + static bool DeriveBits( + Environment* env, + const HashConfig& params, + ByteSource* out); + + static v8::Maybe EncodeOutput( + Environment* env, + const HashConfig& params, + ByteSource* out, + v8::Local* result); +}; + +using HashJob = DeriveBitsJob; + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_HASH_H_ diff --git a/src/crypto/crypto_hkdf.cc b/src/crypto/crypto_hkdf.cc new file mode 100644 index 00000000000000..f6339b129baea6 --- /dev/null +++ b/src/crypto/crypto_hkdf.cc @@ -0,0 +1,147 @@ +#include "crypto/crypto_hkdf.h" +#include "crypto/crypto_keys.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Just; +using v8::Maybe; +using v8::Nothing; +using v8::Uint32; +using v8::Value; + +namespace crypto { +HKDFConfig::HKDFConfig(HKDFConfig&& other) noexcept + : mode(other.mode), + length(other.length), + digest(other.digest), + key(other.key), + salt(std::move(other.salt)), + info(std::move(other.info)) {} + +HKDFConfig& HKDFConfig::operator=(HKDFConfig&& other) noexcept { + if (&other == this) return *this; + this->~HKDFConfig(); + return *new (this) HKDFConfig(std::move(other)); +} + +Maybe HKDFTraits::EncodeOutput( + Environment* env, + const HKDFConfig& params, + ByteSource* out, + v8::Local* result) { + *result = out->ToArrayBuffer(env); + return Just(!result->IsEmpty()); +} + +Maybe HKDFTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + HKDFConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->mode = mode; + + CHECK(args[offset]->IsString()); // Hash + CHECK(args[offset + 1]->IsObject()); // Key + CHECK(IsAnyByteSource(args[offset + 2])); // Salt + CHECK(IsAnyByteSource(args[offset + 3])); // Info + CHECK(args[offset + 4]->IsUint32()); // Length + + Utf8Value hash(env->isolate(), args[offset]); + params->digest = EVP_get_digestbyname(*hash); + if (params->digest == nullptr) { + THROW_ERR_CRYPTO_INVALID_DIGEST(env); + return Nothing(); + } + + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset + 1], Nothing()); + params->key = key->Data(); + + ArrayBufferOrViewContents salt(args[offset + 2]); + ArrayBufferOrViewContents info(args[offset + 3]); + + if (UNLIKELY(!salt.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "salt is too big"); + return Nothing(); + } + if (UNLIKELY(!info.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "info is too big"); + return Nothing(); + } + + params->salt = mode == kCryptoJobAsync + ? salt.ToCopy() + : salt.ToByteSource(); + + params->info = mode == kCryptoJobAsync + ? info.ToCopy() + : info.ToByteSource(); + + params->length = args[offset + 4].As()->Value(); + size_t max_length = EVP_MD_size(params->digest) * kMaxDigestMultiplier; + if (params->length > max_length) { + THROW_ERR_CRYPTO_INVALID_KEYLEN(env); + return Nothing(); + } + + return Just(true); +} + +bool HKDFTraits::DeriveBits( + Environment* env, + const HKDFConfig& params, + ByteSource* out) { + EVPKeyCtxPointer ctx = + EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr)); + if (!ctx || + !EVP_PKEY_derive_init(ctx.get()) || + !EVP_PKEY_CTX_hkdf_mode( + ctx.get(), EVP_PKEY_HKDEF_MODE_EXTRACT_AND_EXPAND) || + !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), params.digest) || + !EVP_PKEY_CTX_set1_hkdf_salt( + ctx.get(), + params.salt.get(), + params.salt.size()) || + !EVP_PKEY_CTX_set1_hkdf_key( + ctx.get(), + params.key->GetSymmetricKey(), + params.key->GetSymmetricKeySize()) || + !EVP_PKEY_CTX_add1_hkdf_info( + ctx.get(), + params.info.get(), + params.info.size())) { + return false; + } + + size_t length = params.length; + char* data = MallocOpenSSL(length); + ByteSource buf = ByteSource::Allocated(data, length); + unsigned char* ptr = reinterpret_cast(data); + if (EVP_PKEY_derive(ctx.get(), ptr, &length) <= 0) + return false; + + *out = std::move(buf); + return true; +} + +void HKDFConfig::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("key", key); + // If the job is sync, then the HKDFConfig does not own the data + if (mode == kCryptoJobAsync) { + tracker->TrackFieldWithSize("salt", salt.size()); + tracker->TrackFieldWithSize("info", info.size()); + } +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_hkdf.h b/src/crypto/crypto_hkdf.h new file mode 100644 index 00000000000000..06774bcde97378 --- /dev/null +++ b/src/crypto/crypto_hkdf.h @@ -0,0 +1,66 @@ +#ifndef SRC_CRYPTO_CRYPTO_HKDF_H_ +#define SRC_CRYPTO_CRYPTO_HKDF_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer.h" +#include "async_wrap.h" +#include "base_object.h" +#include "v8.h" + +namespace node { +namespace crypto { +static constexpr size_t kMaxDigestMultiplier = 255; + +struct HKDFConfig final : public MemoryRetainer { + CryptoJobMode mode; + size_t length; + const EVP_MD* digest; + std::shared_ptr key; + ByteSource salt; + ByteSource info; + + HKDFConfig() = default; + + explicit HKDFConfig(HKDFConfig&& other) noexcept; + + HKDFConfig& operator=(HKDFConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(HKDFConfig); + SET_SELF_SIZE(HKDFConfig); +}; + +struct HKDFTraits final { + using AdditionalParameters = HKDFConfig; + static constexpr const char* JobName = "HKDFJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_DERIVEBITSREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + HKDFConfig* params); + + static bool DeriveBits( + Environment* env, + const HKDFConfig& params, + ByteSource* out); + + static v8::Maybe EncodeOutput( + Environment* env, + const HKDFConfig& params, + ByteSource* out, + v8::Local* result); +}; + +using HKDFJob = DeriveBitsJob; + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_HKDF_H_ diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc new file mode 100644 index 00000000000000..7f4682e900cc12 --- /dev/null +++ b/src/crypto/crypto_hmac.cc @@ -0,0 +1,275 @@ +#include "crypto/crypto_hmac.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_sig.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_buffer.h" +#include "string_bytes.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::Uint32; +using v8::Value; + +namespace crypto { +Hmac::Hmac(Environment* env, Local wrap) + : BaseObject(env, wrap), + ctx_(nullptr) { + MakeWeak(); +} + +void Hmac::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("context", ctx_ ? kSizeOf_HMAC_CTX : 0); +} + +void Hmac::Initialize(Environment* env, Local target) { + Local t = env->NewFunctionTemplate(New); + + t->InstanceTemplate()->SetInternalFieldCount( + Hmac::kInternalFieldCount); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + + env->SetProtoMethod(t, "init", HmacInit); + env->SetProtoMethod(t, "update", HmacUpdate); + env->SetProtoMethod(t, "digest", HmacDigest); + + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "Hmac"), + t->GetFunction(env->context()).ToLocalChecked()).Check(); + + HmacJob::Initialize(env, target); +} + +void Hmac::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + new Hmac(env, args.This()); +} + +void Hmac::HmacInit(const char* hash_type, const char* key, int key_len) { + HandleScope scope(env()->isolate()); + + const EVP_MD* md = EVP_get_digestbyname(hash_type); + if (md == nullptr) + return THROW_ERR_CRYPTO_INVALID_DIGEST(env()); + if (key_len == 0) { + key = ""; + } + ctx_.reset(HMAC_CTX_new()); + if (!ctx_ || !HMAC_Init_ex(ctx_.get(), key, key_len, md, nullptr)) { + ctx_.reset(); + return ThrowCryptoError(env(), ERR_get_error()); + } +} + +void Hmac::HmacInit(const FunctionCallbackInfo& args) { + Hmac* hmac; + ASSIGN_OR_RETURN_UNWRAP(&hmac, args.Holder()); + Environment* env = hmac->env(); + + const node::Utf8Value hash_type(env->isolate(), args[0]); + ByteSource key = ByteSource::FromSecretKeyBytes(env, args[1]); + hmac->HmacInit(*hash_type, key.get(), key.size()); +} + +bool Hmac::HmacUpdate(const char* data, size_t len) { + return ctx_ && HMAC_Update(ctx_.get(), + reinterpret_cast(data), + len) == 1; +} + +void Hmac::HmacUpdate(const FunctionCallbackInfo& args) { + Decode(args, [](Hmac* hmac, const FunctionCallbackInfo& args, + const char* data, size_t size) { + Environment* env = Environment::GetCurrent(args); + if (UNLIKELY(size > INT_MAX)) + return THROW_ERR_OUT_OF_RANGE(env, "data is too long"); + bool r = hmac->HmacUpdate(data, size); + args.GetReturnValue().Set(r); + }); +} + +void Hmac::HmacDigest(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Hmac* hmac; + ASSIGN_OR_RETURN_UNWRAP(&hmac, args.Holder()); + + enum encoding encoding = BUFFER; + if (args.Length() >= 1) { + encoding = ParseEncoding(env->isolate(), args[0], BUFFER); + } + + unsigned char md_value[EVP_MAX_MD_SIZE]; + unsigned int md_len = 0; + + if (hmac->ctx_) { + HMAC_Final(hmac->ctx_.get(), md_value, &md_len); + hmac->ctx_.reset(); + } + + Local error; + MaybeLocal rc = + StringBytes::Encode(env->isolate(), + reinterpret_cast(md_value), + md_len, + encoding, + &error); + if (rc.IsEmpty()) { + CHECK(!error.IsEmpty()); + env->isolate()->ThrowException(error); + return; + } + args.GetReturnValue().Set(rc.ToLocalChecked()); +} + +HmacConfig::HmacConfig(HmacConfig&& other) noexcept + : job_mode(other.job_mode), + mode(other.mode), + key(std::move(other.key)), + data(std::move(other.data)), + signature(std::move(other.signature)), + digest(other.digest) {} + +HmacConfig& HmacConfig::operator=(HmacConfig&& other) noexcept { + if (&other == this) return *this; + this->~HmacConfig(); + return *new (this) HmacConfig(std::move(other)); +} + +void HmacConfig::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("key", key.get()); + // If the job is sync, then the HmacConfig does not own the data + if (job_mode == kCryptoJobAsync) { + tracker->TrackFieldWithSize("data", data.size()); + tracker->TrackFieldWithSize("signature", signature.size()); + } +} + +Maybe HmacTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + HmacConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->job_mode = mode; + + CHECK(args[offset]->IsUint32()); // SignConfiguration::Mode + params->mode = + static_cast(args[offset].As()->Value()); + + CHECK(args[offset + 1]->IsString()); // Hash + CHECK(args[offset + 2]->IsObject()); // Key + + Utf8Value digest(env->isolate(), args[offset + 1]); + params->digest = EVP_get_digestbyname(*digest); + if (params->digest == nullptr) { + THROW_ERR_CRYPTO_INVALID_DIGEST(env); + return Nothing(); + } + + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset + 2], Nothing()); + params->key = key->Data(); + + ArrayBufferOrViewContents data(args[offset + 3]); + if (UNLIKELY(!data.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + return Nothing(); + } + params->data = mode == kCryptoJobAsync + ? data.ToCopy() + : data.ToByteSource(); + + if (!args[offset + 4]->IsUndefined()) { + ArrayBufferOrViewContents signature(args[offset + 4]); + if (UNLIKELY(!signature.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); + return Nothing(); + } + params->signature = mode == kCryptoJobAsync + ? signature.ToCopy() + : signature.ToByteSource(); + } + + return Just(true); +} + +bool HmacTraits::DeriveBits( + Environment* env, + const HmacConfig& params, + ByteSource* out) { + HMACCtxPointer ctx(HMAC_CTX_new()); + + if (!ctx || + !HMAC_Init_ex( + ctx.get(), + params.key->GetSymmetricKey(), + params.key->GetSymmetricKeySize(), + params.digest, + nullptr)) { + return false; + } + + if (!HMAC_Update( + ctx.get(), + params.data.data(), + params.data.size())) { + return false; + } + + char* data = MallocOpenSSL(EVP_MAX_MD_SIZE); + ByteSource buf = ByteSource::Allocated(data, EVP_MAX_MD_SIZE); + unsigned char* ptr = reinterpret_cast(data); + unsigned int len; + + if (!HMAC_Final(ctx.get(), ptr, &len)) { + return false; + } + + buf.Resize(len); + *out = std::move(buf); + + return true; +} + +Maybe HmacTraits::EncodeOutput( + Environment* env, + const HmacConfig& params, + ByteSource* out, + Local* result) { + switch (params.mode) { + case SignConfiguration::kSign: + *result = out->ToArrayBuffer(env); + break; + case SignConfiguration::kVerify: + *result = + out->size() > 0 && + out->size() == params.signature.size() && + memcmp(out->get(), params.signature.get(), out->size()) == 0 + ? v8::True(env->isolate()) + : v8::False(env->isolate()); + break; + default: + UNREACHABLE(); + } + return Just(!result->IsEmpty()); +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_hmac.h b/src/crypto/crypto_hmac.h new file mode 100644 index 00000000000000..fd7dba2ed8a098 --- /dev/null +++ b/src/crypto/crypto_hmac.h @@ -0,0 +1,94 @@ +#ifndef SRC_CRYPTO_CRYPTO_HMAC_H_ +#define SRC_CRYPTO_CRYPTO_HMAC_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "crypto/crypto_sig.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +namespace node { +namespace crypto { +class Hmac : public BaseObject { + public: + static void Initialize(Environment* env, v8::Local target); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Hmac) + SET_SELF_SIZE(Hmac) + + protected: + void HmacInit(const char* hash_type, const char* key, int key_len); + bool HmacUpdate(const char* data, size_t len); + + static void New(const v8::FunctionCallbackInfo& args); + static void HmacInit(const v8::FunctionCallbackInfo& args); + static void HmacUpdate(const v8::FunctionCallbackInfo& args); + static void HmacDigest(const v8::FunctionCallbackInfo& args); + + Hmac(Environment* env, v8::Local wrap); + + static void Sign(const v8::FunctionCallbackInfo& args); + + private: + HMACCtxPointer ctx_; +}; + +struct HmacConfig final : public MemoryRetainer { + CryptoJobMode job_mode; + SignConfiguration::Mode mode; + std::shared_ptr key; + ByteSource data; + ByteSource signature; + const EVP_MD* digest; + + HmacConfig() = default; + + explicit HmacConfig(HmacConfig&& other) noexcept; + + HmacConfig& operator=(HmacConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(HmacConfig); + SET_SELF_SIZE(HmacConfig); +}; + +struct HmacTraits final { + using AdditionalParameters = HmacConfig; + static constexpr const char* JobName = "HmacJob"; + +// TODO(@jasnell): Sign request vs. Verify request + + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_SIGNREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + HmacConfig* params); + + static bool DeriveBits( + Environment* env, + const HmacConfig& params, + ByteSource* out); + + static v8::Maybe EncodeOutput( + Environment* env, + const HmacConfig& params, + ByteSource* out, + v8::Local* result); +}; + +using HmacJob = DeriveBitsJob; + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_HMAC_H_ diff --git a/src/crypto/crypto_keygen.cc b/src/crypto/crypto_keygen.cc new file mode 100644 index 00000000000000..c6ce46584f4104 --- /dev/null +++ b/src/crypto/crypto_keygen.cc @@ -0,0 +1,107 @@ +#include "crypto/crypto_keygen.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +#include + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Int32; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Object; +using v8::Uint32; +using v8::Value; + +namespace crypto { +// NidKeyPairGenJob input arguments: +// 1. CryptoJobMode +// 2. NID +// 3. Public Format +// 4. Public Type +// 5. Private Format +// 6. Private Type +// 7. Cipher +// 8. Passphrase +Maybe NidKeyPairGenTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int* offset, + NidKeyPairGenConfig* params) { + CHECK(args[*offset]->IsInt32()); + params->params.id = args[*offset].As()->Value(); + + *offset += 1; + + return Just(true); +} + +EVPKeyCtxPointer NidKeyPairGenTraits::Setup(NidKeyPairGenConfig* params) { + EVPKeyCtxPointer ctx = + EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(params->params.id, nullptr)); + if (!ctx || EVP_PKEY_keygen_init(ctx.get()) <= 0) + return EVPKeyCtxPointer(); + + return ctx; +} + +void SecretKeyGenConfig::MemoryInfo(MemoryTracker* tracker) const { + if (out != nullptr) + tracker->TrackFieldWithSize("out", length); +} + +Maybe SecretKeyGenTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int* offset, + SecretKeyGenConfig* params) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[*offset]->IsUint32()); + params->length = std::trunc(args[*offset].As()->Value() / CHAR_BIT); + if (params->length > INT_MAX) { + char msg[1024]; + snprintf(msg, sizeof(msg), + "length must be less than or equal to %lu bits", + static_cast(INT_MAX) * CHAR_BIT); + THROW_ERR_OUT_OF_RANGE(env, msg); + return Nothing(); + } + *offset += 1; + return Just(true); +} + +KeyGenJobStatus SecretKeyGenTraits::DoKeyGen( + Environment* env, + SecretKeyGenConfig* params) { + CHECK_LE(params->length, INT_MAX); + params->out = MallocOpenSSL(params->length); + EntropySource(reinterpret_cast(params->out), params->length); + return KeyGenJobStatus::ERR_OK; +} + +Maybe SecretKeyGenTraits::EncodeKey( + Environment* env, + SecretKeyGenConfig* params, + Local* result) { + ByteSource out = ByteSource::Allocated(params->out, params->length); + std::shared_ptr data = + KeyObjectData::CreateSecret(std::move(out)); + return Just(KeyObjectHandle::Create(env, data).ToLocal(result)); +} + +namespace Keygen { +void Initialize(Environment* env, Local target) { + NidKeyPairGenJob::Initialize(env, target); + SecretKeyGenJob::Initialize(env, target); +} +} // namespace Keygen +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_keygen.h b/src/crypto/crypto_keygen.h new file mode 100644 index 00000000000000..fde680baec1f88 --- /dev/null +++ b/src/crypto/crypto_keygen.h @@ -0,0 +1,292 @@ +#ifndef SRC_CRYPTO_CRYPTO_KEYGEN_H_ +#define SRC_CRYPTO_CRYPTO_KEYGEN_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer.h" +#include "async_wrap.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +namespace node { +namespace crypto { +namespace Keygen { +void Initialize(Environment* env, v8::Local target); +} // namespace Keygen + +enum class KeyGenJobStatus { + ERR_OK, + ERR_FAILED +}; + +// A Base CryptoJob for generating secret keys or key pairs. +// The KeyGenTraits is largely responsible for the details of +// the implementation, while KeyGenJob handles the common +// mechanisms. +template +class KeyGenJob final : public CryptoJob { + public: + using AdditionalParams = typename KeyGenTraits::AdditionalParameters; + + static void New(const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + + CryptoJobMode mode = GetCryptoJobMode(args[0]); + + unsigned int offset = 1; + + AdditionalParams params; + if (KeyGenTraits::AdditionalConfig(mode, args, &offset, ¶ms) + .IsNothing()) { + // The KeyGenTraits::AdditionalConfig is responsible for + // calling an appropriate THROW_CRYPTO_* variant reporting + // whatever error caused initialization to fail. + return; + } + + new KeyGenJob(env, args.This(), mode, std::move(params)); + } + + static void Initialize( + Environment* env, + v8::Local target) { + CryptoJob::Initialize(New, env, target); + } + + KeyGenJob( + Environment* env, + v8::Local object, + CryptoJobMode mode, + AdditionalParams&& params) + : CryptoJob( + env, + object, + KeyGenTraits::Provider, + mode, + std::move(params)) {} + + void DoThreadPoolWork() override { + // Make sure the the CSPRNG is properly seeded so the results are secure + CheckEntropy(); + + AdditionalParams* params = CryptoJob::params(); + + switch (KeyGenTraits::DoKeyGen(AsyncWrap::env(), params)) { + case KeyGenJobStatus::ERR_OK: + status_ = KeyGenJobStatus::ERR_OK; + // Success! + break; + case KeyGenJobStatus::ERR_FAILED: { + CryptoErrorVector* errors = CryptoJob::errors(); + errors->Capture(); + if (errors->empty()) + errors->push_back(std::string("Key generation job failed")); + } + } + } + + v8::Maybe ToResult( + v8::Local* err, + v8::Local* result) override { + Environment* env = AsyncWrap::env(); + CryptoErrorVector* errors = CryptoJob::errors(); + AdditionalParams* params = CryptoJob::params(); + if (status_ == KeyGenJobStatus::ERR_OK && + LIKELY(!KeyGenTraits::EncodeKey(env, params, result).IsNothing())) { + *err = Undefined(env->isolate()); + return v8::Just(true); + } + + if (errors->empty()) + errors->Capture(); + CHECK(!errors->empty()); + *result = Undefined(env->isolate()); + return v8::Just(errors->ToException(env).ToLocal(err)); + } + + SET_SELF_SIZE(KeyGenJob); + + private: + KeyGenJobStatus status_ = KeyGenJobStatus::ERR_FAILED; +}; + +// A Base KeyGenTraits for Key Pair generation algorithms. +template +struct KeyPairGenTraits final { + using AdditionalParameters = + typename KeyPairAlgorithmTraits::AdditionalParameters; + + static const AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_KEYPAIRGENREQUEST; + static constexpr const char* JobName = KeyPairAlgorithmTraits::JobName; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + AdditionalParameters* params) { + // Notice that offset is a pointer. Each of the AdditionalConfig, + // GetPublicKeyEncodingFromJs, and GetPrivateKeyEncodingFromJs + // functions will update the value of the offset as they successfully + // process input parameters. This allows each job to have a variable + // number of input parameters specific to each job type. + if (KeyPairAlgorithmTraits::AdditionalConfig(mode, args, offset, params) + .IsNothing()) { + return v8::Just(false); + } + + params->public_key_encoding = ManagedEVPPKey::GetPublicKeyEncodingFromJs( + args, + offset, + kKeyContextGenerate); + + auto private_key_encoding = + ManagedEVPPKey::GetPrivateKeyEncodingFromJs( + args, + offset, + kKeyContextGenerate); + + if (!private_key_encoding.IsEmpty()) + params->private_key_encoding = private_key_encoding.Release(); + + return v8::Just(true); + } + + static KeyGenJobStatus DoKeyGen( + Environment* env, + AdditionalParameters* params) { + EVPKeyCtxPointer ctx = KeyPairAlgorithmTraits::Setup(params); + if (!ctx || EVP_PKEY_keygen_init(ctx.get()) <= 0) + return KeyGenJobStatus::ERR_FAILED; + + // Generate the key + EVP_PKEY* pkey = nullptr; + if (!EVP_PKEY_keygen(ctx.get(), &pkey)) + return KeyGenJobStatus::ERR_FAILED; + + params->key = ManagedEVPPKey(EVPKeyPointer(pkey)); + return KeyGenJobStatus::ERR_OK; + } + + static v8::Maybe EncodeKey( + Environment* env, + AdditionalParameters* params, + v8::Local* result) { + v8::Local keys[2]; + if (ManagedEVPPKey::ToEncodedPublicKey( + env, + std::move(params->key), + params->public_key_encoding, + &keys[0]).IsNothing() || + ManagedEVPPKey::ToEncodedPrivateKey( + env, + std::move(params->key), + params->private_key_encoding, + &keys[1]).IsNothing()) { + return v8::Nothing(); + } + *result = v8::Array::New(env->isolate(), keys, arraysize(keys)); + return v8::Just(true); + } +}; + +struct SecretKeyGenConfig final : public MemoryRetainer { + size_t length; // Expressed a a number of bits + char* out = nullptr; // Placeholder for the generated key bytes + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(SecretKeyGenConfig) + SET_SELF_SIZE(SecretKeyGenConfig) +}; + +struct SecretKeyGenTraits final { + using AdditionalParameters = SecretKeyGenConfig; + static const AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_KEYGENREQUEST; + static constexpr const char* JobName = "SecretKeyGenJob"; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + SecretKeyGenConfig* params); + + static KeyGenJobStatus DoKeyGen( + Environment* env, + SecretKeyGenConfig* params); + + static v8::Maybe EncodeKey( + Environment* env, + SecretKeyGenConfig* params, + v8::Local* result); +}; + +template +struct KeyPairGenConfig final : public MemoryRetainer { + PublicKeyEncodingConfig public_key_encoding; + PrivateKeyEncodingConfig private_key_encoding; + ManagedEVPPKey key; + AlgorithmParams params; + + KeyPairGenConfig() = default; + + explicit KeyPairGenConfig(KeyPairGenConfig&& other) noexcept + : public_key_encoding(other.public_key_encoding), + private_key_encoding( + std::forward( + other.private_key_encoding)), + key(std::move(other.key)), + params(std::move(other.params)) {} + + KeyPairGenConfig& operator=(KeyPairGenConfig&& other) noexcept { + if (&other == this) return *this; + this->~KeyPairGenConfig(); + return *new (this) KeyPairGenConfig(std::move(other)); + } + + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackField("key", key); + tracker->TrackFieldWithSize("private_key_encoding.passphrase", + private_key_encoding.passphrase_.size()); + tracker->TrackField("params", params); + } + + SET_MEMORY_INFO_NAME(KeyPairGenConfig) + SET_SELF_SIZE(KeyPairGenConfig) +}; + +struct NidKeyPairParams final : public MemoryRetainer { + int id; + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(NidKeyPairParams) + SET_SELF_SIZE(NidKeyPairParams) +}; + +using NidKeyPairGenConfig = KeyPairGenConfig; + +struct NidKeyPairGenTraits final { + using AdditionalParameters = NidKeyPairGenConfig; + static constexpr const char* JobName = "NidKeyPairGenJob"; + + static EVPKeyCtxPointer Setup(NidKeyPairGenConfig* params); + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + NidKeyPairGenConfig* params); +}; + +using NidKeyPairGenJob = KeyGenJob>; +using SecretKeyGenJob = KeyGenJob; +} // namespace crypto +} // namespace node + +#endif // !defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_KEYGEN_H_ + diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc new file mode 100644 index 00000000000000..5504e91d92d8b0 --- /dev/null +++ b/src/crypto/crypto_keys.cc @@ -0,0 +1,1304 @@ +#include "crypto/crypto_keys.h" +#include "crypto/crypto_common.h" +#include "crypto/crypto_dsa.h" +#include "crypto/crypto_ecdh.h" +#include "crypto/crypto_dh.h" +#include "crypto/crypto_rsa.h" +#include "crypto/crypto_util.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node.h" +#include "node_buffer.h" +#include "string_bytes.h" +#include "threadpoolwork-inl.h" +#include "util-inl.h" +#include "v8.h" + +namespace node { + +using v8::Array; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Int32; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::NewStringType; +using v8::Nothing; +using v8::Number; +using v8::Object; +using v8::String; +using v8::Uint32; +using v8::Undefined; +using v8::Value; + +namespace crypto { +namespace { +void GetKeyFormatAndTypeFromJs( + AsymmetricKeyEncodingConfig* config, + const FunctionCallbackInfo& args, + unsigned int* offset, + KeyEncodingContext context) { + // During key pair generation, it is possible not to specify a key encoding, + // which will lead to a key object being returned. + if (args[*offset]->IsUndefined()) { + CHECK_EQ(context, kKeyContextGenerate); + CHECK(args[*offset + 1]->IsUndefined()); + config->output_key_object_ = true; + } else { + config->output_key_object_ = false; + + CHECK(args[*offset]->IsInt32()); + config->format_ = static_cast( + args[*offset].As()->Value()); + + if (args[*offset + 1]->IsInt32()) { + config->type_ = Just(static_cast( + args[*offset + 1].As()->Value())); + } else { + CHECK(context == kKeyContextInput && config->format_ == kKeyFormatPEM); + CHECK(args[*offset + 1]->IsNullOrUndefined()); + config->type_ = Nothing(); + } + } + + *offset += 2; +} + +ParseKeyResult TryParsePublicKey( + EVPKeyPointer* pkey, + const BIOPointer& bp, + const char* name, + // NOLINTNEXTLINE(runtime/int) + const std::function& parse) { + unsigned char* der_data; + long der_len; // NOLINT(runtime/int) + + // This skips surrounding data and decodes PEM to DER. + { + MarkPopErrorOnReturn mark_pop_error_on_return; + if (PEM_bytes_read_bio(&der_data, &der_len, nullptr, name, + bp.get(), nullptr, nullptr) != 1) + return ParseKeyResult::kParseKeyNotRecognized; + } + + // OpenSSL might modify the pointer, so we need to make a copy before parsing. + const unsigned char* p = der_data; + pkey->reset(parse(&p, der_len)); + OPENSSL_clear_free(der_data, der_len); + + return *pkey ? ParseKeyResult::kParseKeyOk : + ParseKeyResult::kParseKeyFailed; +} + +ParseKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey, + const char* key_pem, + int key_pem_len) { + BIOPointer bp(BIO_new_mem_buf(const_cast(key_pem), key_pem_len)); + if (!bp) + return ParseKeyResult::kParseKeyFailed; + + ParseKeyResult ret; + + // Try parsing as a SubjectPublicKeyInfo first. + ret = TryParsePublicKey(pkey, bp, "PUBLIC KEY", + [](const unsigned char** p, long l) { // NOLINT(runtime/int) + return d2i_PUBKEY(nullptr, p, l); + }); + if (ret != ParseKeyResult::kParseKeyNotRecognized) + return ret; + + // Maybe it is PKCS#1. + CHECK(BIO_reset(bp.get())); + ret = TryParsePublicKey(pkey, bp, "RSA PUBLIC KEY", + [](const unsigned char** p, long l) { // NOLINT(runtime/int) + return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l); + }); + if (ret != ParseKeyResult::kParseKeyNotRecognized) + return ret; + + // X.509 fallback. + CHECK(BIO_reset(bp.get())); + return TryParsePublicKey(pkey, bp, "CERTIFICATE", + [](const unsigned char** p, long l) { // NOLINT(runtime/int) + X509Pointer x509(d2i_X509(nullptr, p, l)); + return x509 ? X509_get_pubkey(x509.get()) : nullptr; + }); +} + +ParseKeyResult ParsePublicKey(EVPKeyPointer* pkey, + const PublicKeyEncodingConfig& config, + const char* key, + size_t key_len) { + if (config.format_ == kKeyFormatPEM) { + return ParsePublicKeyPEM(pkey, key, key_len); + } else { + CHECK_EQ(config.format_, kKeyFormatDER); + + const unsigned char* p = reinterpret_cast(key); + if (config.type_.ToChecked() == kKeyEncodingPKCS1) { + pkey->reset(d2i_PublicKey(EVP_PKEY_RSA, nullptr, &p, key_len)); + } else { + CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSPKI); + pkey->reset(d2i_PUBKEY(nullptr, &p, key_len)); + } + + return *pkey ? ParseKeyResult::kParseKeyOk : + ParseKeyResult::kParseKeyFailed; + } +} + +bool IsASN1Sequence(const unsigned char* data, size_t size, + size_t* data_offset, size_t* data_size) { + if (size < 2 || data[0] != 0x30) + return false; + + if (data[1] & 0x80) { + // Long form. + size_t n_bytes = data[1] & ~0x80; + if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) + return false; + size_t length = 0; + for (size_t i = 0; i < n_bytes; i++) + length = (length << 8) | data[i + 2]; + *data_offset = 2 + n_bytes; + *data_size = std::min(size - 2 - n_bytes, length); + } else { + // Short form. + *data_offset = 2; + *data_size = std::min(size - 2, data[1]); + } + + return true; +} + +bool IsRSAPrivateKey(const unsigned char* data, size_t size) { + // Both RSAPrivateKey and RSAPublicKey structures start with a SEQUENCE. + size_t offset, len; + if (!IsASN1Sequence(data, size, &offset, &len)) + return false; + + // An RSAPrivateKey sequence always starts with a single-byte integer whose + // value is either 0 or 1, whereas an RSAPublicKey starts with the modulus + // (which is the product of two primes and therefore at least 4), so we can + // decide the type of the structure based on the first three bytes of the + // sequence. + return len >= 3 && + data[offset] == 2 && + data[offset + 1] == 1 && + !(data[offset + 2] & 0xfe); +} + +bool IsEncryptedPrivateKeyInfo(const unsigned char* data, size_t size) { + // Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE. + size_t offset, len; + if (!IsASN1Sequence(data, size, &offset, &len)) + return false; + + // A PrivateKeyInfo sequence always starts with an integer whereas an + // EncryptedPrivateKeyInfo starts with an AlgorithmIdentifier. + return len >= 1 && + data[offset] != 2; +} + +ParseKeyResult ParsePrivateKey(EVPKeyPointer* pkey, + const PrivateKeyEncodingConfig& config, + const char* key, + size_t key_len) { + // OpenSSL needs a non-const pointer, that's why the const_cast is required. + char* const passphrase = const_cast(config.passphrase_.get()); + + if (config.format_ == kKeyFormatPEM) { + BIOPointer bio(BIO_new_mem_buf(key, key_len)); + if (!bio) + return ParseKeyResult::kParseKeyFailed; + + pkey->reset(PEM_read_bio_PrivateKey(bio.get(), + nullptr, + PasswordCallback, + passphrase)); + } else { + CHECK_EQ(config.format_, kKeyFormatDER); + + if (config.type_.ToChecked() == kKeyEncodingPKCS1) { + const unsigned char* p = reinterpret_cast(key); + pkey->reset(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, key_len)); + } else if (config.type_.ToChecked() == kKeyEncodingPKCS8) { + BIOPointer bio(BIO_new_mem_buf(key, key_len)); + if (!bio) + return ParseKeyResult::kParseKeyFailed; + + if (IsEncryptedPrivateKeyInfo( + reinterpret_cast(key), key_len)) { + pkey->reset(d2i_PKCS8PrivateKey_bio(bio.get(), + nullptr, + PasswordCallback, + passphrase)); + } else { + PKCS8Pointer p8inf(d2i_PKCS8_PRIV_KEY_INFO_bio(bio.get(), nullptr)); + if (p8inf) + pkey->reset(EVP_PKCS82PKEY(p8inf.get())); + } + } else { + CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSEC1); + const unsigned char* p = reinterpret_cast(key); + pkey->reset(d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, key_len)); + } + } + + // OpenSSL can fail to parse the key but still return a non-null pointer. + unsigned long err = ERR_peek_error(); // NOLINT(runtime/int) + if (err != 0) + pkey->reset(); + + if (*pkey) + return ParseKeyResult::kParseKeyOk; + if (ERR_GET_LIB(err) == ERR_LIB_PEM && + ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ) { + if (config.passphrase_.get() == nullptr) + return ParseKeyResult::kParseKeyNeedPassphrase; + } + return ParseKeyResult::kParseKeyFailed; +} + +Local BIOToStringOrBuffer(Environment* env, + BIO* bio, + PKFormatType format) { + BUF_MEM* bptr; + BIO_get_mem_ptr(bio, &bptr); + if (format == kKeyFormatPEM) { + // PEM is an ASCII format, so we will return it as a string. + return String::NewFromUtf8(env->isolate(), bptr->data, + NewStringType::kNormal, + bptr->length).ToLocalChecked(); + } else { + CHECK_EQ(format, kKeyFormatDER); + // DER is binary, return it as a buffer. + return Buffer::Copy(env, bptr->data, bptr->length).ToLocalChecked(); + } +} + + +MaybeLocal WritePrivateKey( + Environment* env, + EVP_PKEY* pkey, + const PrivateKeyEncodingConfig& config) { + BIOPointer bio(BIO_new(BIO_s_mem())); + CHECK(bio); + + bool err; + + PKEncodingType encoding_type = config.type_.ToChecked(); + if (encoding_type == kKeyEncodingPKCS1) { + // PKCS#1 is only permitted for RSA keys. + CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA); + + RSAPointer rsa(EVP_PKEY_get1_RSA(pkey)); + if (config.format_ == kKeyFormatPEM) { + // Encode PKCS#1 as PEM. + const char* pass = config.passphrase_.get(); + err = PEM_write_bio_RSAPrivateKey( + bio.get(), rsa.get(), + config.cipher_, + reinterpret_cast(const_cast(pass)), + config.passphrase_.size(), + nullptr, nullptr) != 1; + } else { + // Encode PKCS#1 as DER. This does not permit encryption. + CHECK_EQ(config.format_, kKeyFormatDER); + CHECK_NULL(config.cipher_); + err = i2d_RSAPrivateKey_bio(bio.get(), rsa.get()) != 1; + } + } else if (encoding_type == kKeyEncodingPKCS8) { + if (config.format_ == kKeyFormatPEM) { + // Encode PKCS#8 as PEM. + err = PEM_write_bio_PKCS8PrivateKey( + bio.get(), pkey, + config.cipher_, + const_cast(config.passphrase_.get()), + config.passphrase_.size(), + nullptr, nullptr) != 1; + } else { + // Encode PKCS#8 as DER. + CHECK_EQ(config.format_, kKeyFormatDER); + err = i2d_PKCS8PrivateKey_bio( + bio.get(), pkey, + config.cipher_, + const_cast(config.passphrase_.get()), + config.passphrase_.size(), + nullptr, nullptr) != 1; + } + } else { + CHECK_EQ(encoding_type, kKeyEncodingSEC1); + + // SEC1 is only permitted for EC keys. + CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_EC); + + ECKeyPointer ec_key(EVP_PKEY_get1_EC_KEY(pkey)); + if (config.format_ == kKeyFormatPEM) { + // Encode SEC1 as PEM. + const char* pass = config.passphrase_.get(); + err = PEM_write_bio_ECPrivateKey( + bio.get(), ec_key.get(), + config.cipher_, + reinterpret_cast(const_cast(pass)), + config.passphrase_.size(), + nullptr, nullptr) != 1; + } else { + // Encode SEC1 as DER. This does not permit encryption. + CHECK_EQ(config.format_, kKeyFormatDER); + CHECK_NULL(config.cipher_); + err = i2d_ECPrivateKey_bio(bio.get(), ec_key.get()) != 1; + } + } + + if (err) { + ThrowCryptoError(env, ERR_get_error(), "Failed to encode private key"); + return MaybeLocal(); + } + return BIOToStringOrBuffer(env, bio.get(), config.format_); +} + +bool WritePublicKeyInner(EVP_PKEY* pkey, + const BIOPointer& bio, + const PublicKeyEncodingConfig& config) { + if (config.type_.ToChecked() == kKeyEncodingPKCS1) { + // PKCS#1 is only valid for RSA keys. + CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA); + RSAPointer rsa(EVP_PKEY_get1_RSA(pkey)); + if (config.format_ == kKeyFormatPEM) { + // Encode PKCS#1 as PEM. + return PEM_write_bio_RSAPublicKey(bio.get(), rsa.get()) == 1; + } else { + // Encode PKCS#1 as DER. + CHECK_EQ(config.format_, kKeyFormatDER); + return i2d_RSAPublicKey_bio(bio.get(), rsa.get()) == 1; + } + } else { + CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSPKI); + if (config.format_ == kKeyFormatPEM) { + // Encode SPKI as PEM. + return PEM_write_bio_PUBKEY(bio.get(), pkey) == 1; + } else { + // Encode SPKI as DER. + CHECK_EQ(config.format_, kKeyFormatDER); + return i2d_PUBKEY_bio(bio.get(), pkey) == 1; + } + } +} + +MaybeLocal WritePublicKey(Environment* env, + EVP_PKEY* pkey, + const PublicKeyEncodingConfig& config) { + BIOPointer bio(BIO_new(BIO_s_mem())); + CHECK(bio); + + if (!WritePublicKeyInner(pkey, bio, config)) { + ThrowCryptoError(env, ERR_get_error(), "Failed to encode public key"); + return MaybeLocal(); + } + return BIOToStringOrBuffer(env, bio.get(), config.format_); +} + +Maybe ExportJWKSecretKey( + Environment* env, + std::shared_ptr key, + Local target) { + CHECK_EQ(key->GetKeyType(), kKeyTypeSecret); + + Local error; + Local raw; + MaybeLocal key_data = + StringBytes::Encode( + env->isolate(), + key->GetSymmetricKey(), + key->GetSymmetricKeySize(), + BASE64URL, + &error); + if (key_data.IsEmpty()) { + CHECK(!error.IsEmpty()); + env->isolate()->ThrowException(error); + return Nothing(); + } + if (!key_data.ToLocal(&raw)) + return Nothing(); + + if (target->Set( + env->context(), + env->jwk_kty_string(), + env->jwk_oct_string()).IsNothing() || + target->Set( + env->context(), + env->jwk_k_string(), + raw).IsNothing()) { + return Nothing(); + } + + return Just(true); +} + +std::shared_ptr ImportJWKSecretKey( + Environment* env, + Local jwk) { + Local key; + if (!jwk->Get(env->context(), env->jwk_k_string()).ToLocal(&key) || + !key->IsString()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK secret key format"); + return std::shared_ptr(); + } + + ByteSource key_data = ByteSource::FromEncodedString(env, key.As()); + if (key_data.size() > INT_MAX) { + THROW_ERR_CRYPTO_INVALID_KEYLEN(env); + return std::shared_ptr(); + } + + return KeyObjectData::CreateSecret(std::move(key_data)); +} + +Maybe ExportJWKAsymmetricKey( + Environment* env, + std::shared_ptr key, + Local target) { + switch (EVP_PKEY_id(key->GetAsymmetricKey().get())) { + case EVP_PKEY_RSA: + // Fall through + case EVP_PKEY_RSA_PSS: return ExportJWKRsaKey(env, key, target); + case EVP_PKEY_DSA: return ExportJWKDsaKey(env, key, target); + case EVP_PKEY_EC: return ExportJWKEcKey(env, key, target); + } + THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); + return Just(false); +} + +std::shared_ptr ImportJWKAsymmetricKey( + Environment* env, + Local jwk, + const char* kty, + const FunctionCallbackInfo& args, + unsigned int offset) { + if (strcmp(kty, "RSA") == 0) { + return ImportJWKRsaKey(env, jwk, args, offset); + } else if (strcmp(kty, "DSA") == 0) { + return ImportJWKDsaKey(env, jwk, args, offset); + } else if (strcmp(kty, "EC") == 0) { + return ImportJWKEcKey(env, jwk, args, offset); + } + + char msg[1024]; + snprintf(msg, sizeof(msg), "%s is not a support JWK key type", kty); + THROW_ERR_CRYPTO_INVALID_JWK(env, msg); + return std::shared_ptr(); +} + +Maybe GetSecretKeyDetail( + Environment* env, + std::shared_ptr key, + Local target) { + // For the secret key detail, all we care about is the length, + // converted to bits. + + size_t length = key->GetSymmetricKeySize() * CHAR_BIT; + return target->Set( + env->context(), + env->length_string(), + Number::New(env->isolate(), length)); +} + +Maybe GetAsymmetricKeyDetail( + Environment* env, + std::shared_ptr key, + Local target) { + switch (EVP_PKEY_id(key->GetAsymmetricKey().get())) { + case EVP_PKEY_RSA: + // Fall through + case EVP_PKEY_RSA_PSS: return GetRsaKeyDetail(env, key, target); + case EVP_PKEY_DSA: return GetDsaKeyDetail(env, key, target); + case EVP_PKEY_EC: return GetEcKeyDetail(env, key, target); + case EVP_PKEY_DH: return GetDhKeyDetail(env, key, target); + } + THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); + return Nothing(); +} +} // namespace + +ManagedEVPPKey::ManagedEVPPKey(EVPKeyPointer&& pkey) : pkey_(std::move(pkey)) {} + +ManagedEVPPKey::ManagedEVPPKey(const ManagedEVPPKey& that) { + *this = that; +} + +ManagedEVPPKey& ManagedEVPPKey::operator=(const ManagedEVPPKey& that) { + pkey_.reset(that.get()); + + if (pkey_) + EVP_PKEY_up_ref(pkey_.get()); + + return *this; +} + +ManagedEVPPKey::operator bool() const { + return !!pkey_; +} + +EVP_PKEY* ManagedEVPPKey::get() const { + return pkey_.get(); +} + +void ManagedEVPPKey::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("pkey", + !pkey_ ? 0 : kSizeOf_EVP_PKEY + + size_of_private_key() + + size_of_public_key()); +} + +size_t ManagedEVPPKey::size_of_private_key() const { + size_t len = 0; + return (pkey_ && EVP_PKEY_get_raw_private_key( + pkey_.get(), nullptr, &len) == 1) ? len : 0; +} + +size_t ManagedEVPPKey::size_of_public_key() const { + size_t len = 0; + return (pkey_ && EVP_PKEY_get_raw_public_key( + pkey_.get(), nullptr, &len) == 1) ? len : 0; +} + +Maybe ManagedEVPPKey::ToEncodedPublicKey( + Environment* env, + ManagedEVPPKey key, + const PublicKeyEncodingConfig& config, + Local* out) { + if (!key) return Nothing(); + if (config.output_key_object_) { + // Note that this has the downside of containing sensitive data of the + // private key. + std::shared_ptr data = + KeyObjectData::CreateAsymmetric(kKeyTypePublic, std::move(key)); + return Just(KeyObjectHandle::Create(env, data).ToLocal(out)); + } + return Just(WritePublicKey(env, key.get(), config).ToLocal(out)); +} + +Maybe ManagedEVPPKey::ToEncodedPrivateKey( + Environment* env, + ManagedEVPPKey key, + const PrivateKeyEncodingConfig& config, + Local* out) { + if (!key) return Nothing(); + if (config.output_key_object_) { + std::shared_ptr data = + KeyObjectData::CreateAsymmetric(kKeyTypePrivate, std::move(key)); + return Just(KeyObjectHandle::Create(env, data).ToLocal(out)); + } + + return Just(WritePrivateKey(env, key.get(), config).ToLocal(out)); +} + +NonCopyableMaybe +ManagedEVPPKey::GetPrivateKeyEncodingFromJs( + const FunctionCallbackInfo& args, + unsigned int* offset, + KeyEncodingContext context) { + Environment* env = Environment::GetCurrent(args); + + PrivateKeyEncodingConfig result; + GetKeyFormatAndTypeFromJs(&result, args, offset, context); + + if (result.output_key_object_) { + if (context != kKeyContextInput) + (*offset)++; + } else { + bool needs_passphrase = false; + if (context != kKeyContextInput) { + if (args[*offset]->IsString()) { + Utf8Value cipher_name(env->isolate(), args[*offset]); + result.cipher_ = EVP_get_cipherbyname(*cipher_name); + if (result.cipher_ == nullptr) { + THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env); + return NonCopyableMaybe(); + } + needs_passphrase = true; + } else { + CHECK(args[*offset]->IsNullOrUndefined()); + result.cipher_ = nullptr; + } + (*offset)++; + } + + if (IsAnyByteSource(args[*offset])) { + CHECK_IMPLIES(context != kKeyContextInput, result.cipher_ != nullptr); + ArrayBufferOrViewContents passphrase(args[*offset]); + if (UNLIKELY(!passphrase.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "passphrase is too big"); + return NonCopyableMaybe(); + } + result.passphrase_ = passphrase.ToNullTerminatedCopy(); + } else { + CHECK(args[*offset]->IsNullOrUndefined() && !needs_passphrase); + } + } + + (*offset)++; + return NonCopyableMaybe(std::move(result)); +} + +PublicKeyEncodingConfig ManagedEVPPKey::GetPublicKeyEncodingFromJs( + const FunctionCallbackInfo& args, + unsigned int* offset, + KeyEncodingContext context) { + PublicKeyEncodingConfig result; + GetKeyFormatAndTypeFromJs(&result, args, offset, context); + return result; +} + +ManagedEVPPKey ManagedEVPPKey::GetPrivateKeyFromJs( + const FunctionCallbackInfo& args, + unsigned int* offset, + bool allow_key_object) { + if (args[*offset]->IsString() || IsAnyByteSource(args[*offset])) { + Environment* env = Environment::GetCurrent(args); + ByteSource key = ByteSource::FromStringOrBuffer(env, args[(*offset)++]); + NonCopyableMaybe config = + GetPrivateKeyEncodingFromJs(args, offset, kKeyContextInput); + if (config.IsEmpty()) + return ManagedEVPPKey(); + + EVPKeyPointer pkey; + ParseKeyResult ret = + ParsePrivateKey(&pkey, config.Release(), key.get(), key.size()); + return GetParsedKey(env, std::move(pkey), ret, + "Failed to read private key"); + } else { + CHECK(args[*offset]->IsObject() && allow_key_object); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[*offset].As(), ManagedEVPPKey()); + CHECK_EQ(key->Data()->GetKeyType(), kKeyTypePrivate); + (*offset) += 4; + return key->Data()->GetAsymmetricKey(); + } +} + +ManagedEVPPKey ManagedEVPPKey::GetPublicOrPrivateKeyFromJs( + const FunctionCallbackInfo& args, + unsigned int* offset) { + if (IsAnyByteSource(args[*offset])) { + Environment* env = Environment::GetCurrent(args); + ArrayBufferOrViewContents data(args[(*offset)++]); + if (UNLIKELY(!data.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "keyData is too big"); + return ManagedEVPPKey(); + } + NonCopyableMaybe config_ = + GetPrivateKeyEncodingFromJs(args, offset, kKeyContextInput); + if (config_.IsEmpty()) + return ManagedEVPPKey(); + + ParseKeyResult ret; + PrivateKeyEncodingConfig config = config_.Release(); + EVPKeyPointer pkey; + if (config.format_ == kKeyFormatPEM) { + // For PEM, we can easily determine whether it is a public or private key + // by looking for the respective PEM tags. + ret = ParsePublicKeyPEM(&pkey, data.data(), data.size()); + if (ret == ParseKeyResult::kParseKeyNotRecognized) { + ret = ParsePrivateKey(&pkey, config, data.data(), data.size()); + } + } else { + // For DER, the type determines how to parse it. SPKI, PKCS#8 and SEC1 are + // easy, but PKCS#1 can be a public key or a private key. + bool is_public; + switch (config.type_.ToChecked()) { + case kKeyEncodingPKCS1: + is_public = !IsRSAPrivateKey( + reinterpret_cast(data.data()), data.size()); + break; + case kKeyEncodingSPKI: + is_public = true; + break; + case kKeyEncodingPKCS8: + case kKeyEncodingSEC1: + is_public = false; + break; + default: + UNREACHABLE("Invalid key encoding type"); + } + + if (is_public) { + ret = ParsePublicKey(&pkey, config, data.data(), data.size()); + } else { + ret = ParsePrivateKey(&pkey, config, data.data(), data.size()); + } + } + + return ManagedEVPPKey::GetParsedKey( + env, std::move(pkey), ret, "Failed to read asymmetric key"); + } else { + CHECK(args[*offset]->IsObject()); + KeyObjectHandle* key = Unwrap(args[*offset].As()); + CHECK_NOT_NULL(key); + CHECK_NE(key->Data()->GetKeyType(), kKeyTypeSecret); + (*offset) += 4; + return key->Data()->GetAsymmetricKey(); + } +} + +ManagedEVPPKey ManagedEVPPKey::GetParsedKey(Environment* env, + EVPKeyPointer&& pkey, + ParseKeyResult ret, + const char* default_msg) { + switch (ret) { + case ParseKeyResult::kParseKeyOk: + CHECK(pkey); + break; + case ParseKeyResult::kParseKeyNeedPassphrase: + THROW_ERR_MISSING_PASSPHRASE(env, + "Passphrase required for encrypted key"); + break; + default: + ThrowCryptoError(env, ERR_get_error(), default_msg); + } + + return ManagedEVPPKey(std::move(pkey)); +} + +KeyObjectData::KeyObjectData( + ByteSource symmetric_key) + : key_type_(KeyType::kKeyTypeSecret), + symmetric_key_(std::move(symmetric_key)), + symmetric_key_len_(symmetric_key_.size()), + asymmetric_key_() {} + +KeyObjectData::KeyObjectData( + KeyType type, + const ManagedEVPPKey& pkey) + : key_type_(type), + symmetric_key_(), + symmetric_key_len_(0), + asymmetric_key_{pkey} {} + +void KeyObjectData::MemoryInfo(MemoryTracker* tracker) const { + switch (GetKeyType()) { + case kKeyTypeSecret: + tracker->TrackFieldWithSize("symmetric_key", symmetric_key_.size()); + break; + case kKeyTypePrivate: + // Fall through + case kKeyTypePublic: + tracker->TrackFieldWithSize("key", asymmetric_key_); + break; + default: + UNREACHABLE(); + } +} + +std::shared_ptr KeyObjectData::CreateSecret( + const ArrayBufferOrViewContents& buf) { + return CreateSecret(buf.ToCopy()); +} + +std::shared_ptr KeyObjectData::CreateSecret(ByteSource key) { + CHECK(key); + return std::shared_ptr(new KeyObjectData(std::move(key))); +} + +std::shared_ptr KeyObjectData::CreateAsymmetric( + KeyType key_type, + const ManagedEVPPKey& pkey) { + CHECK(pkey); + return std::shared_ptr(new KeyObjectData(key_type, pkey)); +} + +KeyType KeyObjectData::GetKeyType() const { + return key_type_; +} + +ManagedEVPPKey KeyObjectData::GetAsymmetricKey() const { + CHECK_NE(key_type_, kKeyTypeSecret); + return asymmetric_key_; +} + +const char* KeyObjectData::GetSymmetricKey() const { + CHECK_EQ(key_type_, kKeyTypeSecret); + return symmetric_key_.get(); +} + +size_t KeyObjectData::GetSymmetricKeySize() const { + CHECK_EQ(key_type_, kKeyTypeSecret); + return symmetric_key_len_; +} + +v8::Local KeyObjectHandle::Initialize(Environment* env) { + Local templ = env->crypto_key_object_handle_constructor(); + if (!templ.IsEmpty()) { + return templ; + } + Local t = env->NewFunctionTemplate(New); + t->InstanceTemplate()->SetInternalFieldCount( + KeyObjectHandle::kInternalFieldCount); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + + env->SetProtoMethod(t, "init", Init); + env->SetProtoMethodNoSideEffect(t, "getSymmetricKeySize", + GetSymmetricKeySize); + env->SetProtoMethodNoSideEffect(t, "getAsymmetricKeyType", + GetAsymmetricKeyType); + env->SetProtoMethod(t, "export", Export); + env->SetProtoMethod(t, "exportJwk", ExportJWK); + env->SetProtoMethod(t, "initECRaw", InitECRaw); + env->SetProtoMethod(t, "initJwk", InitJWK); + env->SetProtoMethod(t, "keyDetail", GetKeyDetail); + + auto function = t->GetFunction(env->context()).ToLocalChecked(); + env->set_crypto_key_object_handle_constructor(function); + return function; +} + +MaybeLocal KeyObjectHandle::Create( + Environment* env, + std::shared_ptr data) { + Local obj; + Local ctor = KeyObjectHandle::Initialize(env); + CHECK(!env->crypto_key_object_handle_constructor().IsEmpty()); + if (!ctor->NewInstance(env->context(), 0, nullptr).ToLocal(&obj)) + return MaybeLocal(); + + KeyObjectHandle* key = Unwrap(obj); + CHECK_NOT_NULL(key); + key->data_ = data; + return obj; +} + +const std::shared_ptr& KeyObjectHandle::Data() { + return data_; +} + +void KeyObjectHandle::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + new KeyObjectHandle(env, args.This()); +} + +KeyObjectHandle::KeyObjectHandle(Environment* env, + Local wrap) + : BaseObject(env, wrap) { + MakeWeak(); +} + +void KeyObjectHandle::Init(const FunctionCallbackInfo& args) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + MarkPopErrorOnReturn mark_pop_error_on_return; + + CHECK(args[0]->IsInt32()); + KeyType type = static_cast(args[0].As()->Value()); + + unsigned int offset; + ManagedEVPPKey pkey; + + switch (type) { + case kKeyTypeSecret: { + CHECK_EQ(args.Length(), 2); + ArrayBufferOrViewContents buf(args[1]); + key->data_ = KeyObjectData::CreateSecret(buf.ToCopy()); + break; + } + case kKeyTypePublic: { + CHECK_EQ(args.Length(), 4); + + offset = 1; + pkey = ManagedEVPPKey::GetPublicOrPrivateKeyFromJs(args, &offset); + if (!pkey) + return; + key->data_ = KeyObjectData::CreateAsymmetric(type, pkey); + break; + } + case kKeyTypePrivate: { + CHECK_EQ(args.Length(), 5); + + offset = 1; + pkey = ManagedEVPPKey::GetPrivateKeyFromJs(args, &offset, false); + if (!pkey) + return; + key->data_ = KeyObjectData::CreateAsymmetric(type, pkey); + break; + } + default: + UNREACHABLE(); + } +} + +void KeyObjectHandle::InitJWK(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + MarkPopErrorOnReturn mark_pop_error_on_return; + + // The argument must be a JavaScript object that we will inspect + // to get the JWK properties from. + CHECK(args[0]->IsObject()); + + // Step one, Secret key or not? + Local input = args[0].As(); + + Local kty; + if (!input->Get(env->context(), env->jwk_kty_string()).ToLocal(&kty) || + !kty->IsString()) { + return THROW_ERR_CRYPTO_INVALID_JWK(env); + } + + Utf8Value kty_string(env->isolate(), kty); + + if (strcmp(*kty_string, "oct") == 0) { + // Secret key + key->data_ = ImportJWKSecretKey(env, input); + if (!key->data_) { + // ImportJWKSecretKey is responsible for throwing an appropriate error + return; + } + } else { + key->data_ = ImportJWKAsymmetricKey(env, input, *kty_string, args, 1); + if (!key->data_) { + // ImportJWKAsymmetricKey is responsible for throwing an appropriate error + return; + } + } + + args.GetReturnValue().Set(key->data_->GetKeyType()); +} + +void KeyObjectHandle::InitECRaw(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + + CHECK(args[0]->IsString()); + Utf8Value name(env->isolate(), args[0]); + + MarkPopErrorOnReturn mark_pop_error_on_return; + + int id = OBJ_txt2nid(*name); + ECKeyPointer eckey(EC_KEY_new_by_curve_name(id)); + if (!eckey) + return args.GetReturnValue().Set(false); + + const EC_GROUP* group = EC_KEY_get0_group(eckey.get()); + ECPointPointer pub(ECDH::BufferToPoint(env, group, args[1])); + + if (!pub || + !eckey || + !EC_KEY_set_public_key(eckey.get(), pub.get())) { + return args.GetReturnValue().Set(false); + } + + EVPKeyPointer pkey(EVP_PKEY_new()); + if (!EVP_PKEY_assign_EC_KEY(pkey.get(), eckey.get())) + args.GetReturnValue().Set(false); + + eckey.release(); // Release ownership of the key + + key->data_ = + KeyObjectData::CreateAsymmetric( + kKeyTypePublic, + ManagedEVPPKey(std::move(pkey))); + + args.GetReturnValue().Set(true); +} + +void KeyObjectHandle::GetKeyDetail(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + + CHECK(args[0]->IsObject()); + + std::shared_ptr data = key->Data(); + + switch (data->GetKeyType()) { + case kKeyTypeSecret: + if (GetSecretKeyDetail(env, data, args[0].As()).IsNothing()) + return; + break; + case kKeyTypePublic: + // Fall through + case kKeyTypePrivate: + if (GetAsymmetricKeyDetail(env, data, args[0].As()).IsNothing()) + return; + break; + default: + UNREACHABLE(); + } + + args.GetReturnValue().Set(args[0]); +} + +Local KeyObjectHandle::GetAsymmetricKeyType() const { + const ManagedEVPPKey& key = data_->GetAsymmetricKey(); + switch (EVP_PKEY_id(key.get())) { + case EVP_PKEY_RSA: + return env()->crypto_rsa_string(); + case EVP_PKEY_RSA_PSS: + return env()->crypto_rsa_pss_string(); + case EVP_PKEY_DSA: + return env()->crypto_dsa_string(); + case EVP_PKEY_DH: + return env()->crypto_dh_string(); + case EVP_PKEY_EC: + return env()->crypto_ec_string(); + case EVP_PKEY_ED25519: + return env()->crypto_ed25519_string(); + case EVP_PKEY_ED448: + return env()->crypto_ed448_string(); + case EVP_PKEY_X25519: + return env()->crypto_x25519_string(); + case EVP_PKEY_X448: + return env()->crypto_x448_string(); + default: + return Undefined(env()->isolate()); + } +} + +void KeyObjectHandle::GetAsymmetricKeyType( + const FunctionCallbackInfo& args) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + + args.GetReturnValue().Set(key->GetAsymmetricKeyType()); +} + +void KeyObjectHandle::GetSymmetricKeySize( + const FunctionCallbackInfo& args) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + args.GetReturnValue().Set( + static_cast(key->Data()->GetSymmetricKeySize())); +} + +void KeyObjectHandle::Export(const FunctionCallbackInfo& args) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + + KeyType type = key->Data()->GetKeyType(); + + MaybeLocal result; + if (type == kKeyTypeSecret) { + result = key->ExportSecretKey(); + } else if (type == kKeyTypePublic) { + unsigned int offset = 0; + PublicKeyEncodingConfig config = + ManagedEVPPKey::GetPublicKeyEncodingFromJs( + args, &offset, kKeyContextExport); + CHECK_EQ(offset, static_cast(args.Length())); + result = key->ExportPublicKey(config); + } else { + CHECK_EQ(type, kKeyTypePrivate); + unsigned int offset = 0; + NonCopyableMaybe config = + ManagedEVPPKey::GetPrivateKeyEncodingFromJs( + args, &offset, kKeyContextExport); + if (config.IsEmpty()) + return; + CHECK_EQ(offset, static_cast(args.Length())); + result = key->ExportPrivateKey(config.Release()); + } + + if (!result.IsEmpty()) + args.GetReturnValue().Set(result.ToLocalChecked()); +} + +Local KeyObjectHandle::ExportSecretKey() const { + const char* buf = data_->GetSymmetricKey(); + unsigned int len = data_->GetSymmetricKeySize(); + return Buffer::Copy(env(), buf, len).ToLocalChecked(); +} + +MaybeLocal KeyObjectHandle::ExportPublicKey( + const PublicKeyEncodingConfig& config) const { + return WritePublicKey(env(), data_->GetAsymmetricKey().get(), config); +} + +MaybeLocal KeyObjectHandle::ExportPrivateKey( + const PrivateKeyEncodingConfig& config) const { + return WritePrivateKey(env(), data_->GetAsymmetricKey().get(), config); +} + +void KeyObjectHandle::ExportJWK( + const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); + + CHECK(args[0]->IsObject()); + + switch (key->Data()->GetKeyType()) { + case kKeyTypeSecret: + if (ExportJWKSecretKey(env, key->Data(), args[0].As()) + .IsNothing()) { + return; + } + break; + case kKeyTypePublic: + // Fall through + case kKeyTypePrivate: + if (ExportJWKAsymmetricKey(env, key->Data(), args[0].As()) + .IsNothing()) { + return; + } + break; + default: + UNREACHABLE(); + } + + args.GetReturnValue().Set(args[0]); +} + +void NativeKeyObject::Initialize(Environment* env, Local target) { + env->SetMethod(target, "createNativeKeyObjectClass", + NativeKeyObject::CreateNativeKeyObjectClass); +} + +void NativeKeyObject::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsObject()); + KeyObjectHandle* handle = Unwrap(args[0].As()); + new NativeKeyObject(env, args.This(), handle->Data()); +} + +void NativeKeyObject::CreateNativeKeyObjectClass( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK_EQ(args.Length(), 1); + Local callback = args[0]; + CHECK(callback->IsFunction()); + + Local t = env->NewFunctionTemplate(NativeKeyObject::New); + t->InstanceTemplate()->SetInternalFieldCount( + KeyObjectHandle::kInternalFieldCount); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + + Local ctor = t->GetFunction(env->context()).ToLocalChecked(); + + Local recv = Undefined(env->isolate()); + Local ret_v; + if (!callback.As()->Call( + env->context(), recv, 1, &ctor).ToLocal(&ret_v)) { + return; + } + Local ret = ret_v.As(); + if (!ret->Get(env->context(), 1).ToLocal(&ctor)) return; + env->set_crypto_key_object_secret_constructor(ctor.As()); + if (!ret->Get(env->context(), 2).ToLocal(&ctor)) return; + env->set_crypto_key_object_public_constructor(ctor.As()); + if (!ret->Get(env->context(), 3).ToLocal(&ctor)) return; + env->set_crypto_key_object_private_constructor(ctor.As()); + args.GetReturnValue().Set(ret); +} + +BaseObjectPtr NativeKeyObject::KeyObjectTransferData::Deserialize( + Environment* env, + Local context, + std::unique_ptr self) { + if (context != env->context()) { + THROW_ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE(env); + return {}; + } + + Local handle = KeyObjectHandle::Create(env, data_).ToLocalChecked(); + Local key_ctor; + Local arg = FIXED_ONE_BYTE_STRING(env->isolate(), + "internal/crypto/keys"); + if (env->native_module_require()-> + Call(context, Null(env->isolate()), 1, &arg).IsEmpty()) { + return {}; + } + switch (data_->GetKeyType()) { + case kKeyTypeSecret: + key_ctor = env->crypto_key_object_secret_constructor(); + break; + case kKeyTypePublic: + key_ctor = env->crypto_key_object_public_constructor(); + break; + case kKeyTypePrivate: + key_ctor = env->crypto_key_object_private_constructor(); + break; + default: + CHECK(false); + } + + Local key = + key_ctor->NewInstance(context, 1, &handle).ToLocalChecked(); + return BaseObjectPtr(Unwrap(key.As())); +} + +BaseObject::TransferMode NativeKeyObject::GetTransferMode() const { + return BaseObject::TransferMode::kCloneable; +} + +std::unique_ptr NativeKeyObject::CloneForMessaging() + const { + return std::make_unique(handle_data_); +} + +WebCryptoKeyExportStatus PKEY_SPKI_Export( + KeyObjectData* key_data, + ByteSource* out) { + CHECK_EQ(key_data->GetKeyType(), kKeyTypePublic); + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!i2d_PUBKEY_bio(bio.get(), key_data->GetAsymmetricKey().get())) + return WebCryptoKeyExportStatus::ERR_FAILED; + + *out = ByteSource::FromBIO(bio); + return WebCryptoKeyExportStatus::ERR_OK; +} + +WebCryptoKeyExportStatus PKEY_PKCS8_Export( + KeyObjectData* key_data, + ByteSource* out) { + CHECK_EQ(key_data->GetKeyType(), kKeyTypePrivate); + BIOPointer bio(BIO_new(BIO_s_mem())); + PKCS8Pointer p8inf(EVP_PKEY2PKCS8(key_data->GetAsymmetricKey().get())); + if (!i2d_PKCS8_PRIV_KEY_INFO_bio(bio.get(), p8inf.get())) + return WebCryptoKeyExportStatus::ERR_FAILED; + + *out = ByteSource::FromBIO(bio); + return WebCryptoKeyExportStatus::ERR_OK; +} + +namespace Keys { +void Initialize(Environment* env, Local target) { + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "KeyObjectHandle"), + KeyObjectHandle::Initialize(env)).Check(); + + NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatRaw); + NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatPKCS8); + NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatSPKI); + NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatJWK); + + NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED25519); + NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED448); + NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519); + NODE_DEFINE_CONSTANT(target, EVP_PKEY_X448); + NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1); + NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS8); + NODE_DEFINE_CONSTANT(target, kKeyEncodingSPKI); + NODE_DEFINE_CONSTANT(target, kKeyEncodingSEC1); + NODE_DEFINE_CONSTANT(target, kKeyFormatDER); + NODE_DEFINE_CONSTANT(target, kKeyFormatPEM); + NODE_DEFINE_CONSTANT(target, kKeyTypeSecret); + NODE_DEFINE_CONSTANT(target, kKeyTypePublic); + NODE_DEFINE_CONSTANT(target, kKeyTypePrivate); + NODE_DEFINE_CONSTANT(target, kSigEncDER); + NODE_DEFINE_CONSTANT(target, kSigEncP1363); +} +} // namespace Keys + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h new file mode 100644 index 00000000000000..2fc28c75745dd3 --- /dev/null +++ b/src/crypto/crypto_keys.h @@ -0,0 +1,401 @@ +#ifndef SRC_CRYPTO_CRYPTO_KEYS_H_ +#define SRC_CRYPTO_CRYPTO_KEYS_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_util.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" +#include "node_buffer.h" +#include "node_worker.h" +#include "v8.h" + +#include + +#include +#include + +namespace node { +namespace crypto { +enum PKEncodingType { + // RSAPublicKey / RSAPrivateKey according to PKCS#1. + kKeyEncodingPKCS1, + // PrivateKeyInfo or EncryptedPrivateKeyInfo according to PKCS#8. + kKeyEncodingPKCS8, + // SubjectPublicKeyInfo according to X.509. + kKeyEncodingSPKI, + // ECPrivateKey according to SEC1. + kKeyEncodingSEC1 +}; + +enum PKFormatType { + kKeyFormatDER, + kKeyFormatPEM +}; + +enum KeyType { + kKeyTypeSecret, + kKeyTypePublic, + kKeyTypePrivate +}; + +enum KeyEncodingContext { + kKeyContextInput, + kKeyContextExport, + kKeyContextGenerate +}; + +enum class ParseKeyResult { + kParseKeyOk, + kParseKeyNotRecognized, + kParseKeyNeedPassphrase, + kParseKeyFailed +}; + +struct AsymmetricKeyEncodingConfig { + bool output_key_object_ = false; + PKFormatType format_ = kKeyFormatDER; + v8::Maybe type_ = v8::Nothing(); +}; + +using PublicKeyEncodingConfig = AsymmetricKeyEncodingConfig; + +struct PrivateKeyEncodingConfig : public AsymmetricKeyEncodingConfig { + const EVP_CIPHER* cipher_; + ByteSource passphrase_; +}; + +// This uses the built-in reference counter of OpenSSL to manage an EVP_PKEY +// which is slightly more efficient than using a shared pointer and easier to +// use. +class ManagedEVPPKey : public MemoryRetainer { + public: + ManagedEVPPKey() = default; + explicit ManagedEVPPKey(EVPKeyPointer&& pkey); + ManagedEVPPKey(const ManagedEVPPKey& that); + ManagedEVPPKey& operator=(const ManagedEVPPKey& that); + + operator bool() const; + EVP_PKEY* get() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(ManagedEVPPKey) + SET_SELF_SIZE(ManagedEVPPKey) + + static PublicKeyEncodingConfig GetPublicKeyEncodingFromJs( + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + KeyEncodingContext context); + + static NonCopyableMaybe GetPrivateKeyEncodingFromJs( + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + KeyEncodingContext context); + + static ManagedEVPPKey GetParsedKey(Environment* env, + EVPKeyPointer&& pkey, + ParseKeyResult ret, + const char* default_msg); + + static ManagedEVPPKey GetPublicOrPrivateKeyFromJs( + const v8::FunctionCallbackInfo& args, + unsigned int* offset); + + static ManagedEVPPKey GetPrivateKeyFromJs( + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + bool allow_key_object); + + static v8::Maybe ToEncodedPublicKey( + Environment* env, + ManagedEVPPKey key, + const PublicKeyEncodingConfig& config, + v8::Local* out); + + static v8::Maybe ToEncodedPrivateKey( + Environment* env, + ManagedEVPPKey key, + const PrivateKeyEncodingConfig& config, + v8::Local* out); + + private: + size_t size_of_private_key() const; + size_t size_of_public_key() const; + + EVPKeyPointer pkey_; +}; + +// Objects of this class can safely be shared among threads. +class KeyObjectData : public MemoryRetainer { + public: + static std::shared_ptr CreateSecret( + const ArrayBufferOrViewContents& buf); + + static std::shared_ptr CreateSecret(ByteSource key); + + static std::shared_ptr CreateAsymmetric( + KeyType type, + const ManagedEVPPKey& pkey); + + KeyType GetKeyType() const; + + // These functions allow unprotected access to the raw key material and should + // only be used to implement cryptographic operations requiring the key. + ManagedEVPPKey GetAsymmetricKey() const; + const char* GetSymmetricKey() const; + size_t GetSymmetricKeySize() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(KeyObjectData); + SET_SELF_SIZE(KeyObjectData); + + private: + explicit KeyObjectData(ByteSource symmetric_key); + + KeyObjectData( + KeyType type, + const ManagedEVPPKey& pkey); + + const KeyType key_type_; + const ByteSource symmetric_key_; + const unsigned int symmetric_key_len_; + const ManagedEVPPKey asymmetric_key_; +}; + +class KeyObjectHandle : public BaseObject { + public: + static v8::Local Initialize(Environment* env); + + static v8::MaybeLocal Create(Environment* env, + std::shared_ptr data); + + // TODO(tniessen): track the memory used by OpenSSL types + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(KeyObjectHandle) + SET_SELF_SIZE(KeyObjectHandle) + + const std::shared_ptr& Data(); + + protected: + static void New(const v8::FunctionCallbackInfo& args); + + static void Init(const v8::FunctionCallbackInfo& args); + static void InitECRaw(const v8::FunctionCallbackInfo& args); + static void InitJWK(const v8::FunctionCallbackInfo& args); + static void GetKeyDetail(const v8::FunctionCallbackInfo& args); + + static void ExportJWK(const v8::FunctionCallbackInfo& args); + + static void GetAsymmetricKeyType( + const v8::FunctionCallbackInfo& args); + v8::Local GetAsymmetricKeyType() const; + + static void GetSymmetricKeySize( + const v8::FunctionCallbackInfo& args); + + static void Export(const v8::FunctionCallbackInfo& args); + + v8::Local ExportSecretKey() const; + v8::MaybeLocal ExportPublicKey( + const PublicKeyEncodingConfig& config) const; + v8::MaybeLocal ExportPrivateKey( + const PrivateKeyEncodingConfig& config) const; + + KeyObjectHandle(Environment* env, + v8::Local wrap); + + private: + std::shared_ptr data_; +}; + +class NativeKeyObject : public BaseObject { + public: + static void Initialize(Environment* env, v8::Local target); + + static void New(const v8::FunctionCallbackInfo& args); + static void CreateNativeKeyObjectClass( + const v8::FunctionCallbackInfo& args); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(NativeKeyObject) + SET_SELF_SIZE(NativeKeyObject) + + class KeyObjectTransferData : public worker::TransferData { + public: + explicit KeyObjectTransferData(const std::shared_ptr& data) + : data_(data) {} + + BaseObjectPtr Deserialize( + Environment* env, + v8::Local context, + std::unique_ptr self) override; + + SET_MEMORY_INFO_NAME(KeyObjectTransferData) + SET_SELF_SIZE(KeyObjectTransferData) + SET_NO_MEMORY_INFO() + + private: + std::shared_ptr data_; + }; + + BaseObject::TransferMode GetTransferMode() const override; + std::unique_ptr CloneForMessaging() const override; + + private: + NativeKeyObject(Environment* env, + v8::Local wrap, + const std::shared_ptr& handle_data) + : BaseObject(env, wrap), + handle_data_(handle_data) { + MakeWeak(); + } + + std::shared_ptr handle_data_; +}; + +enum WebCryptoKeyFormat { + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatSPKI, + kWebCryptoKeyFormatJWK +}; + +enum class WebCryptoKeyExportStatus { + ERR_OK, + ERR_INVALID_KEY_TYPE, + ERR_FAILED +}; + +template +class KeyExportJob final : public CryptoJob { + public: + using AdditionalParams = typename KeyExportTraits::AdditionalParameters; + + static void New(const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + + CryptoJobMode mode = GetCryptoJobMode(args[0]); + + CHECK(args[1]->IsUint32()); // Export Type + CHECK(args[2]->IsObject()); // KeyObject + + WebCryptoKeyFormat format = + static_cast(args[1].As()->Value()); + + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[2]); + + CHECK_NOT_NULL(key); + + AdditionalParams params; + if (KeyExportTraits::AdditionalConfig(args, 3, ¶ms).IsNothing()) { + // The KeyExportTraits::AdditionalConfig is responsible for + // calling an appropriate THROW_CRYPTO_* variant reporting + // whatever error caused initialization to fail. + return; + } + + new KeyExportJob( + env, + args.This(), + mode, + key->Data(), + format, + std::move(params)); + } + + static void Initialize( + Environment* env, + v8::Local target) { + CryptoJob::Initialize(New, env, target); + } + + KeyExportJob( + Environment* env, + v8::Local object, + CryptoJobMode mode, + std::shared_ptr key, + WebCryptoKeyFormat format, + AdditionalParams&& params) + : CryptoJob( + env, + object, + AsyncWrap::PROVIDER_KEYEXPORTREQUEST, + mode, + std::move(params)), + key_(key), + format_(format) {} + + WebCryptoKeyFormat format() const { return format_; } + + void DoThreadPoolWork() override { + switch (KeyExportTraits::DoExport( + key_, + format_, + *CryptoJob::params(), + &out_)) { + case WebCryptoKeyExportStatus::ERR_OK: + // Success! + break; + case WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE: + // Fall through + // TODO(@jasnell): Separate error for this + case WebCryptoKeyExportStatus::ERR_FAILED: { + CryptoErrorVector* errors = CryptoJob::errors(); + errors->Capture(); + if (errors->empty()) + errors->push_back("Key export failed."); + } + } + } + + v8::Maybe ToResult( + v8::Local* err, + v8::Local* result) override { + Environment* env = AsyncWrap::env(); + CryptoErrorVector* errors = CryptoJob::errors(); + if (out_.size() > 0) { + CHECK(errors->empty()); + *err = v8::Undefined(env->isolate()); + *result = out_.ToArrayBuffer(env); + return v8::Just(!result->IsEmpty()); + } + + if (errors->empty()) + errors->Capture(); + CHECK(!errors->empty()); + *result = v8::Undefined(env->isolate()); + return v8::Just(errors->ToException(env).ToLocal(err)); + } + + SET_SELF_SIZE(KeyExportJob) + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackFieldWithSize("out", out_.size()); + CryptoJob::MemoryInfo(tracker); + } + + private: + std::shared_ptr key_; + WebCryptoKeyFormat format_; + ByteSource out_; +}; + +WebCryptoKeyExportStatus PKEY_SPKI_Export( + KeyObjectData* key_data, + ByteSource* out); + +WebCryptoKeyExportStatus PKEY_PKCS8_Export( + KeyObjectData* key_data, + ByteSource* out); + +namespace Keys { +void Initialize(Environment* env, v8::Local target); +} // namespace Keys + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_KEYS_H_ diff --git a/src/crypto/crypto_pbkdf2.cc b/src/crypto/crypto_pbkdf2.cc new file mode 100644 index 00000000000000..8cf36cdf8dc036 --- /dev/null +++ b/src/crypto/crypto_pbkdf2.cc @@ -0,0 +1,149 @@ +#include "crypto/crypto_pbkdf2.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_buffer.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Int32; +using v8::Just; +using v8::Maybe; +using v8::Nothing; +using v8::Value; + +namespace crypto { +PBKDF2Config::PBKDF2Config(PBKDF2Config&& other) noexcept + : mode(other.mode), + pass(std::move(other.pass)), + salt(std::move(other.salt)), + iterations(other.iterations), + length(other.length), + digest(other.digest) {} + +PBKDF2Config& PBKDF2Config::operator=(PBKDF2Config&& other) noexcept { + if (&other == this) return *this; + this->~PBKDF2Config(); + return *new (this) PBKDF2Config(std::move(other)); +} + +void PBKDF2Config::MemoryInfo(MemoryTracker* tracker) const { + // The the job is sync, the PBKDF2Config does not own the data + if (mode == kCryptoJobAsync) { + tracker->TrackFieldWithSize("pass", pass.size()); + tracker->TrackFieldWithSize("salt", salt.size()); + } +} + +Maybe PBKDF2Traits::EncodeOutput( + Environment* env, + const PBKDF2Config& params, + ByteSource* out, + v8::Local* result) { + *result = out->ToArrayBuffer(env); + return Just(!result->IsEmpty()); +} + +// The input arguments for the job are: +// 1. CryptoJobMode +// 2. The password +// 3. The salt +// 4. The number of iterations +// 5. The number of bytes to generate +// 6. The digest algorithm name +Maybe PBKDF2Traits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + PBKDF2Config* params) { + Environment* env = Environment::GetCurrent(args); + + params->mode = mode; + + ArrayBufferOrViewContents pass(args[offset]); + ArrayBufferOrViewContents salt(args[offset + 1]); + + if (UNLIKELY(!pass.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); + return Nothing(); + } + + if (UNLIKELY(!salt.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "salt is too large"); + return Nothing(); + } + + params->pass = mode == kCryptoJobAsync + ? pass.ToCopy() + : pass.ToByteSource(); + + params->salt = mode == kCryptoJobAsync + ? salt.ToCopy() + : salt.ToByteSource(); + + CHECK(args[offset + 2]->IsInt32()); // iteration_count + CHECK(args[offset + 3]->IsInt32()); // length + CHECK(args[offset + 4]->IsString()); // digest_name + + params->iterations = args[offset + 2].As()->Value(); + if (params->iterations < 0) { + char msg[1024]; + snprintf(msg, sizeof(msg), "iterations must be <= %d", INT_MAX); + THROW_ERR_OUT_OF_RANGE(env, msg); + return Nothing(); + } + + params->length = args[offset + 3].As()->Value(); + if (params->length < 0) { + char msg[1024]; + snprintf(msg, sizeof(msg), "length must be <= %d", INT_MAX); + THROW_ERR_OUT_OF_RANGE(env, msg); + return Nothing(); + } + + Utf8Value name(args.GetIsolate(), args[offset + 4]); + params->digest = EVP_get_digestbyname(*name); + if (params->digest == nullptr) { + char errmsg[1024]; + snprintf(errmsg, sizeof(errmsg), "Invalid digest: %s", *name); + THROW_ERR_CRYPTO_INVALID_DIGEST(env, errmsg); + return Nothing(); + } + + return Just(true); +} + +bool PBKDF2Traits::DeriveBits( + Environment* env, + const PBKDF2Config& params, + ByteSource* out) { + char* data = MallocOpenSSL(params.length); + ByteSource buf = ByteSource::Allocated(data, params.length); + unsigned char* ptr = reinterpret_cast(data); + + // Both pass and salt may be zero length here. + // The generated bytes are stored in buf, which is + // assigned to out on success. + + if (PKCS5_PBKDF2_HMAC( + params.pass.get(), + params.pass.size(), + params.salt.data(), + params.salt.size(), + params.iterations, + params.digest, + params.length, + ptr) <= 0) { + return false; + } + *out = std::move(buf); + return true; +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_pbkdf2.h b/src/crypto/crypto_pbkdf2.h new file mode 100644 index 00000000000000..42b95627e0da62 --- /dev/null +++ b/src/crypto/crypto_pbkdf2.h @@ -0,0 +1,76 @@ +#ifndef SRC_CRYPTO_CRYPTO_PBKDF2_H_ +#define SRC_CRYPTO_CRYPTO_PBKDF2_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_util.h" +#include "async_wrap.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +namespace node { +namespace crypto { +// PBKDF2 is a pseudo-random key derivation scheme defined +// in https://tools.ietf.org/html/rfc8018 +// +// The algorithm takes as input a password and salt +// (both of which may, but should not be zero-length), +// a number of iterations, a hash digest algorithm, and +// an output length. +// +// The salt should be as unique as possible, and should +// be at least 16 bytes in length. +// +// The iteration count should be as high as possible. + +struct PBKDF2Config final : public MemoryRetainer { + CryptoJobMode mode; + ByteSource pass; + ByteSource salt; + int32_t iterations; + int32_t length; + const EVP_MD* digest = nullptr; + + PBKDF2Config() = default; + + explicit PBKDF2Config(PBKDF2Config&& other) noexcept; + + PBKDF2Config& operator=(PBKDF2Config&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(PBKDF2Config); + SET_SELF_SIZE(PBKDF2Config); +}; + +struct PBKDF2Traits final { + using AdditionalParameters = PBKDF2Config; + static constexpr const char* JobName = "PBKDF2Job"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_PBKDF2REQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + PBKDF2Config* params); + + static bool DeriveBits( + Environment* env, + const PBKDF2Config& params, + ByteSource* out); + + static v8::Maybe EncodeOutput( + Environment* env, + const PBKDF2Config& params, + ByteSource* out, + v8::Local* result); +}; + +using PBKDF2Job = DeriveBitsJob; + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_PBKDF2_H_ diff --git a/src/crypto/crypto_random.cc b/src/crypto/crypto_random.cc new file mode 100644 index 00000000000000..bad2edfed3062e --- /dev/null +++ b/src/crypto/crypto_random.cc @@ -0,0 +1,73 @@ +#include "crypto/crypto_random.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Object; +using v8::Uint32; +using v8::Value; + +namespace crypto { +Maybe RandomBytesTraits::EncodeOutput( + Environment* env, + const RandomBytesConfig& params, + ByteSource* unused, + v8::Local* result) { + *result = v8::Undefined(env->isolate()); + return Just(!result->IsEmpty()); +} + +Maybe RandomBytesTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + RandomBytesConfig* params) { + Environment* env = Environment::GetCurrent(args); + CHECK(IsAnyByteSource(args[offset])); // Buffer to fill + CHECK(args[offset + 1]->IsUint32()); // Offset + CHECK(args[offset + 2]->IsUint32()); // Size + + ArrayBufferOrViewContents in(args[offset]); + + const uint32_t byte_offset = args[offset + 1].As()->Value(); + const uint32_t size = args[offset + 2].As()->Value(); + CHECK_GE(byte_offset + size, byte_offset); // Overflow check. + CHECK_LE(byte_offset + size, in.size()); // Bounds check. + + if (UNLIKELY(size > INT_MAX)) { + THROW_ERR_OUT_OF_RANGE(env, "buffer is too large"); + return Nothing(); + } + + params->buffer = in.data() + byte_offset; + params->size = size; + + return Just(true); +} + +bool RandomBytesTraits::DeriveBits( + Environment* env, + const RandomBytesConfig& params, + ByteSource* unused) { + CheckEntropy(); // Ensure that OpenSSL's PRNG is properly seeded. + return RAND_bytes(params.buffer, params.size) != 0; +} + +namespace Random { +void Initialize(Environment* env, Local target) { + RandomBytesJob::Initialize(env, target); +} +} // namespace Random +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_random.h b/src/crypto/crypto_random.h new file mode 100644 index 00000000000000..b9171c83d33e21 --- /dev/null +++ b/src/crypto/crypto_random.h @@ -0,0 +1,57 @@ +#ifndef SRC_CRYPTO_CRYPTO_RANDOM_H_ +#define SRC_CRYPTO_CRYPTO_RANDOM_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_util.h" +#include "base_object.h" +#include "allocated_buffer.h" +#include "env.h" +#include "memory_tracker.h" +#include "node_internals.h" +#include "v8.h" + +namespace node { +namespace crypto { +struct RandomBytesConfig final : public MemoryRetainer { + unsigned char* buffer; + size_t size; + SET_NO_MEMORY_INFO(); + SET_MEMORY_INFO_NAME(RandomBytesConfig); + SET_SELF_SIZE(RandomBytesConfig); +}; + +struct RandomBytesTraits final { + using AdditionalParameters = RandomBytesConfig; + static constexpr const char* JobName = "RandomBytesJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_RANDOMBYTESREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + RandomBytesConfig* params); + + static bool DeriveBits( + Environment* env, + const RandomBytesConfig& params, + ByteSource* out_); + + static v8::Maybe EncodeOutput( + Environment* env, + const RandomBytesConfig& params, + ByteSource* unused, + v8::Local* result); +}; + +using RandomBytesJob = DeriveBitsJob; + +namespace Random { +void Initialize(Environment* env, v8::Local target); +} // namespace Random +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_RANDOM_H_ diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc new file mode 100644 index 00000000000000..3576e6084c4a76 --- /dev/null +++ b/src/crypto/crypto_rsa.cc @@ -0,0 +1,550 @@ +#include "crypto/crypto_rsa.h" +#include "crypto/crypto_bio.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +#include +#include + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Int32; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Number; +using v8::Object; +using v8::String; +using v8::Uint32; +using v8::Value; + +namespace crypto { +EVPKeyCtxPointer RsaKeyGenTraits::Setup(RsaKeyPairGenConfig* params) { + EVPKeyCtxPointer ctx( + EVP_PKEY_CTX_new_id( + params->params.variant == kKeyVariantRSA_PSS + ? EVP_PKEY_RSA_PSS + : EVP_PKEY_RSA, + nullptr)); + + if (EVP_PKEY_keygen_init(ctx.get()) <= 0) + return EVPKeyCtxPointer(); + + if (EVP_PKEY_CTX_set_rsa_keygen_bits( + ctx.get(), + params->params.modulus_bits) <= 0) { + return EVPKeyCtxPointer(); + } + + // 0x10001 is the default RSA exponent. + if (params->params.exponent != 0x10001) { + BignumPointer bn(BN_new()); + CHECK_NOT_NULL(bn.get()); + CHECK(BN_set_word(bn.get(), params->params.exponent)); + // EVP_CTX accepts ownership of bn on success. + if (EVP_PKEY_CTX_set_rsa_keygen_pubexp(ctx.get(), bn.get()) <= 0) + return EVPKeyCtxPointer(); + + bn.release(); + } + + if (params->params.variant == kKeyVariantRSA_PSS) { + if (params->params.md != nullptr && + EVP_PKEY_CTX_set_rsa_pss_keygen_md(ctx.get(), params->params.md) <= 0) { + return EVPKeyCtxPointer(); + } + + if (params->params.mgf1_md != nullptr && + EVP_PKEY_CTX_set_rsa_pss_keygen_mgf1_md( + ctx.get(), + params->params.mgf1_md) <= 0) { + return EVPKeyCtxPointer(); + } + + if (params->params.saltlen >= 0 && + EVP_PKEY_CTX_set_rsa_pss_keygen_saltlen( + ctx.get(), + params->params.saltlen) <= 0) { + return EVPKeyCtxPointer(); + } + } + + return ctx; +} + +// Input parameters to the RsaKeyGenJob: +// For key variants RSA-OAEP and RSA-SSA-PKCS1-v1_5 +// 1. CryptoJobMode +// 2. Key Variant +// 3. Modulus Bits +// 4. Public Exponent +// 5. Public Format +// 6. Public Type +// 7. Private Format +// 8. Private Type +// 9. Cipher +// 10. Passphrase +// +// For RSA-PSS variant +// 1. CryptoJobMode +// 2. Key Variant +// 3. Modulus Bits +// 4. Public Exponent +// 5. Digest +// 6. mgf1 Digest +// 7. Salt length +// 8. Public Format +// 9. Public Type +// 10. Private Format +// 11. Private Type +// 12. Cipher +// 13. Passphrase +Maybe RsaKeyGenTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int* offset, + RsaKeyPairGenConfig* params) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args[*offset]->IsUint32()); // Variant + CHECK(args[*offset + 1]->IsUint32()); // Modulus bits + CHECK(args[*offset + 2]->IsUint32()); // Exponent + + params->params.variant = + static_cast(args[*offset].As()->Value()); + + CHECK_IMPLIES(params->params.variant != kKeyVariantRSA_PSS, + args.Length() == 10); + CHECK_IMPLIES(params->params.variant == kKeyVariantRSA_PSS, + args.Length() == 13); + + params->params.modulus_bits = args[*offset + 1].As()->Value(); + params->params.exponent = args[*offset + 2].As()->Value(); + + *offset += 3; + + if (params->params.variant == kKeyVariantRSA_PSS) { + if (!args[*offset]->IsUndefined()) { + CHECK(args[*offset]->IsString()); + Utf8Value digest(env->isolate(), args[*offset]); + params->params.md = EVP_get_digestbyname(*digest); + if (params->params.md == nullptr) { + char msg[1024]; + snprintf(msg, sizeof(msg), "md specifies an invalid digest"); + THROW_ERR_CRYPTO_INVALID_DIGEST(env, msg); + return Nothing(); + } + } + + if (!args[*offset + 1]->IsUndefined()) { + CHECK(args[*offset + 1]->IsString()); + Utf8Value digest(env->isolate(), args[*offset + 1]); + params->params.mgf1_md = EVP_get_digestbyname(*digest); + if (params->params.mgf1_md == nullptr) { + char msg[1024]; + snprintf(msg, sizeof(msg), "mgf1_md specifies an invalid digest"); + THROW_ERR_CRYPTO_INVALID_DIGEST(env, msg); + return Nothing(); + } + } + + if (!args[*offset + 2]->IsUndefined()) { + CHECK(args[*offset + 2]->IsInt32()); + params->params.saltlen = args[*offset + 2].As()->Value(); + if (params->params.saltlen < 0) { + char msg[1024]; + snprintf(msg, sizeof(msg), "salt length is out of range"); + THROW_ERR_OUT_OF_RANGE(env, msg); + return Nothing(); + } + } + + *offset += 3; + } + + return Just(true); +} + +namespace { +WebCryptoKeyExportStatus RSA_JWK_Export( + KeyObjectData* key_data, + const RSAKeyExportConfig& params, + ByteSource* out) { + return WebCryptoKeyExportStatus::ERR_FAILED; +} + +template +WebCryptoCipherStatus RSA_Cipher( + Environment* env, + KeyObjectData* key_data, + const RSACipherConfig& params, + const ByteSource& in, + ByteSource* out) { + CHECK_NE(key_data->GetKeyType(), kKeyTypeSecret); + + EVPKeyCtxPointer ctx( + EVP_PKEY_CTX_new(key_data->GetAsymmetricKey().get(), nullptr)); + + if (!ctx || init(ctx.get()) <= 0) + return WebCryptoCipherStatus::ERR_FAILED; + + if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), params.padding) <= 0) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + if (params.digest != nullptr && + (EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), params.digest) <= 0 || + EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), params.digest) <= 0)) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + size_t label_len = params.label.size(); + if (label_len > 0) { + void* label = OPENSSL_memdup(params.label.get(), label_len); + CHECK_NOT_NULL(label); + if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx.get(), label, label_len) <= 0) { + OPENSSL_free(label); + return WebCryptoCipherStatus::ERR_FAILED; + } + } + + size_t out_len = 0; + if (cipher( + ctx.get(), + nullptr, + &out_len, + in.data(), + in.size()) <= 0) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + char* data = MallocOpenSSL(out_len); + ByteSource buf = ByteSource::Allocated(data, out_len); + unsigned char* ptr = reinterpret_cast(data); + + if (cipher( + ctx.get(), + ptr, + &out_len, + in.data(), + in.size()) <= 0) { + return WebCryptoCipherStatus::ERR_FAILED; + } + + buf.Resize(out_len); + + *out = std::move(buf); + return WebCryptoCipherStatus::ERR_OK; +} +} // namespace + +Maybe RSAKeyExportTraits::AdditionalConfig( + const FunctionCallbackInfo& args, + unsigned int offset, + RSAKeyExportConfig* params) { + CHECK(args[offset]->IsUint32()); // RSAKeyVariant + params->variant = + static_cast(args[offset].As()->Value()); + return Just(true); +} + +WebCryptoKeyExportStatus RSAKeyExportTraits::DoExport( + std::shared_ptr key_data, + WebCryptoKeyFormat format, + const RSAKeyExportConfig& params, + ByteSource* out) { + CHECK_NE(key_data->GetKeyType(), kKeyTypeSecret); + + switch (format) { + case kWebCryptoKeyFormatRaw: + // Not supported for RSA keys of either type + return WebCryptoKeyExportStatus::ERR_FAILED; + case kWebCryptoKeyFormatJWK: + return RSA_JWK_Export(key_data.get(), params, out); + case kWebCryptoKeyFormatPKCS8: + if (key_data->GetKeyType() != kKeyTypePrivate) + return WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE; + return PKEY_PKCS8_Export(key_data.get(), out); + case kWebCryptoKeyFormatSPKI: + if (key_data->GetKeyType() != kKeyTypePublic) + return WebCryptoKeyExportStatus::ERR_INVALID_KEY_TYPE; + return PKEY_SPKI_Export(key_data.get(), out); + default: + UNREACHABLE(); + } +} + +RSACipherConfig::RSACipherConfig(RSACipherConfig&& other) noexcept + : mode(other.mode), + label(std::move(other.label)), + padding(other.padding), + digest(other.digest) {} + +void RSACipherConfig::MemoryInfo(MemoryTracker* tracker) const { + if (mode == kCryptoJobAsync) + tracker->TrackFieldWithSize("label", label.size()); +} + +Maybe RSACipherTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + WebCryptoCipherMode cipher_mode, + RSACipherConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->mode = mode; + params->padding = RSA_PKCS1_OAEP_PADDING; + + CHECK(args[offset]->IsUint32()); + RSAKeyVariant variant = + static_cast(args[offset].As()->Value()); + + switch (variant) { + case kKeyVariantRSA_OAEP: { + CHECK(args[offset + 1]->IsString()); // digest + Utf8Value digest(env->isolate(), args[offset + 1]); + + params->digest = EVP_get_digestbyname(*digest); + if (params->digest == nullptr) { + THROW_ERR_CRYPTO_INVALID_DIGEST(env); + return Nothing(); + } + + if (IsAnyByteSource(args[offset + 2])) { + ArrayBufferOrViewContents label(args[offset + 2]); + if (UNLIKELY(!label.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "label is too big"); + return Nothing(); + } + params->label = label.ToCopy(); + } + break; + } + default: + THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); + return Nothing(); + } + + return Just(true); +} + +WebCryptoCipherStatus RSACipherTraits::DoCipher( + Environment* env, + std::shared_ptr key_data, + WebCryptoCipherMode cipher_mode, + const RSACipherConfig& params, + const ByteSource& in, + ByteSource* out) { + switch (cipher_mode) { + case kWebCryptoCipherEncrypt: + CHECK_EQ(key_data->GetKeyType(), kKeyTypePublic); + return RSA_Cipher( + env, key_data.get(), params, in, out); + case kWebCryptoCipherDecrypt: + CHECK_EQ(key_data->GetKeyType(), kKeyTypePrivate); + return RSA_Cipher( + env, key_data.get(), params, in, out); + } + return WebCryptoCipherStatus::ERR_FAILED; +} + +Maybe ExportJWKRsaKey( + Environment* env, + std::shared_ptr key, + Local target) { + ManagedEVPPKey pkey = key->GetAsymmetricKey(); + int type = EVP_PKEY_id(pkey.get()); + CHECK(type == EVP_PKEY_RSA || type == EVP_PKEY_RSA_PSS); + + RSA* rsa = EVP_PKEY_get0_RSA(pkey.get()); + CHECK_NOT_NULL(rsa); + + const BIGNUM* n; + const BIGNUM* e; + const BIGNUM* d; + const BIGNUM* p; + const BIGNUM* q; + const BIGNUM* dp; + const BIGNUM* dq; + const BIGNUM* qi; + RSA_get0_key(rsa, &n, &e, &d); + + if (target->Set( + env->context(), + env->jwk_kty_string(), + env->jwk_rsa_string()).IsNothing()) { + return Nothing(); + } + + if (SetEncodedValue(env, target, env->jwk_n_string(), n).IsNothing() || + SetEncodedValue(env, target, env->jwk_e_string(), e).IsNothing()) { + return Nothing(); + } + + if (key->GetKeyType() == kKeyTypePrivate) { + RSA_get0_factors(rsa, &p, &q); + RSA_get0_crt_params(rsa, &dp, &dq, &qi); + if (SetEncodedValue(env, target, env->jwk_d_string(), d).IsNothing() || + SetEncodedValue(env, target, env->jwk_p_string(), p).IsNothing() || + SetEncodedValue(env, target, env->jwk_q_string(), q).IsNothing() || + SetEncodedValue(env, target, env->jwk_dp_string(), dp).IsNothing() || + SetEncodedValue(env, target, env->jwk_dq_string(), dq).IsNothing() || + SetEncodedValue(env, target, env->jwk_qi_string(), qi).IsNothing()) { + return Nothing(); + } + } + + return Just(true); +} + +std::shared_ptr ImportJWKRsaKey( + Environment* env, + Local jwk, + const FunctionCallbackInfo& args, + unsigned int offset) { + Local n_value; + Local e_value; + Local d_value; + + if (!jwk->Get(env->context(), env->jwk_n_string()).ToLocal(&n_value) || + !jwk->Get(env->context(), env->jwk_e_string()).ToLocal(&e_value) || + !jwk->Get(env->context(), env->jwk_d_string()).ToLocal(&d_value) || + !n_value->IsString() || + !e_value->IsString()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key"); + return std::shared_ptr(); + } + + if (!d_value->IsUndefined() && !d_value->IsString()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key"); + return std::shared_ptr(); + } + + KeyType type = d_value->IsString() ? kKeyTypePrivate : kKeyTypePublic; + + RsaPointer rsa(RSA_new()); + + ByteSource n = ByteSource::FromEncodedString(env, n_value.As()); + ByteSource e = ByteSource::FromEncodedString(env, e_value.As()); + + if (!RSA_set0_key( + rsa.get(), + n.ToBN().release(), + e.ToBN().release(), + nullptr)) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key"); + return std::shared_ptr(); + } + + if (type == kKeyTypePrivate) { + Local p_value; + Local q_value; + Local dp_value; + Local dq_value; + Local qi_value; + + if (!jwk->Get(env->context(), env->jwk_p_string()).ToLocal(&p_value) || + !jwk->Get(env->context(), env->jwk_q_string()).ToLocal(&q_value) || + !jwk->Get(env->context(), env->jwk_dp_string()).ToLocal(&dp_value) || + !jwk->Get(env->context(), env->jwk_dq_string()).ToLocal(&dq_value) || + !jwk->Get(env->context(), env->jwk_qi_string()).ToLocal(&qi_value)) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key"); + return std::shared_ptr(); + } + + if (!p_value->IsString() || + !q_value->IsString() || + !dp_value->IsString() || + !dq_value->IsString() || + !qi_value->IsString()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key"); + return std::shared_ptr(); + } + + ByteSource d = ByteSource::FromEncodedString(env, d_value.As()); + ByteSource q = ByteSource::FromEncodedString(env, q_value.As()); + ByteSource p = ByteSource::FromEncodedString(env, p_value.As()); + ByteSource dp = ByteSource::FromEncodedString(env, dp_value.As()); + ByteSource dq = ByteSource::FromEncodedString(env, dq_value.As()); + ByteSource qi = ByteSource::FromEncodedString(env, qi_value.As()); + + if (!RSA_set0_key(rsa.get(), nullptr, nullptr, d.ToBN().release()) || + !RSA_set0_factors(rsa.get(), p.ToBN().release(), q.ToBN().release()) || + !RSA_set0_crt_params( + rsa.get(), + dp.ToBN().release(), + dq.ToBN().release(), + qi.ToBN().release())) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key"); + return std::shared_ptr(); + } + } + + EVPKeyPointer pkey(EVP_PKEY_new()); + CHECK_EQ(EVP_PKEY_set1_RSA(pkey.get(), rsa.get()), 1); + + return KeyObjectData::CreateAsymmetric(type, ManagedEVPPKey(std::move(pkey))); +} + +Maybe GetRsaKeyDetail( + Environment* env, + std::shared_ptr key, + Local target) { + const BIGNUM* e; // Public Exponent + const BIGNUM* n; // Modulus + + ManagedEVPPKey pkey = key->GetAsymmetricKey(); + int type = EVP_PKEY_id(pkey.get()); + CHECK(type == EVP_PKEY_RSA || type == EVP_PKEY_RSA_PSS); + + RSA* rsa = EVP_PKEY_get0_RSA(pkey.get()); + CHECK_NOT_NULL(rsa); + + RSA_get0_key(rsa, &n, &e, nullptr); + + size_t modulus_length = BN_num_bytes(n) * CHAR_BIT; + + if (target->Set( + env->context(), + env->modulus_length_string(), + Number::New(env->isolate(), modulus_length)).IsNothing()) { + return Nothing(); + } + + int len = BN_num_bytes(e); + AllocatedBuffer public_exponent = AllocatedBuffer::AllocateManaged(env, len); + unsigned char* data = + reinterpret_cast(public_exponent.data()); + CHECK_EQ(BN_bn2binpad(e, data, len), len); + + return target->Set( + env->context(), + env->public_exponent_string(), + public_exponent.ToArrayBuffer()); +} + +namespace RSAAlg { +void Initialize(Environment* env, Local target) { + RSAKeyPairGenJob::Initialize(env, target); + RSAKeyExportJob::Initialize(env, target); + RSACipherJob::Initialize(env, target); + + NODE_DEFINE_CONSTANT(target, kKeyVariantRSA_SSA_PKCS1_V1_5); + NODE_DEFINE_CONSTANT(target, kKeyVariantRSA_PSS); + NODE_DEFINE_CONSTANT(target, kKeyVariantRSA_OAEP); +} +} // namespace RSAAlg +} // namespace crypto +} // namespace node + diff --git a/src/crypto/crypto_rsa.h b/src/crypto/crypto_rsa.h new file mode 100644 index 00000000000000..fc2a9130389347 --- /dev/null +++ b/src/crypto/crypto_rsa.h @@ -0,0 +1,140 @@ +#ifndef SRC_CRYPTO_CRYPTO_RSA_H_ +#define SRC_CRYPTO_CRYPTO_RSA_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_cipher.h" +#include "crypto/crypto_keygen.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +namespace node { +namespace crypto { +enum RSAKeyVariant { + kKeyVariantRSA_SSA_PKCS1_V1_5, + kKeyVariantRSA_PSS, + kKeyVariantRSA_OAEP +}; + +struct RsaKeyPairParams final : public MemoryRetainer { + RSAKeyVariant variant; + unsigned int modulus_bits; + unsigned int exponent; + + // The following used for RSA-PSS + const EVP_MD* md = nullptr; + const EVP_MD* mgf1_md = nullptr; + int saltlen = 0; + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(RsaKeyPairParams); + SET_SELF_SIZE(RsaKeyPairParams); +}; + +using RsaKeyPairGenConfig = KeyPairGenConfig; + +struct RsaKeyGenTraits final { + using AdditionalParameters = RsaKeyPairGenConfig; + static constexpr const char* JobName = "RsaKeyPairGenJob"; + + static EVPKeyCtxPointer Setup(RsaKeyPairGenConfig* params); + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int* offset, + RsaKeyPairGenConfig* params); +}; + +using RSAKeyPairGenJob = KeyGenJob>; + +struct RSAKeyExportConfig final : public MemoryRetainer { + RSAKeyVariant variant = kKeyVariantRSA_SSA_PKCS1_V1_5; + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(RSAKeyExportConfig) + SET_SELF_SIZE(RSAKeyExportConfig) +}; + +struct RSAKeyExportTraits final { + static constexpr const char* JobName = "RSAKeyExportJob"; + using AdditionalParameters = RSAKeyExportConfig; + + static v8::Maybe AdditionalConfig( + const v8::FunctionCallbackInfo& args, + unsigned int offset, + RSAKeyExportConfig* config); + + static WebCryptoKeyExportStatus DoExport( + std::shared_ptr key_data, + WebCryptoKeyFormat format, + const RSAKeyExportConfig& params, + ByteSource* out); +}; + +using RSAKeyExportJob = KeyExportJob; + +struct RSACipherConfig final : public MemoryRetainer { + CryptoJobMode mode; + ByteSource label; + int padding = 0; + const EVP_MD* digest = nullptr; + + RSACipherConfig() = default; + + RSACipherConfig(RSACipherConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(RSACipherConfig); + SET_SELF_SIZE(RSACipherConfig); +}; + +struct RSACipherTraits final { + static constexpr const char* JobName = "RSACipherJob"; + using AdditionalParameters = RSACipherConfig; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + WebCryptoCipherMode cipher_mode, + RSACipherConfig* config); + + static WebCryptoCipherStatus DoCipher( + Environment* env, + std::shared_ptr key_data, + WebCryptoCipherMode cipher_mode, + const RSACipherConfig& params, + const ByteSource& in, + ByteSource* out); +}; + +using RSACipherJob = CipherJob; + +v8::Maybe ExportJWKRsaKey( + Environment* env, + std::shared_ptr key, + v8::Local target); + +std::shared_ptr ImportJWKRsaKey( + Environment* env, + v8::Local jwk, + const v8::FunctionCallbackInfo& args, + unsigned int offset); + +v8::Maybe GetRsaKeyDetail( + Environment* env, + std::shared_ptr key, + v8::Local target); + +namespace RSAAlg { +void Initialize(Environment* env, v8::Local target); +} // namespace RSAAlg +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_RSA_H_ diff --git a/src/crypto/crypto_scrypt.cc b/src/crypto/crypto_scrypt.cc new file mode 100644 index 00000000000000..1af9853a6710e3 --- /dev/null +++ b/src/crypto/crypto_scrypt.cc @@ -0,0 +1,153 @@ +#include "crypto/crypto_scrypt.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_buffer.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Int32; +using v8::Just; +using v8::Maybe; +using v8::Nothing; +using v8::Uint32; +using v8::Value; + +namespace crypto { +#ifndef OPENSSL_NO_SCRYPT + +ScryptConfig::ScryptConfig(ScryptConfig&& other) noexcept + : mode(other.mode), + pass(std::move(other.pass)), + salt(std::move(other.salt)), + N(other.N), + r(other.r), + p(other.p), + length(other.length) {} + +ScryptConfig& ScryptConfig::operator=(ScryptConfig&& other) noexcept { + if (&other == this) return *this; + this->~ScryptConfig(); + return *new (this) ScryptConfig(std::move(other)); +} + +void ScryptConfig::MemoryInfo(MemoryTracker* tracker) const { + if (mode == kCryptoJobAsync) { + tracker->TrackFieldWithSize("pass", pass.size()); + tracker->TrackFieldWithSize("salt", salt.size()); + } +} + +Maybe ScryptTraits::EncodeOutput( + Environment* env, + const ScryptConfig& params, + ByteSource* out, + v8::Local* result) { + *result = out->ToArrayBuffer(env); + return Just(!result->IsEmpty()); +} + +Maybe ScryptTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + ScryptConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->mode = mode; + + ArrayBufferOrViewContents pass(args[offset]); + ArrayBufferOrViewContents salt(args[offset + 1]); + + if (UNLIKELY(!pass.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); + return Nothing(); + } + + if (UNLIKELY(!salt.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "salt is too large"); + return Nothing(); + } + + params->pass = mode == kCryptoJobAsync + ? pass.ToCopy() + : pass.ToByteSource(); + + params->salt = mode == kCryptoJobAsync + ? salt.ToCopy() + : salt.ToByteSource(); + + CHECK(args[offset + 2]->IsUint32()); // N + CHECK(args[offset + 3]->IsUint32()); // r + CHECK(args[offset + 4]->IsUint32()); // p + CHECK(args[offset + 5]->IsNumber()); // maxmem + CHECK(args[offset + 6]->IsInt32()); // length + + params->N = args[offset + 2].As()->Value(); + params->r = args[offset + 3].As()->Value(); + params->p = args[offset + 4].As()->Value(); + params->maxmem = args[offset + 5]->IntegerValue(env->context()).ToChecked(); + + if (EVP_PBE_scrypt( + nullptr, + 0, + nullptr, + 0, + params->N, + params->r, + params->p, + params->maxmem, + nullptr, + 0) != 1) { + THROW_ERR_CRYPTO_INVALID_SCRYPT_PARAMS(env); + return Nothing(); + } + + params->length = args[offset + 6].As()->Value(); + if (params->length < 0) { + char msg[1024]; + snprintf(msg, sizeof(msg), "length must be <= %d", INT_MAX); + THROW_ERR_OUT_OF_RANGE(env, msg); + return Nothing(); + } + + return Just(true); +} + +bool ScryptTraits::DeriveBits( + Environment* env, + const ScryptConfig& params, + ByteSource* out) { + char* data = MallocOpenSSL(params.length); + ByteSource buf = ByteSource::Allocated(data, params.length); + unsigned char* ptr = reinterpret_cast(data); + + // Botht the pass and salt may be zero-length at this point + + if (!EVP_PBE_scrypt( + params.pass.get(), + params.pass.size(), + params.salt.data(), + params.salt.size(), + params.N, + params.r, + params.p, + params.maxmem, + ptr, + params.length)) { + return false; + } + *out = std::move(buf); + return true; +} + +#endif // !OPENSSL_NO_SCRYPT + +} // namespace crypto +} // namespace node + diff --git a/src/crypto/crypto_scrypt.h b/src/crypto/crypto_scrypt.h new file mode 100644 index 00000000000000..b51d6c194ad67a --- /dev/null +++ b/src/crypto/crypto_scrypt.h @@ -0,0 +1,87 @@ +#ifndef SRC_CRYPTO_CRYPTO_SCRYPT_H_ +#define SRC_CRYPTO_CRYPTO_SCRYPT_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_util.h" +#include "env.h" +#include "memory_tracker.h" +#include "v8.h" + +namespace node { +namespace crypto { +#ifndef OPENSSL_NO_SCRYPT + +// Scrypt is a password-based key derivation algorithm +// defined in https://tools.ietf.org/html/rfc7914 + +// It takes as input a password, a salt value, and a +// handful of additional parameters that control the +// cost of the operation. In this case, the higher +// the cost, the better the result. The length parameter +// defines the number of bytes that are generated. + +// The salt must be as random as possible and should be +// at least 16 bytes in length. + +struct ScryptConfig final : public MemoryRetainer { + CryptoJobMode mode; + ByteSource pass; + ByteSource salt; + uint32_t N; + uint32_t r; + uint32_t p; + uint64_t maxmem; + int32_t length; + + ScryptConfig() = default; + + explicit ScryptConfig(ScryptConfig&& other) noexcept; + + ScryptConfig& operator=(ScryptConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(ScryptConfig); + SET_SELF_SIZE(ScryptConfig); +}; + +struct ScryptTraits final { + using AdditionalParameters = ScryptConfig; + static constexpr const char* JobName = "ScryptJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_SCRYPTREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + ScryptConfig* params); + + static bool DeriveBits( + Environment* env, + const ScryptConfig& params, + ByteSource* out); + + static v8::Maybe EncodeOutput( + Environment* env, + const ScryptConfig& params, + ByteSource* out, + v8::Local* result); +}; + +using ScryptJob = DeriveBitsJob; + +#else +// If there is no Scrypt support, ScryptJob becomes a non-op +struct ScryptJob { + static void Initialize( + Environment* env, + v8::Local target) {} +} +#endif // !OPENSSL_NO_SCRIPT + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_SCRYPT_H_ diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc new file mode 100644 index 00000000000000..3913843f778b4d --- /dev/null +++ b/src/crypto/crypto_sig.cc @@ -0,0 +1,881 @@ +#include "crypto/crypto_sig.h" +#include "crypto/crypto_ecdh.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Int32; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Object; +using v8::Uint32; +using v8::Value; + +namespace crypto { +namespace { +bool ValidateDSAParameters(EVP_PKEY* key) { +#ifdef NODE_FIPS_MODE + /* Validate DSA2 parameters from FIPS 186-4 */ + if (FIPS_mode() && EVP_PKEY_DSA == EVP_PKEY_base_id(key)) { + DSA* dsa = EVP_PKEY_get0_DSA(key); + const BIGNUM* p; + DSA_get0_pqg(dsa, &p, nullptr, nullptr); + size_t L = BN_num_bits(p); + const BIGNUM* q; + DSA_get0_pqg(dsa, nullptr, &q, nullptr); + size_t N = BN_num_bits(q); + + return (L == 1024 && N == 160) || + (L == 2048 && N == 224) || + (L == 2048 && N == 256) || + (L == 3072 && N == 256); + } +#endif // NODE_FIPS_MODE + + return true; +} + +bool ApplyRSAOptions(const ManagedEVPPKey& pkey, + EVP_PKEY_CTX* pkctx, + int padding, + const Maybe& salt_len) { + if (EVP_PKEY_id(pkey.get()) == EVP_PKEY_RSA || + EVP_PKEY_id(pkey.get()) == EVP_PKEY_RSA2 || + EVP_PKEY_id(pkey.get()) == EVP_PKEY_RSA_PSS) { + if (EVP_PKEY_CTX_set_rsa_padding(pkctx, padding) <= 0) + return false; + if (padding == RSA_PKCS1_PSS_PADDING && salt_len.IsJust()) { + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkctx, salt_len.FromJust()) <= 0) + return false; + } + } + + return true; +} + +AllocatedBuffer Node_SignFinal(Environment* env, + EVPMDPointer&& mdctx, + const ManagedEVPPKey& pkey, + int padding, + Maybe pss_salt_len) { + unsigned char m[EVP_MAX_MD_SIZE]; + unsigned int m_len; + + if (!EVP_DigestFinal_ex(mdctx.get(), m, &m_len)) + return AllocatedBuffer(); + + int signed_sig_len = EVP_PKEY_size(pkey.get()); + CHECK_GE(signed_sig_len, 0); + size_t sig_len = static_cast(signed_sig_len); + AllocatedBuffer sig = AllocatedBuffer::AllocateManaged(env, sig_len); + unsigned char* ptr = reinterpret_cast(sig.data()); + + EVPKeyCtxPointer pkctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); + if (pkctx && + EVP_PKEY_sign_init(pkctx.get()) && + ApplyRSAOptions(pkey, pkctx.get(), padding, pss_salt_len) && + EVP_PKEY_CTX_set_signature_md(pkctx.get(), EVP_MD_CTX_md(mdctx.get())) && + EVP_PKEY_sign(pkctx.get(), ptr, &sig_len, m, m_len)) { + sig.Resize(sig_len); + return sig; + } + + return AllocatedBuffer(); +} + +int GetDefaultSignPadding(const ManagedEVPPKey& key) { + return EVP_PKEY_id(key.get()) == EVP_PKEY_RSA_PSS ? RSA_PKCS1_PSS_PADDING : + RSA_PKCS1_PADDING; +} + +unsigned int GetBytesOfRS(const ManagedEVPPKey& pkey) { + int bits, base_id = EVP_PKEY_base_id(pkey.get()); + + if (base_id == EVP_PKEY_DSA) { + DSA* dsa_key = EVP_PKEY_get0_DSA(pkey.get()); + // Both r and s are computed mod q, so their width is limited by that of q. + bits = BN_num_bits(DSA_get0_q(dsa_key)); + } else if (base_id == EVP_PKEY_EC) { + EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(pkey.get()); + const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key); + bits = EC_GROUP_order_bits(ec_group); + } else { + return kNoDsaSignature; + } + + return (bits + 7) / 8; +} + +// Returns the maximum size of each of the integers (r, s) of the DSA signature. +AllocatedBuffer ConvertSignatureToP1363(Environment* env, + const ManagedEVPPKey& pkey, + AllocatedBuffer&& signature) { + unsigned int n = GetBytesOfRS(pkey); + if (n == kNoDsaSignature) + return std::move(signature); + + const unsigned char* sig_data = + reinterpret_cast(signature.data()); + + ECDSASigPointer asn1_sig(d2i_ECDSA_SIG(nullptr, &sig_data, signature.size())); + if (!asn1_sig) + return AllocatedBuffer(); + + AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, 2 * n); + unsigned char* data = reinterpret_cast(buf.data()); + + const BIGNUM* r = ECDSA_SIG_get0_r(asn1_sig.get()); + const BIGNUM* s = ECDSA_SIG_get0_s(asn1_sig.get()); + CHECK_EQ(n, static_cast(BN_bn2binpad(r, data, n))); + CHECK_EQ(n, static_cast(BN_bn2binpad(s, data + n, n))); + + return buf; +} + + +ByteSource ConvertSignatureToDER( + const ManagedEVPPKey& pkey, + const ArrayBufferOrViewContents& signature) { + unsigned int n = GetBytesOfRS(pkey); + if (n == kNoDsaSignature) + return signature.ToByteSource(); + + const unsigned char* sig_data = + reinterpret_cast(signature.data()); + + if (signature.size() != 2 * n) + return ByteSource(); + + ECDSASigPointer asn1_sig(ECDSA_SIG_new()); + CHECK(asn1_sig); + BIGNUM* r = BN_new(); + CHECK_NOT_NULL(r); + BIGNUM* s = BN_new(); + CHECK_NOT_NULL(s); + CHECK_EQ(r, BN_bin2bn(sig_data, n, r)); + CHECK_EQ(s, BN_bin2bn(sig_data + n, n, s)); + CHECK_EQ(1, ECDSA_SIG_set0(asn1_sig.get(), r, s)); + + unsigned char* data = nullptr; + int len = i2d_ECDSA_SIG(asn1_sig.get(), &data); + + if (len <= 0) + return ByteSource(); + + CHECK_NOT_NULL(data); + + return ByteSource::Allocated(reinterpret_cast(data), len); +} + +void CheckThrow(Environment* env, SignBase::Error error) { + HandleScope scope(env->isolate()); + + switch (error) { + case SignBase::Error::kSignUnknownDigest: + return THROW_ERR_CRYPTO_INVALID_DIGEST(env); + + case SignBase::Error::kSignNotInitialised: + return THROW_ERR_CRYPTO_INVALID_STATE(env, "Not initialised"); + + case SignBase::Error::kSignMalformedSignature: + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Malformed signature"); + + case SignBase::Error::kSignInit: + case SignBase::Error::kSignUpdate: + case SignBase::Error::kSignPrivateKey: + case SignBase::Error::kSignPublicKey: + { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + if (err) + return ThrowCryptoError(env, err); + switch (error) { + case SignBase::Error::kSignInit: + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "EVP_SignInit_ex failed"); + case SignBase::Error::kSignUpdate: + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "EVP_SignUpdate failed"); + case SignBase::Error::kSignPrivateKey: + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "PEM_read_bio_PrivateKey failed"); + case SignBase::Error::kSignPublicKey: + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "PEM_read_bio_PUBKEY failed"); + default: + ABORT(); + } + } + + case SignBase::Error::kSignOk: + return; + } +} +} // namespace + +SignBase::Error SignBase::Init(const char* sign_type) { + CHECK_NULL(mdctx_); + // Historically, "dss1" and "DSS1" were DSA aliases for SHA-1 + // exposed through the public API. + if (strcmp(sign_type, "dss1") == 0 || + strcmp(sign_type, "DSS1") == 0) { + sign_type = "SHA1"; + } + const EVP_MD* md = EVP_get_digestbyname(sign_type); + if (md == nullptr) + return kSignUnknownDigest; + + mdctx_.reset(EVP_MD_CTX_new()); + if (!mdctx_ || !EVP_DigestInit_ex(mdctx_.get(), md, nullptr)) { + mdctx_.reset(); + return kSignInit; + } + + return kSignOk; +} + +SignBase::Error SignBase::Update(const char* data, size_t len) { + if (mdctx_ == nullptr) + return kSignNotInitialised; + if (!EVP_DigestUpdate(mdctx_.get(), data, len)) + return kSignUpdate; + return kSignOk; +} + +SignBase::SignBase(Environment* env, Local wrap) + : BaseObject(env, wrap) {} + +void SignBase::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("mdctx", mdctx_ ? kSizeOf_EVP_MD_CTX : 0); +} + +Sign::Sign(Environment* env, Local wrap) : SignBase(env, wrap) { + MakeWeak(); +} + +void Sign::Initialize(Environment* env, Local target) { + Local t = env->NewFunctionTemplate(New); + + t->InstanceTemplate()->SetInternalFieldCount( + SignBase::kInternalFieldCount); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + + env->SetProtoMethod(t, "init", SignInit); + env->SetProtoMethod(t, "update", SignUpdate); + env->SetProtoMethod(t, "sign", SignFinal); + + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "Sign"), + t->GetFunction(env->context()).ToLocalChecked()).Check(); + + env->SetMethod(target, "signOneShot", Sign::SignSync); + + SignJob::Initialize(env, target); + + constexpr int kSignJobModeSign = SignConfiguration::kSign; + constexpr int kSignJobModeVerify = SignConfiguration::kVerify; + + NODE_DEFINE_CONSTANT(target, kSignJobModeSign); + NODE_DEFINE_CONSTANT(target, kSignJobModeVerify); + NODE_DEFINE_CONSTANT(target, RSA_PKCS1_PSS_PADDING); +} + +void Sign::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + new Sign(env, args.This()); +} + +void Sign::SignInit(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Sign* sign; + ASSIGN_OR_RETURN_UNWRAP(&sign, args.Holder()); + + const node::Utf8Value sign_type(args.GetIsolate(), args[0]); + crypto::CheckThrow(env, sign->Init(*sign_type)); +} + +void Sign::SignUpdate(const FunctionCallbackInfo& args) { + Decode(args, [](Sign* sign, const FunctionCallbackInfo& args, + const char* data, size_t size) { + Environment* env = Environment::GetCurrent(args); + if (UNLIKELY(size > INT_MAX)) + return THROW_ERR_OUT_OF_RANGE(env, "data is too long"); + Error err = sign->Update(data, size); + crypto::CheckThrow(sign->env(), err); + }); +} + +Sign::SignResult Sign::SignFinal( + const ManagedEVPPKey& pkey, + int padding, + const Maybe& salt_len, + DSASigEnc dsa_sig_enc) { + if (!mdctx_) + return SignResult(kSignNotInitialised); + + EVPMDPointer mdctx = std::move(mdctx_); + + if (!ValidateDSAParameters(pkey.get())) + return SignResult(kSignPrivateKey); + + AllocatedBuffer buffer = + Node_SignFinal(env(), std::move(mdctx), pkey, padding, salt_len); + Error error = buffer.data() == nullptr ? kSignPrivateKey : kSignOk; + if (error == kSignOk && dsa_sig_enc == kSigEncP1363) { + buffer = ConvertSignatureToP1363(env(), pkey, std::move(buffer)); + CHECK_NOT_NULL(buffer.data()); + } + return SignResult(error, std::move(buffer)); +} + +void Sign::SignFinal(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Sign* sign; + ASSIGN_OR_RETURN_UNWRAP(&sign, args.Holder()); + + ClearErrorOnReturn clear_error_on_return; + + unsigned int offset = 0; + ManagedEVPPKey key = ManagedEVPPKey::GetPrivateKeyFromJs(args, &offset, true); + if (!key) + return; + + int padding = GetDefaultSignPadding(key); + if (!args[offset]->IsUndefined()) { + CHECK(args[offset]->IsInt32()); + padding = args[offset].As()->Value(); + } + + Maybe salt_len = Nothing(); + if (!args[offset + 1]->IsUndefined()) { + CHECK(args[offset + 1]->IsInt32()); + salt_len = Just(args[offset + 1].As()->Value()); + } + + CHECK(args[offset + 2]->IsInt32()); + DSASigEnc dsa_sig_enc = + static_cast(args[offset + 2].As()->Value()); + + SignResult ret = sign->SignFinal( + key, + padding, + salt_len, + dsa_sig_enc); + + if (ret.error != kSignOk) + return crypto::CheckThrow(env, ret.error); + + args.GetReturnValue().Set(ret.signature.ToBuffer().ToLocalChecked()); +} + +Verify::Verify(Environment* env, Local wrap) + : SignBase(env, wrap) { + MakeWeak(); +} + +void Verify::Initialize(Environment* env, Local target) { + Local t = env->NewFunctionTemplate(New); + + t->InstanceTemplate()->SetInternalFieldCount( + SignBase::kInternalFieldCount); + t->Inherit(BaseObject::GetConstructorTemplate(env)); + + env->SetProtoMethod(t, "init", VerifyInit); + env->SetProtoMethod(t, "update", VerifyUpdate); + env->SetProtoMethod(t, "verify", VerifyFinal); + + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "Verify"), + t->GetFunction(env->context()).ToLocalChecked()).Check(); + + env->SetMethod(target, "verifyOneShot", Verify::VerifySync); +} + +void Verify::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + new Verify(env, args.This()); +} + +void Verify::VerifyInit(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Verify* verify; + ASSIGN_OR_RETURN_UNWRAP(&verify, args.Holder()); + + const node::Utf8Value verify_type(args.GetIsolate(), args[0]); + crypto::CheckThrow(env, verify->Init(*verify_type)); +} + +void Verify::VerifyUpdate(const FunctionCallbackInfo& args) { + Decode(args, [](Verify* verify, + const FunctionCallbackInfo& args, + const char* data, size_t size) { + Environment* env = Environment::GetCurrent(args); + if (UNLIKELY(size > INT_MAX)) + return THROW_ERR_OUT_OF_RANGE(env, "data is too long"); + Error err = verify->Update(data, size); + crypto::CheckThrow(verify->env(), err); + }); +} + +SignBase::Error Verify::VerifyFinal(const ManagedEVPPKey& pkey, + const ByteSource& sig, + int padding, + const Maybe& saltlen, + bool* verify_result) { + if (!mdctx_) + return kSignNotInitialised; + + unsigned char m[EVP_MAX_MD_SIZE]; + unsigned int m_len; + *verify_result = false; + EVPMDPointer mdctx = std::move(mdctx_); + + if (!EVP_DigestFinal_ex(mdctx.get(), m, &m_len)) + return kSignPublicKey; + + EVPKeyCtxPointer pkctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); + if (pkctx && + EVP_PKEY_verify_init(pkctx.get()) > 0 && + ApplyRSAOptions(pkey, pkctx.get(), padding, saltlen) && + EVP_PKEY_CTX_set_signature_md(pkctx.get(), + EVP_MD_CTX_md(mdctx.get())) > 0) { + const unsigned char* s = reinterpret_cast(sig.get()); + const int r = EVP_PKEY_verify(pkctx.get(), s, sig.size(), m, m_len); + *verify_result = r == 1; + } + + return kSignOk; +} + +void Verify::VerifyFinal(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + ClearErrorOnReturn clear_error_on_return; + + Verify* verify; + ASSIGN_OR_RETURN_UNWRAP(&verify, args.Holder()); + + unsigned int offset = 0; + ManagedEVPPKey pkey = + ManagedEVPPKey::GetPublicOrPrivateKeyFromJs(args, &offset); + if (!pkey) + return; + + ArrayBufferOrViewContents hbuf(args[offset]); + if (UNLIKELY(!hbuf.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "buffer is too big"); + + int padding = GetDefaultSignPadding(pkey); + if (!args[offset + 1]->IsUndefined()) { + CHECK(args[offset + 1]->IsInt32()); + padding = args[offset + 1].As()->Value(); + } + + Maybe salt_len = Nothing(); + if (!args[offset + 2]->IsUndefined()) { + CHECK(args[offset + 2]->IsInt32()); + salt_len = Just(args[offset + 2].As()->Value()); + } + + CHECK(args[offset + 3]->IsInt32()); + DSASigEnc dsa_sig_enc = + static_cast(args[offset + 3].As()->Value()); + + ByteSource signature = hbuf.ToByteSource(); + if (dsa_sig_enc == kSigEncP1363) { + signature = ConvertSignatureToDER(pkey, hbuf); + if (signature.get() == nullptr) + return crypto::CheckThrow(env, Error::kSignMalformedSignature); + } + + bool verify_result; + Error err = verify->VerifyFinal(pkey, signature, padding, + salt_len, &verify_result); + if (err != kSignOk) + return crypto::CheckThrow(env, err); + args.GetReturnValue().Set(verify_result); +} + +void Sign::SignSync(const FunctionCallbackInfo& args) { + ClearErrorOnReturn clear_error_on_return; + Environment* env = Environment::GetCurrent(args); + + unsigned int offset = 0; + ManagedEVPPKey key = ManagedEVPPKey::GetPrivateKeyFromJs(args, &offset, true); + if (!key) + return; + + if (!ValidateDSAParameters(key.get())) + return crypto::CheckThrow(env, SignBase::Error::kSignPrivateKey); + + ArrayBufferOrViewContents data(args[offset]); + if (UNLIKELY(!data.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + + const EVP_MD* md; + if (args[offset + 1]->IsNullOrUndefined()) { + md = nullptr; + } else { + const node::Utf8Value sign_type(args.GetIsolate(), args[offset + 1]); + md = EVP_get_digestbyname(*sign_type); + if (md == nullptr) + return crypto::CheckThrow(env, SignBase::Error::kSignUnknownDigest); + } + + int rsa_padding = GetDefaultSignPadding(key); + if (!args[offset + 2]->IsUndefined()) { + CHECK(args[offset + 2]->IsInt32()); + rsa_padding = args[offset + 2].As()->Value(); + } + + Maybe rsa_salt_len = Nothing(); + if (!args[offset + 3]->IsUndefined()) { + CHECK(args[offset + 3]->IsInt32()); + rsa_salt_len = Just(args[offset + 3].As()->Value()); + } + + CHECK(args[offset + 4]->IsInt32()); + DSASigEnc dsa_sig_enc = + static_cast(args[offset + 4].As()->Value()); + + EVP_PKEY_CTX* pkctx = nullptr; + EVPMDPointer mdctx(EVP_MD_CTX_new()); + + if (!mdctx || + !EVP_DigestSignInit(mdctx.get(), &pkctx, md, nullptr, key.get())) { + return crypto::CheckThrow(env, SignBase::Error::kSignInit); + } + + if (!ApplyRSAOptions(key, pkctx, rsa_padding, rsa_salt_len)) + return crypto::CheckThrow(env, SignBase::Error::kSignPrivateKey); + + const unsigned char* input = + reinterpret_cast(data.data()); + size_t sig_len; + if (!EVP_DigestSign(mdctx.get(), nullptr, &sig_len, input, data.size())) + return crypto::CheckThrow(env, SignBase::Error::kSignPrivateKey); + + AllocatedBuffer signature = AllocatedBuffer::AllocateManaged(env, sig_len); + if (!EVP_DigestSign(mdctx.get(), + reinterpret_cast(signature.data()), + &sig_len, + input, + data.size())) { + return crypto::CheckThrow(env, SignBase::Error::kSignPrivateKey); + } + + signature.Resize(sig_len); + + if (dsa_sig_enc == kSigEncP1363) { + signature = ConvertSignatureToP1363(env, key, std::move(signature)); + } + + args.GetReturnValue().Set(signature.ToBuffer().ToLocalChecked()); +} + +void Verify::VerifySync(const FunctionCallbackInfo& args) { + ClearErrorOnReturn clear_error_on_return; + Environment* env = Environment::GetCurrent(args); + + unsigned int offset = 0; + ManagedEVPPKey key = + ManagedEVPPKey::GetPublicOrPrivateKeyFromJs(args, &offset); + if (!key) + return; + + ArrayBufferOrViewContents sig(args[offset]); + ArrayBufferOrViewContents data(args[offset + 1]); + + if (UNLIKELY(!sig.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "sig is too big"); + if (UNLIKELY(!data.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + + const EVP_MD* md; + if (args[offset + 2]->IsNullOrUndefined()) { + md = nullptr; + } else { + const node::Utf8Value sign_type(args.GetIsolate(), args[offset + 2]); + md = EVP_get_digestbyname(*sign_type); + if (md == nullptr) + return crypto::CheckThrow(env, SignBase::Error::kSignUnknownDigest); + } + + int rsa_padding = GetDefaultSignPadding(key); + if (!args[offset + 3]->IsUndefined()) { + CHECK(args[offset + 3]->IsInt32()); + rsa_padding = args[offset + 3].As()->Value(); + } + + Maybe rsa_salt_len = Nothing(); + if (!args[offset + 4]->IsUndefined()) { + CHECK(args[offset + 4]->IsInt32()); + rsa_salt_len = Just(args[offset + 4].As()->Value()); + } + + CHECK(args[offset + 5]->IsInt32()); + DSASigEnc dsa_sig_enc = + static_cast(args[offset + 5].As()->Value()); + + EVP_PKEY_CTX* pkctx = nullptr; + EVPMDPointer mdctx(EVP_MD_CTX_new()); + if (!mdctx || + !EVP_DigestVerifyInit(mdctx.get(), &pkctx, md, nullptr, key.get())) { + return crypto::CheckThrow(env, SignBase::Error::kSignInit); + } + + if (!ApplyRSAOptions(key, pkctx, rsa_padding, rsa_salt_len)) + return crypto::CheckThrow(env, SignBase::Error::kSignPublicKey); + + ByteSource sig_bytes = ByteSource::Foreign(sig.data(), sig.size()); + if (dsa_sig_enc == kSigEncP1363) { + sig_bytes = ConvertSignatureToDER(key, sig); + if (!sig_bytes) + return crypto::CheckThrow(env, SignBase::Error::kSignMalformedSignature); + } + + bool verify_result; + const int r = EVP_DigestVerify( + mdctx.get(), + sig_bytes.data(), + sig_bytes.size(), + reinterpret_cast(data.data()), + data.size()); + switch (r) { + case 1: + verify_result = true; + break; + case 0: + verify_result = false; + break; + default: + return crypto::CheckThrow(env, SignBase::Error::kSignPublicKey); + } + + args.GetReturnValue().Set(verify_result); +} + +SignConfiguration::SignConfiguration(SignConfiguration&& other) noexcept + : job_mode(other.job_mode), + mode(other.mode), + key(std::move(other.key)), + data(std::move(other.data)), + signature(std::move(other.signature)), + digest(other.digest), + flags(other.flags), + padding(other.padding), + salt_length(other.salt_length) {} + +SignConfiguration& SignConfiguration::operator=( + SignConfiguration&& other) noexcept { + if (&other == this) return *this; + this->~SignConfiguration(); + return *new (this) SignConfiguration(std::move(other)); +} + +void SignConfiguration::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("key", key.get()); + if (job_mode == kCryptoJobAsync) { + tracker->TrackFieldWithSize("data", data.size()); + tracker->TrackFieldWithSize("signature", signature.size()); + } +} + +Maybe SignTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + SignConfiguration* params) { + Environment* env = Environment::GetCurrent(args); + + params->job_mode = mode; + + CHECK(args[offset]->IsUint32()); // Sign Mode + CHECK(args[offset + 1]->IsObject()); // Key + + params->mode = + static_cast(args[offset].As()->Value()); + + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset + 1], Nothing()); + params->key = key->Data(); + + ArrayBufferOrViewContents data(args[offset + 2]); + if (UNLIKELY(!data.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + return Nothing(); + } + params->data = mode == kCryptoJobAsync + ? data.ToCopy() + : data.ToByteSource(); + + if (args[offset + 3]->IsString()) { + Utf8Value digest(env->isolate(), args[offset + 3]); + params->digest = EVP_get_digestbyname(*digest); + if (params->digest == nullptr) { + THROW_ERR_CRYPTO_INVALID_DIGEST(env); + return Nothing(); + } + } + + if (args[offset + 4]->IsUint32()) { // Salt length + params->flags |= SignConfiguration::kHasSaltLength; + params->salt_length = args[offset + 4].As()->Value(); + } + if (args[offset + 5]->IsUint32()) { // Padding + params->flags |= SignConfiguration::kHasPadding; + params->padding = args[offset + 5].As()->Value(); + } + + if (params->mode == SignConfiguration::kVerify) { + ArrayBufferOrViewContents signature(args[offset + 6]); + if (UNLIKELY(!signature.CheckSizeInt32())) { + THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); + return Nothing(); + } + // If this is an EC key (assuming ECDSA) we need to convert the + // the signature from WebCrypto format into DER format... + if (EVP_PKEY_id(params->key->GetAsymmetricKey().get()) == EVP_PKEY_EC) { + params->signature = + ConvertFromWebCryptoSignature( + params->key->GetAsymmetricKey(), + signature.ToByteSource()); + } else { + params->signature = mode == kCryptoJobAsync + ? signature.ToCopy() + : signature.ToByteSource(); + } + } + + return Just(true); +} + +bool SignTraits::DeriveBits( + Environment* env, + const SignConfiguration& params, + ByteSource* out) { + EVPMDPointer context(EVP_MD_CTX_new()); + EVP_PKEY_CTX* ctx = nullptr; + + switch (params.mode) { + case SignConfiguration::kSign: + CHECK_EQ(params.key->GetKeyType(), kKeyTypePrivate); + if (!EVP_DigestSignInit( + context.get(), + &ctx, + params.digest, + nullptr, + params.key->GetAsymmetricKey().get())) { + return false; + } + break; + case SignConfiguration::kVerify: + CHECK_EQ(params.key->GetKeyType(), kKeyTypePublic); + if (!EVP_DigestVerifyInit( + context.get(), + &ctx, + params.digest, + nullptr, + params.key->GetAsymmetricKey().get())) { + return false; + } + break; + } + + int padding = params.flags & SignConfiguration::kHasPadding + ? params.padding + : GetDefaultSignPadding(params.key->GetAsymmetricKey()); + + Maybe salt_length = params.flags & SignConfiguration::kHasSaltLength + ? Just(params.salt_length) : Nothing(); + + if (!ApplyRSAOptions( + params.key->GetAsymmetricKey(), + ctx, + padding, + salt_length)) { + return false; + } + + switch (params.mode) { + case SignConfiguration::kSign: { + size_t len; + if (!EVP_DigestSignUpdate( + context.get(), + params.data.data(), + params.data.size()) || + !EVP_DigestSignFinal(context.get(), nullptr, &len)) { + return false; + } + char* data = MallocOpenSSL(len); + ByteSource buf = ByteSource::Allocated(data, len); + unsigned char* ptr = reinterpret_cast(data); + if (!EVP_DigestSignFinal(context.get(), ptr, &len)) + return false; + + // If this is an EC key (assuming ECDSA) we have to + // convert the signature in to the proper format. + if (EVP_PKEY_id(params.key->GetAsymmetricKey().get()) == EVP_PKEY_EC) { + *out = ConvertToWebCryptoSignature(params.key->GetAsymmetricKey(), buf); + } else { + buf.Resize(len); + *out = std::move(buf); + } + break; + } + case SignConfiguration::kVerify: { + char* data = MallocOpenSSL(1); + data[0] = 0; + *out = ByteSource::Allocated(data, 1); + if (!EVP_DigestVerifyUpdate( + context.get(), + params.data.data(), + params.data.size())) { + return false; + } + + if (EVP_DigestVerifyFinal( + context.get(), + params.signature.data(), + params.signature.size()) == 1) { + data[0] = 1; + } + } + } + + return true; +} + +Maybe SignTraits::EncodeOutput( + Environment* env, + const SignConfiguration& params, + ByteSource* out, + Local* result) { + switch (params.mode) { + case SignConfiguration::kSign: + *result = out->ToArrayBuffer(env); + break; + case SignConfiguration::kVerify: + *result = out->get()[0] == 1 + ? v8::True(env->isolate()) + : v8::False(env->isolate()); + break; + default: + UNREACHABLE(); + } + return Just(!result->IsEmpty()); +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_sig.h b/src/crypto/crypto_sig.h new file mode 100644 index 00000000000000..2713ca4ca6ba4f --- /dev/null +++ b/src/crypto/crypto_sig.h @@ -0,0 +1,165 @@ +#ifndef SRC_CRYPTO_CRYPTO_SIG_H_ +#define SRC_CRYPTO_CRYPTO_SIG_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" + +namespace node { +namespace crypto { +static const unsigned int kNoDsaSignature = static_cast(-1); + +enum DSASigEnc { + kSigEncDER, kSigEncP1363 +}; + +class SignBase : public BaseObject { + public: + typedef enum { + kSignOk, + kSignUnknownDigest, + kSignInit, + kSignNotInitialised, + kSignUpdate, + kSignPrivateKey, + kSignPublicKey, + kSignMalformedSignature + } Error; + + SignBase(Environment* env, v8::Local wrap); + + Error Init(const char* sign_type); + Error Update(const char* data, size_t len); + + // TODO(joyeecheung): track the memory used by OpenSSL types + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(SignBase) + SET_SELF_SIZE(SignBase) + + protected: + EVPMDPointer mdctx_; +}; + +class Sign : public SignBase { + public: + static void Initialize(Environment* env, v8::Local target); + + struct SignResult { + Error error; + AllocatedBuffer signature; + + explicit SignResult( + Error err, + AllocatedBuffer&& sig = AllocatedBuffer()) + : error(err), signature(std::move(sig)) {} + }; + + SignResult SignFinal( + const ManagedEVPPKey& pkey, + int padding, + const v8::Maybe& saltlen, + DSASigEnc dsa_sig_enc); + + static void SignSync(const v8::FunctionCallbackInfo& args); + + protected: + static void New(const v8::FunctionCallbackInfo& args); + static void SignInit(const v8::FunctionCallbackInfo& args); + static void SignUpdate(const v8::FunctionCallbackInfo& args); + static void SignFinal(const v8::FunctionCallbackInfo& args); + + Sign(Environment* env, v8::Local wrap); +}; + +class Verify : public SignBase { + public: + static void Initialize(Environment* env, v8::Local target); + + Error VerifyFinal(const ManagedEVPPKey& key, + const ByteSource& sig, + int padding, + const v8::Maybe& saltlen, + bool* verify_result); + + static void VerifySync(const v8::FunctionCallbackInfo& args); + + protected: + static void New(const v8::FunctionCallbackInfo& args); + static void VerifyInit(const v8::FunctionCallbackInfo& args); + static void VerifyUpdate(const v8::FunctionCallbackInfo& args); + static void VerifyFinal(const v8::FunctionCallbackInfo& args); + + Verify(Environment* env, v8::Local wrap); +}; + +struct SignConfiguration final : public MemoryRetainer { + enum Mode { + kSign, + kVerify + }; + enum Flags { + kHasNone = 0, + kHasSaltLength = 1, + kHasPadding = 2 + }; + + CryptoJobMode job_mode; + Mode mode; + std::shared_ptr key; + ByteSource data; + ByteSource signature; + const EVP_MD* digest = nullptr; + int flags = SignConfiguration::kHasNone; + int padding = 0; + int salt_length = 0; + + SignConfiguration() = default; + + explicit SignConfiguration(SignConfiguration&& other) noexcept; + + SignConfiguration& operator=(SignConfiguration&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(SignConfiguration); + SET_SELF_SIZE(SignConfiguration); +}; + +struct SignTraits final { + using AdditionalParameters = SignConfiguration; + static constexpr const char* JobName = "SignJob"; + +// TODO(@jasnell): Sign request vs. Verify request + + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_SIGNREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + SignConfiguration* params); + + static bool DeriveBits( + Environment* env, + const SignConfiguration& params, + ByteSource* out); + + static v8::Maybe EncodeOutput( + Environment* env, + const SignConfiguration& params, + ByteSource* out, + v8::Local* result); +}; + +using SignJob = DeriveBitsJob; + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_SIG_H_ diff --git a/src/crypto/crypto_spkac.cc b/src/crypto/crypto_spkac.cc new file mode 100644 index 00000000000000..5688f9d86eae86 --- /dev/null +++ b/src/crypto/crypto_spkac.cc @@ -0,0 +1,128 @@ +#include "crypto/crypto_spkac.h" +#include "crypto/crypto_common.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node.h" +#include "v8.h" + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace crypto { +namespace SPKAC { +bool VerifySpkac(const ArrayBufferOrViewContents& input) { + NetscapeSPKIPointer spki( + NETSCAPE_SPKI_b64_decode(input.data(), input.size())); + if (!spki) + return false; + + EVPKeyPointer pkey(X509_PUBKEY_get(spki->spkac->pubkey)); + if (!pkey) + return false; + + return NETSCAPE_SPKI_verify(spki.get(), pkey.get()) > 0; +} + +void VerifySpkac(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + ArrayBufferOrViewContents input(args[0]); + if (input.size() == 0) + return args.GetReturnValue().SetEmptyString(); + + if (UNLIKELY(!input.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "spkac is too large"); + + args.GetReturnValue().Set(VerifySpkac(input)); +} + +AllocatedBuffer ExportPublicKey(Environment* env, + const ArrayBufferOrViewContents& input, + size_t* size) { + BIOPointer bio(BIO_new(BIO_s_mem())); + if (!bio) return AllocatedBuffer(); + + NetscapeSPKIPointer spki( + NETSCAPE_SPKI_b64_decode(input.data(), input.size())); + if (!spki) return AllocatedBuffer(); + + EVPKeyPointer pkey(NETSCAPE_SPKI_get_pubkey(spki.get())); + if (!pkey) return AllocatedBuffer(); + + if (PEM_write_bio_PUBKEY(bio.get(), pkey.get()) <= 0) + return AllocatedBuffer(); + + BUF_MEM* ptr; + BIO_get_mem_ptr(bio.get(), &ptr); + + *size = ptr->length; + AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, *size); + memcpy(buf.data(), ptr->data, *size); + + return buf; +} + +void ExportPublicKey(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + ArrayBufferOrViewContents input(args[0]); + if (input.size() == 0) + return args.GetReturnValue().SetEmptyString(); + + if (UNLIKELY(!input.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "spkac is too large"); + + size_t pkey_size; + AllocatedBuffer pkey = ExportPublicKey(env, input, &pkey_size); + if (pkey.data() == nullptr) + return args.GetReturnValue().SetEmptyString(); + + args.GetReturnValue().Set(pkey.ToBuffer().ToLocalChecked()); +} + +OpenSSLBuffer ExportChallenge(const ArrayBufferOrViewContents& input) { + NetscapeSPKIPointer sp( + NETSCAPE_SPKI_b64_decode(input.data(), input.size())); + if (!sp) + return nullptr; + + unsigned char* buf = nullptr; + ASN1_STRING_to_UTF8(&buf, sp->spkac->challenge); + + return OpenSSLBuffer(reinterpret_cast(buf)); +} + +void ExportChallenge(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + ArrayBufferOrViewContents input(args[0]); + if (input.size() == 0) + return args.GetReturnValue().SetEmptyString(); + + if (UNLIKELY(!input.CheckSizeInt32())) + return THROW_ERR_OUT_OF_RANGE(env, "spkac is too large"); + + OpenSSLBuffer cert = ExportChallenge(input); + if (!cert) + return args.GetReturnValue().SetEmptyString(); + + Local outString = + Encode(env->isolate(), cert.get(), strlen(cert.get()), BUFFER); + + args.GetReturnValue().Set(outString); +} + +void Initialize(Environment* env, Local target) { + env->SetMethodNoSideEffect(target, "certVerifySpkac", VerifySpkac); + env->SetMethodNoSideEffect(target, "certExportPublicKey", ExportPublicKey); + env->SetMethodNoSideEffect(target, "certExportChallenge", ExportChallenge); +} + +} // namespace SPKAC +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_spkac.h b/src/crypto/crypto_spkac.h new file mode 100644 index 00000000000000..ed204c81337716 --- /dev/null +++ b/src/crypto/crypto_spkac.h @@ -0,0 +1,20 @@ +#ifndef SRC_CRYPTO_CRYPTO_SPKAC_H_ +#define SRC_CRYPTO_CRYPTO_SPKAC_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "env.h" +#include "v8.h" + +#include + +namespace node { +namespace crypto { +namespace SPKAC { +void Initialize(Environment* env, v8::Local); +} // namespace SPKAC +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_SPKAC_H_ diff --git a/src/crypto/crypto_ssl.cc b/src/crypto/crypto_ssl.cc new file mode 100644 index 00000000000000..8e361dd373a95c --- /dev/null +++ b/src/crypto/crypto_ssl.cc @@ -0,0 +1,860 @@ +#include "crypto/crypto_ssl.h" +#include "crypto/crypto_clienthello-inl.h" +#include "crypto/crypto_common.h" +#include "crypto/crypto_util.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_buffer.h" +#include "tls_wrap.h" +#include "v8.h" + +namespace node { + +using v8::Array; +using v8::ArrayBufferView; +using v8::Boolean; +using v8::Context; +using v8::Exception; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::MaybeLocal; +using v8::Object; +using v8::String; +using v8::Uint32; +using v8::Value; + +namespace crypto { +// Just to generate static methods +template void SSLWrap::AddMethods(Environment* env, + Local t); +template void SSLWrap::ConfigureSecureContext(SecureContext* sc); +template int SSLWrap::SetCACerts(SecureContext* sc); +template void SSLWrap::MemoryInfo(MemoryTracker* tracker) const; +template SSL_SESSION* SSLWrap::GetSessionCallback( + SSL* s, + const unsigned char* key, + int len, + int* copy); +template int SSLWrap::NewSessionCallback(SSL* s, + SSL_SESSION* sess); +template void SSLWrap::KeylogCallback(const SSL* s, + const char* line); +template void SSLWrap::OnClientHello( + void* arg, + const ClientHelloParser::ClientHello& hello); +template int SSLWrap::TLSExtStatusCallback(SSL* s, void* arg); +template void SSLWrap::DestroySSL(); +template int SSLWrap::SSLCertCallback(SSL* s, void* arg); +template void SSLWrap::WaitForCertCb(CertCb cb, void* arg); +template int SSLWrap::SelectALPNCallback( + SSL* s, + const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg); + +template +void SSLWrap::AddMethods(Environment* env, Local t) { + HandleScope scope(env->isolate()); + + env->SetProtoMethodNoSideEffect(t, "getPeerCertificate", GetPeerCertificate); + env->SetProtoMethodNoSideEffect(t, "getCertificate", GetCertificate); + env->SetProtoMethodNoSideEffect(t, "getFinished", GetFinished); + env->SetProtoMethodNoSideEffect(t, "getPeerFinished", GetPeerFinished); + env->SetProtoMethodNoSideEffect(t, "getSession", GetSession); + env->SetProtoMethod(t, "setSession", SetSession); + env->SetProtoMethod(t, "loadSession", LoadSession); + env->SetProtoMethodNoSideEffect(t, "isSessionReused", IsSessionReused); + env->SetProtoMethodNoSideEffect(t, "verifyError", VerifyError); + env->SetProtoMethodNoSideEffect(t, "getCipher", GetCipher); + env->SetProtoMethodNoSideEffect(t, "getSharedSigalgs", GetSharedSigalgs); + env->SetProtoMethodNoSideEffect( + t, "exportKeyingMaterial", ExportKeyingMaterial); + env->SetProtoMethod(t, "endParser", EndParser); + env->SetProtoMethod(t, "certCbDone", CertCbDone); + env->SetProtoMethod(t, "renegotiate", Renegotiate); + env->SetProtoMethodNoSideEffect(t, "getTLSTicket", GetTLSTicket); + env->SetProtoMethod(t, "newSessionDone", NewSessionDone); + env->SetProtoMethod(t, "setOCSPResponse", SetOCSPResponse); + env->SetProtoMethod(t, "requestOCSP", RequestOCSP); + env->SetProtoMethodNoSideEffect(t, "getEphemeralKeyInfo", + GetEphemeralKeyInfo); + env->SetProtoMethodNoSideEffect(t, "getProtocol", GetProtocol); + + env->SetProtoMethod(t, "setMaxSendFragment", SetMaxSendFragment); + + env->SetProtoMethodNoSideEffect(t, "getALPNNegotiatedProtocol", + GetALPNNegotiatedProto); + env->SetProtoMethod(t, "setALPNProtocols", SetALPNProtocols); +} + + +template +void SSLWrap::ConfigureSecureContext(SecureContext* sc) { + // OCSP stapling + SSL_CTX_set_tlsext_status_cb(sc->ctx_.get(), TLSExtStatusCallback); + SSL_CTX_set_tlsext_status_arg(sc->ctx_.get(), nullptr); +} + + +template +SSL_SESSION* SSLWrap::GetSessionCallback(SSL* s, + const unsigned char* key, + int len, + int* copy) { + Base* w = static_cast(SSL_get_app_data(s)); + + *copy = 0; + return w->next_sess_.release(); +} + +template +int SSLWrap::NewSessionCallback(SSL* s, SSL_SESSION* sess) { + Base* w = static_cast(SSL_get_app_data(s)); + Environment* env = w->ssl_env(); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + + if (!w->session_callbacks_) + return 0; + + // Check if session is small enough to be stored + int size = i2d_SSL_SESSION(sess, nullptr); + if (size > SecureContext::kMaxSessionSize) + return 0; + + // Serialize session + Local session = Buffer::New(env, size).ToLocalChecked(); + unsigned char* session_data = reinterpret_cast( + Buffer::Data(session)); + memset(session_data, 0, size); + i2d_SSL_SESSION(sess, &session_data); + + unsigned int session_id_length; + const unsigned char* session_id_data = SSL_SESSION_get_id(sess, + &session_id_length); + Local session_id = Buffer::Copy( + env, + reinterpret_cast(session_id_data), + session_id_length).ToLocalChecked(); + Local argv[] = { session_id, session }; + // On servers, we pause the handshake until callback of 'newSession', which + // calls NewSessionDoneCb(). On clients, there is no callback to wait for. + if (w->is_server()) + w->awaiting_new_session_ = true; + w->MakeCallback(env->onnewsession_string(), arraysize(argv), argv); + + return 0; +} + +template +void SSLWrap::KeylogCallback(const SSL* s, const char* line) { + Base* w = static_cast(SSL_get_app_data(s)); + Environment* env = w->ssl_env(); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + + const size_t size = strlen(line); + Local line_bf = Buffer::Copy(env, line, 1 + size).ToLocalChecked(); + char* data = Buffer::Data(line_bf); + data[size] = '\n'; + w->MakeCallback(env->onkeylog_string(), 1, &line_bf); +} + +template +void SSLWrap::OnClientHello(void* arg, + const ClientHelloParser::ClientHello& hello) { + Base* w = static_cast(arg); + Environment* env = w->ssl_env(); + HandleScope handle_scope(env->isolate()); + Local context = env->context(); + Context::Scope context_scope(context); + + Local hello_obj = Object::New(env->isolate()); + Local buff = Buffer::Copy( + env, + reinterpret_cast(hello.session_id()), + hello.session_size()).ToLocalChecked(); + hello_obj->Set(context, env->session_id_string(), buff).Check(); + if (hello.servername() == nullptr) { + hello_obj->Set(context, + env->servername_string(), + String::Empty(env->isolate())).Check(); + } else { + Local servername = OneByteString(env->isolate(), + hello.servername(), + hello.servername_size()); + hello_obj->Set(context, env->servername_string(), servername).Check(); + } + hello_obj->Set(context, + env->tls_ticket_string(), + Boolean::New(env->isolate(), hello.has_ticket())).Check(); + + Local argv[] = { hello_obj }; + w->MakeCallback(env->onclienthello_string(), arraysize(argv), argv); +} + +template +void SSLWrap::GetPeerCertificate( + const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = w->ssl_env(); + + bool abbreviated = args.Length() < 1 || !args[0]->IsTrue(); + + Local ret; + if (GetPeerCert(env, w->ssl_, abbreviated, w->is_server()).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +template +void SSLWrap::GetCertificate( + const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = w->ssl_env(); + + Local ret; + if (GetCert(env, w->ssl_).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +template +void SSLWrap::GetFinished(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + // We cannot just pass nullptr to SSL_get_finished() + // because it would further be propagated to memcpy(), + // where the standard requirements as described in ISO/IEC 9899:2011 + // sections 7.21.2.1, 7.21.1.2, and 7.1.4, would be violated. + // Thus, we use a dummy byte. + char dummy[1]; + size_t len = SSL_get_finished(w->ssl_.get(), dummy, sizeof dummy); + if (len == 0) + return; + + AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, len); + CHECK_EQ(len, SSL_get_finished(w->ssl_.get(), buf.data(), len)); + args.GetReturnValue().Set(buf.ToBuffer().ToLocalChecked()); +} + +template +void SSLWrap::GetPeerFinished(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + // We cannot just pass nullptr to SSL_get_peer_finished() + // because it would further be propagated to memcpy(), + // where the standard requirements as described in ISO/IEC 9899:2011 + // sections 7.21.2.1, 7.21.1.2, and 7.1.4, would be violated. + // Thus, we use a dummy byte. + char dummy[1]; + size_t len = SSL_get_peer_finished(w->ssl_.get(), dummy, sizeof dummy); + if (len == 0) + return; + + AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, len); + CHECK_EQ(len, SSL_get_peer_finished(w->ssl_.get(), buf.data(), len)); + args.GetReturnValue().Set(buf.ToBuffer().ToLocalChecked()); +} + +template +void SSLWrap::GetSession(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + SSL_SESSION* sess = SSL_get_session(w->ssl_.get()); + if (sess == nullptr) + return; + + int slen = i2d_SSL_SESSION(sess, nullptr); + if (slen <= 0) + return; // Invalid or malformed session. + + AllocatedBuffer sbuf = AllocatedBuffer::AllocateManaged(env, slen); + unsigned char* p = reinterpret_cast(sbuf.data()); + CHECK_LT(0, i2d_SSL_SESSION(sess, &p)); + args.GetReturnValue().Set(sbuf.ToBuffer().ToLocalChecked()); +} + +template +void SSLWrap::SetSession(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + if (args.Length() < 1) + return THROW_ERR_MISSING_ARGS(env, "Session argument is mandatory"); + + THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "Session"); + + SSLSessionPointer sess = GetTLSSession(args[0]); + if (sess == nullptr) + return; + + if (!SetTLSSession(w->ssl_, sess)) + return env->ThrowError("SSL_set_session error"); +} + +template +void SSLWrap::LoadSession(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + // TODO(@sam-github) check arg length and types in js, and CHECK in c++ + if (args.Length() >= 1 && Buffer::HasInstance(args[0])) { + ArrayBufferViewContents sbuf(args[0]); + + const unsigned char* p = sbuf.data(); + SSL_SESSION* sess = d2i_SSL_SESSION(nullptr, &p, sbuf.length()); + + // Setup next session and move hello to the BIO buffer + w->next_sess_.reset(sess); + } +} + +template +void SSLWrap::IsSessionReused(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + bool yes = SSL_session_reused(w->ssl_.get()); + args.GetReturnValue().Set(yes); +} + +template +void SSLWrap::EndParser(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + w->hello_parser_.End(); +} + +template +void SSLWrap::Renegotiate(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + ClearErrorOnReturn clear_error_on_return; + + if (SSL_renegotiate(w->ssl_.get()) != 1) { + return ThrowCryptoError(w->ssl_env(), ERR_get_error()); + } +} + +template +void SSLWrap::GetTLSTicket(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = w->ssl_env(); + + SSL_SESSION* sess = SSL_get_session(w->ssl_.get()); + if (sess == nullptr) + return; + + const unsigned char* ticket; + size_t length; + SSL_SESSION_get0_ticket(sess, &ticket, &length); + + if (ticket == nullptr) + return; + + Local buff = Buffer::Copy( + env, reinterpret_cast(ticket), length).ToLocalChecked(); + + args.GetReturnValue().Set(buff); +} + +template +void SSLWrap::NewSessionDone(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + w->awaiting_new_session_ = false; + w->NewSessionDoneCb(); +} + +template +void SSLWrap::SetOCSPResponse(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = w->env(); + + if (args.Length() < 1) + return THROW_ERR_MISSING_ARGS(env, "OCSP response argument is mandatory"); + + THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "OCSP response"); + + w->ocsp_response_.Reset(args.GetIsolate(), args[0].As()); +} + +template +void SSLWrap::RequestOCSP(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + SSL_set_tlsext_status_type(w->ssl_.get(), TLSEXT_STATUSTYPE_ocsp); +} + +template +void SSLWrap::GetEphemeralKeyInfo( + const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = Environment::GetCurrent(args); + + CHECK(w->ssl_); + + // tmp key is available on only client + if (w->is_server()) + return args.GetReturnValue().SetNull(); + + Local ret; + if (GetEphemeralKey(env, w->ssl_).ToLocal(&ret)) + args.GetReturnValue().Set(ret); + + // TODO(@sam-github) semver-major: else return ThrowCryptoError(env, + // ERR_get_error()) +} + +template +void SSLWrap::SetMaxSendFragment( + const FunctionCallbackInfo& args) { + CHECK(args.Length() >= 1 && args[0]->IsNumber()); + + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + int rv = SSL_set_max_send_fragment( + w->ssl_.get(), + args[0]->Int32Value(w->ssl_env()->context()).FromJust()); + args.GetReturnValue().Set(rv); +} + +template +void SSLWrap::VerifyError(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + // XXX(bnoordhuis) The UNABLE_TO_GET_ISSUER_CERT error when there is no + // peer certificate is questionable but it's compatible with what was + // here before. + long x509_verify_error = // NOLINT(runtime/int) + VerifyPeerCertificate(w->ssl_, X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT); + + if (x509_verify_error == X509_V_OK) + return args.GetReturnValue().SetNull(); + + const char* reason = X509_verify_cert_error_string(x509_verify_error); + const char* code = reason; + code = X509ErrorCode(x509_verify_error); + + Isolate* isolate = args.GetIsolate(); + Local reason_string = OneByteString(isolate, reason); + Local exception_value = Exception::Error(reason_string); + Local exception_object = + exception_value->ToObject(isolate->GetCurrentContext()).ToLocalChecked(); + exception_object->Set(w->env()->context(), w->env()->code_string(), + OneByteString(isolate, code)).Check(); + args.GetReturnValue().Set(exception_object); +} + +template +void SSLWrap::GetCipher(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = w->ssl_env(); + + const SSL_CIPHER* c = SSL_get_current_cipher(w->ssl_.get()); + if (c == nullptr) + return; + + Local ret; + if (GetCipherInfo(env, w->ssl_).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +template +void SSLWrap::GetSharedSigalgs(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = w->ssl_env(); + + SSL* ssl = w->ssl_.get(); + int nsig = SSL_get_shared_sigalgs(ssl, 0, nullptr, nullptr, nullptr, nullptr, + nullptr); + MaybeStackBuffer, 16> ret_arr(nsig); + + for (int i = 0; i < nsig; i++) { + int hash_nid; + int sign_nid; + std::string sig_with_md; + + SSL_get_shared_sigalgs(ssl, i, &sign_nid, &hash_nid, nullptr, nullptr, + nullptr); + + switch (sign_nid) { + case EVP_PKEY_RSA: + sig_with_md = "RSA+"; + break; + + case EVP_PKEY_RSA_PSS: + sig_with_md = "RSA-PSS+"; + break; + + case EVP_PKEY_DSA: + sig_with_md = "DSA+"; + break; + + case EVP_PKEY_EC: + sig_with_md = "ECDSA+"; + break; + + case NID_ED25519: + sig_with_md = "Ed25519+"; + break; + + case NID_ED448: + sig_with_md = "Ed448+"; + break; +#ifndef OPENSSL_NO_GOST + case NID_id_GostR3410_2001: + sig_with_md = "gost2001+"; + break; + + case NID_id_GostR3410_2012_256: + sig_with_md = "gost2012_256+"; + break; + + case NID_id_GostR3410_2012_512: + sig_with_md = "gost2012_512+"; + break; +#endif // !OPENSSL_NO_GOST + default: + const char* sn = OBJ_nid2sn(sign_nid); + + if (sn != nullptr) { + sig_with_md = std::string(sn) + "+"; + } else { + sig_with_md = "UNDEF+"; + } + break; + } + + const char* sn_hash = OBJ_nid2sn(hash_nid); + if (sn_hash != nullptr) { + sig_with_md += std::string(sn_hash); + } else { + sig_with_md += "UNDEF"; + } + ret_arr[i] = OneByteString(env->isolate(), sig_with_md.c_str()); + } + + args.GetReturnValue().Set( + Array::New(env->isolate(), ret_arr.out(), ret_arr.length())); +} + +template +void SSLWrap::ExportKeyingMaterial( + const FunctionCallbackInfo& args) { + CHECK(args[0]->IsInt32()); + CHECK(args[1]->IsString()); + + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = w->ssl_env(); + + uint32_t olen = args[0].As()->Value(); + node::Utf8Value label(env->isolate(), args[1]); + + AllocatedBuffer out = AllocatedBuffer::AllocateManaged(env, olen); + + ByteSource context; + bool use_context = !args[2]->IsUndefined(); + if (use_context) + context = ByteSource::FromBuffer(args[2]); + + if (SSL_export_keying_material(w->ssl_.get(), + reinterpret_cast(out.data()), + olen, + *label, + label.length(), + reinterpret_cast( + context.get()), + context.size(), + use_context) != 1) { + return ThrowCryptoError(env, ERR_get_error(), "SSL_export_keying_material"); + } + + args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); +} + +template +void SSLWrap::GetProtocol(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + const char* tls_version = SSL_get_version(w->ssl_.get()); + args.GetReturnValue().Set(OneByteString(args.GetIsolate(), tls_version)); +} + +template +int SSLWrap::SelectALPNCallback(SSL* s, + const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg) { + Base* w = static_cast(SSL_get_app_data(s)); + Environment* env = w->env(); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local alpn_buffer = + w->object()->GetPrivate( + env->context(), + env->alpn_buffer_private_symbol()).ToLocalChecked(); + ArrayBufferViewContents alpn_protos(alpn_buffer); + int status = SSL_select_next_proto(const_cast(out), outlen, + alpn_protos.data(), alpn_protos.length(), + in, inlen); + // According to 3.2. Protocol Selection of RFC7301, fatal + // no_application_protocol alert shall be sent but OpenSSL 1.0.2 does not + // support it yet. See + // https://rt.openssl.org/Ticket/Display.html?id=3463&user=guest&pass=guest + return status == OPENSSL_NPN_NEGOTIATED ? SSL_TLSEXT_ERR_OK + : SSL_TLSEXT_ERR_NOACK; +} + +template +void SSLWrap::GetALPNNegotiatedProto( + const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + const unsigned char* alpn_proto; + unsigned int alpn_proto_len; + + SSL_get0_alpn_selected(w->ssl_.get(), &alpn_proto, &alpn_proto_len); + + Local result; + if (alpn_proto_len == 0) { + result = False(args.GetIsolate()); + } else if (alpn_proto_len == sizeof("h2") - 1 && + 0 == memcmp(alpn_proto, "h2", sizeof("h2") - 1)) { + result = w->env()->h2_string(); + } else if (alpn_proto_len == sizeof("http/1.1") - 1 && + 0 == memcmp(alpn_proto, "http/1.1", sizeof("http/1.1") - 1)) { + result = w->env()->http_1_1_string(); + } else { + result = OneByteString(args.GetIsolate(), alpn_proto, alpn_proto_len); + } + + args.GetReturnValue().Set(result); +} + +template +void SSLWrap::SetALPNProtocols(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = w->env(); + if (args.Length() < 1 || !Buffer::HasInstance(args[0])) + return env->ThrowTypeError("Must give a Buffer as first argument"); + + if (w->is_client()) { + CHECK(SetALPN(w->ssl_, args[0])); + } else { + CHECK( + w->object()->SetPrivate( + env->context(), + env->alpn_buffer_private_symbol(), + args[0]).FromJust()); + // Server should select ALPN protocol from list of advertised by client + SSL_CTX_set_alpn_select_cb(SSL_get_SSL_CTX(w->ssl_.get()), + SelectALPNCallback, + nullptr); + } +} + +template +int SSLWrap::TLSExtStatusCallback(SSL* s, void* arg) { + Base* w = static_cast(SSL_get_app_data(s)); + Environment* env = w->env(); + HandleScope handle_scope(env->isolate()); + + if (w->is_client()) { + // Incoming response + Local arg; + MaybeLocal ret = GetSSLOCSPResponse(env, s, Null(env->isolate())); + if (ret.ToLocal(&arg)) + w->MakeCallback(env->onocspresponse_string(), 1, &arg); + + // No async acceptance is possible, so always return 1 to accept the + // response. The listener for 'OCSPResponse' event has no control over + // return value, but it can .destroy() the connection if the response is not + // acceptable. + return 1; + } else { + // Outgoing response + if (w->ocsp_response_.IsEmpty()) + return SSL_TLSEXT_ERR_NOACK; + + Local obj = PersistentToLocal::Default(env->isolate(), + w->ocsp_response_); + size_t len = obj->ByteLength(); + + // OpenSSL takes control of the pointer after accepting it + unsigned char* data = MallocOpenSSL(len); + obj->CopyContents(data, len); + + if (!SSL_set_tlsext_status_ocsp_resp(s, data, len)) + OPENSSL_free(data); + w->ocsp_response_.Reset(); + + return SSL_TLSEXT_ERR_OK; + } +} + +template +void SSLWrap::WaitForCertCb(CertCb cb, void* arg) { + cert_cb_ = cb; + cert_cb_arg_ = arg; +} + +template +int SSLWrap::SSLCertCallback(SSL* s, void* arg) { + Base* w = static_cast(SSL_get_app_data(s)); + + if (!w->is_server()) + return 1; + + if (!w->is_waiting_cert_cb()) + return 1; + + if (w->cert_cb_running_) + // Not an error. Suspend handshake with SSL_ERROR_WANT_X509_LOOKUP, and + // handshake will continue after certcb is done. + return -1; + + Environment* env = w->env(); + Local context = env->context(); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(context); + w->cert_cb_running_ = true; + + Local info = Object::New(env->isolate()); + + const char* servername = GetServerName(s); + if (servername == nullptr) { + info->Set(context, + env->servername_string(), + String::Empty(env->isolate())).Check(); + } else { + Local str = OneByteString(env->isolate(), servername, + strlen(servername)); + info->Set(context, env->servername_string(), str).Check(); + } + + const bool ocsp = (SSL_get_tlsext_status_type(s) == TLSEXT_STATUSTYPE_ocsp); + info->Set(context, env->ocsp_request_string(), + Boolean::New(env->isolate(), ocsp)).Check(); + + Local argv[] = { info }; + w->MakeCallback(env->oncertcb_string(), arraysize(argv), argv); + + if (!w->cert_cb_running_) + return 1; + + // Performing async action, wait... + return -1; +} + +template +void SSLWrap::CertCbDone(const FunctionCallbackInfo& args) { + Base* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + Environment* env = w->env(); + + CHECK(w->is_waiting_cert_cb() && w->cert_cb_running_); + + Local object = w->object(); + Local ctx = object->Get(env->context(), + env->sni_context_string()).ToLocalChecked(); + Local cons = env->secure_context_constructor_template(); + + if (cons->HasInstance(ctx)) { + SecureContext* sc = Unwrap(ctx.As()); + CHECK_NOT_NULL(sc); + // Store the SNI context for later use. + w->sni_context_ = BaseObjectPtr(sc); + + if (UseSNIContext(w->ssl_, w->sni_context_) && !w->SetCACerts(sc)) { + // Not clear why sometimes we throw error, and sometimes we call + // onerror(). Both cause .destroy(), but onerror does a bit more. + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + return ThrowCryptoError(env, err, "CertCbDone"); + } + } else if (ctx->IsObject()) { + // Failure: incorrect SNI context object + Local err = Exception::TypeError(env->sni_context_err_string()); + w->MakeCallback(env->onerror_string(), 1, &err); + return; + } + + CertCb cb; + void* arg; + + cb = w->cert_cb_; + arg = w->cert_cb_arg_; + + w->cert_cb_running_ = false; + w->cert_cb_ = nullptr; + w->cert_cb_arg_ = nullptr; + + cb(arg); +} + +template +void SSLWrap::DestroySSL() { + if (!ssl_) + return; + + env_->isolate()->AdjustAmountOfExternalAllocatedMemory(-kExternalSize); + ssl_.reset(); +} + +template +int SSLWrap::SetCACerts(SecureContext* sc) { + int err = SSL_set1_verify_cert_store(ssl_.get(), + SSL_CTX_get_cert_store(sc->ctx_.get())); + if (err != 1) + return err; + + STACK_OF(X509_NAME)* list = SSL_dup_CA_list( + SSL_CTX_get_client_CA_list(sc->ctx_.get())); + + // NOTE: `SSL_set_client_CA_list` takes the ownership of `list` + SSL_set_client_CA_list(ssl_.get(), list); + return 1; +} + +template +void SSLWrap::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("ocsp_response", ocsp_response_); + tracker->TrackField("sni_context", sni_context_); +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_ssl.h b/src/crypto/crypto_ssl.h new file mode 100644 index 00000000000000..a35b46f3381909 --- /dev/null +++ b/src/crypto/crypto_ssl.h @@ -0,0 +1,146 @@ +#ifndef SRC_CRYPTO_CRYPTO_SSL_H_ +#define SRC_CRYPTO_CRYPTO_SSL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_context.h" +#include "crypto/crypto_clienthello.h" +#include "crypto/crypto_util.h" +#include "env.h" +#include "v8.h" + +namespace node { +namespace crypto { +// SSLWrap implicitly depends on the inheriting class' handle having an +// internal pointer to the Base class. +template +class SSLWrap { + public: + enum Kind { + kClient, + kServer + }; + + SSLWrap(Environment* env, SecureContext* sc, Kind kind) + : env_(env), + kind_(kind), + next_sess_(nullptr), + session_callbacks_(false), + awaiting_new_session_(false), + cert_cb_(nullptr), + cert_cb_arg_(nullptr), + cert_cb_running_(false) { + ssl_.reset(SSL_new(sc->ctx_.get())); + CHECK(ssl_); + env_->isolate()->AdjustAmountOfExternalAllocatedMemory(kExternalSize); + } + + virtual ~SSLWrap() { + DestroySSL(); + } + + inline void enable_session_callbacks() { session_callbacks_ = true; } + inline bool is_server() const { return kind_ == kServer; } + inline bool is_client() const { return kind_ == kClient; } + inline bool is_awaiting_new_session() const { return awaiting_new_session_; } + inline bool is_waiting_cert_cb() const { return cert_cb_ != nullptr; } + + void MemoryInfo(MemoryTracker* tracker) const; + + protected: + typedef void (*CertCb)(void* arg); + + // OpenSSL structures are opaque. Estimate SSL memory size for OpenSSL 1.1.1b: + // SSL: 6224 + // SSL->SSL3_STATE: 1040 + // ...some buffers: 42 * 1024 + // NOTE: Actually it is much more than this + static const int64_t kExternalSize = 6224 + 1040 + 42 * 1024; + + static void ConfigureSecureContext(SecureContext* sc); + static void AddMethods(Environment* env, v8::Local t); + + static SSL_SESSION* GetSessionCallback(SSL* s, + const unsigned char* key, + int len, + int* copy); + static int NewSessionCallback(SSL* s, SSL_SESSION* sess); + static void KeylogCallback(const SSL* s, const char* line); + static void OnClientHello(void* arg, + const ClientHelloParser::ClientHello& hello); + + static void GetPeerCertificate( + const v8::FunctionCallbackInfo& args); + static void GetCertificate(const v8::FunctionCallbackInfo& args); + static void GetFinished(const v8::FunctionCallbackInfo& args); + static void GetPeerFinished(const v8::FunctionCallbackInfo& args); + static void GetSession(const v8::FunctionCallbackInfo& args); + static void SetSession(const v8::FunctionCallbackInfo& args); + static void LoadSession(const v8::FunctionCallbackInfo& args); + static void IsSessionReused(const v8::FunctionCallbackInfo& args); + static void VerifyError(const v8::FunctionCallbackInfo& args); + static void GetCipher(const v8::FunctionCallbackInfo& args); + static void GetSharedSigalgs(const v8::FunctionCallbackInfo& args); + static void ExportKeyingMaterial( + const v8::FunctionCallbackInfo& args); + static void EndParser(const v8::FunctionCallbackInfo& args); + static void CertCbDone(const v8::FunctionCallbackInfo& args); + static void Renegotiate(const v8::FunctionCallbackInfo& args); + static void GetTLSTicket(const v8::FunctionCallbackInfo& args); + static void NewSessionDone(const v8::FunctionCallbackInfo& args); + static void SetOCSPResponse(const v8::FunctionCallbackInfo& args); + static void RequestOCSP(const v8::FunctionCallbackInfo& args); + static void GetEphemeralKeyInfo( + const v8::FunctionCallbackInfo& args); + static void GetProtocol(const v8::FunctionCallbackInfo& args); + +#ifdef SSL_set_max_send_fragment + static void SetMaxSendFragment( + const v8::FunctionCallbackInfo& args); +#endif // SSL_set_max_send_fragment + + static void GetALPNNegotiatedProto( + const v8::FunctionCallbackInfo& args); + static void SetALPNProtocols(const v8::FunctionCallbackInfo& args); + static int SelectALPNCallback(SSL* s, + const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg); + static int TLSExtStatusCallback(SSL* s, void* arg); + static int SSLCertCallback(SSL* s, void* arg); + + void DestroySSL(); + void WaitForCertCb(CertCb cb, void* arg); + int SetCACerts(SecureContext* sc); + + inline Environment* ssl_env() const { + return env_; + } + + Environment* const env_; + Kind kind_; + SSLSessionPointer next_sess_; + SSLPointer ssl_; + bool session_callbacks_; + bool awaiting_new_session_; + + // SSL_set_cert_cb + CertCb cert_cb_; + void* cert_cb_arg_; + bool cert_cb_running_; + + ClientHelloParser hello_parser_; + + v8::Global ocsp_response_; + BaseObjectPtr sni_context_; + + friend class SecureContext; +}; + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_SSL_H_ diff --git a/src/crypto/crypto_timing.cc b/src/crypto/crypto_timing.cc new file mode 100644 index 00000000000000..0a9b167223b846 --- /dev/null +++ b/src/crypto/crypto_timing.cc @@ -0,0 +1,55 @@ +#include "crypto/crypto_timing.h" +#include "crypto/crypto_util.h" +#include "env-inl.h" +#include "node_errors.h" +#include "v8.h" +#include "node.h" + +#include + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace crypto { +namespace Timing { +void TimingSafeEqual(const FunctionCallbackInfo& args) { + // Moving the type checking into JS leads to test failures, most likely due + // to V8 inlining certain parts of the wrapper. Therefore, keep them in C++. + // Refs: https://github.com/nodejs/node/issues/34073. + Environment* env = Environment::GetCurrent(args); + if (!IsAnyByteSource(args[0])) { + THROW_ERR_INVALID_ARG_TYPE( + env, "The \"buf1\" argument must be an instance of " + "ArrayBuffer, Buffer, TypedArray, or DataView."); + return; + } + if (!IsAnyByteSource(args[1])) { + THROW_ERR_INVALID_ARG_TYPE( + env, "The \"buf2\" argument must be an instance of " + "ArrayBuffer, Buffer, TypedArray, or DataView."); + return; + } + + ArrayBufferOrViewContents buf1(args[0]); + ArrayBufferOrViewContents buf2(args[1]); + + if (buf1.size() != buf2.size()) { + THROW_ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH(env); + return; + } + + return args.GetReturnValue().Set( + CRYPTO_memcmp(buf1.data(), buf2.data(), buf1.size()) == 0); +} + +void Initialize(Environment* env, Local target) { + env->SetMethodNoSideEffect(target, "timingSafeEqual", TimingSafeEqual); +} +} // namespace Timing + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_timing.h b/src/crypto/crypto_timing.h new file mode 100644 index 00000000000000..a164650861978c --- /dev/null +++ b/src/crypto/crypto_timing.h @@ -0,0 +1,20 @@ +#ifndef SRC_CRYPTO_CRYPTO_TIMING_H_ +#define SRC_CRYPTO_CRYPTO_TIMING_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "env.h" +#include "v8.h" + +namespace node { +namespace crypto { +namespace Timing { +void Initialize(Environment* env, v8::Local target); + +} // namespace Timing +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_CRYPTO_CRYPTO_TIMING_H_ diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc new file mode 100644 index 00000000000000..c223c0b9fde008 --- /dev/null +++ b/src/crypto/crypto_util.cc @@ -0,0 +1,600 @@ +#include "crypto/crypto_util.h" +#include "crypto/crypto_bio.h" +#include "crypto/crypto_keys.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_buffer.h" +#include "node_options-inl.h" +#include "string_bytes.h" +#include "threadpoolwork-inl.h" +#include "util-inl.h" +#include "v8.h" + +#include "math.h" + +namespace node { + +using v8::ArrayBuffer; +using v8::BackingStore; +using v8::Context; +using v8::Exception; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::NewStringType; +using v8::Nothing; +using v8::Object; +using v8::String; +using v8::Value; + +namespace crypto { +int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx) { + // From https://www.openssl.org/docs/man1.1.1/man3/SSL_verify_cb: + // + // If VerifyCallback returns 1, the verification process is continued. If + // VerifyCallback always returns 1, the TLS/SSL handshake will not be + // terminated with respect to verification failures and the connection will + // be established. The calling process can however retrieve the error code + // of the last verification error using SSL_get_verify_result(3) or by + // maintaining its own error storage managed by VerifyCallback. + // + // Since we cannot perform I/O quickly enough with X509_STORE_CTX_ APIs in + // this callback, we ignore all preverify_ok errors and let the handshake + // continue. It is imperative that the user use Connection::VerifyError after + // the 'secure' callback has been made. + return 1; +} + +void CheckEntropy() { + for (;;) { + int status = RAND_status(); + CHECK_GE(status, 0); // Cannot fail. + if (status != 0) + break; + + // Give up, RAND_poll() not supported. + if (RAND_poll() == 0) + break; + } +} + +bool EntropySource(unsigned char* buffer, size_t length) { + // Ensure that OpenSSL's PRNG is properly seeded. + CheckEntropy(); + // RAND_bytes() can return 0 to indicate that the entropy data is not truly + // random. That's okay, it's still better than V8's stock source of entropy, + // which is /dev/urandom on UNIX platforms and the current time on Windows. + return RAND_bytes(buffer, length) != -1; +} + +int PasswordCallback(char* buf, int size, int rwflag, void* u) { + const char* passphrase = static_cast(u); + if (passphrase != nullptr) { + size_t buflen = static_cast(size); + size_t len = strlen(passphrase); + if (buflen < len) + return -1; + memcpy(buf, passphrase, len); + return len; + } + + return -1; +} + +// This callback is used to avoid the default passphrase callback in OpenSSL +// which will typically prompt for the passphrase. The prompting is designed +// for the OpenSSL CLI, but works poorly for Node.js because it involves +// synchronous interaction with the controlling terminal, something we never +// want, and use this function to avoid it. +int NoPasswordCallback(char* buf, int size, int rwflag, void* u) { + return 0; +} + +void InitCryptoOnce() { +#ifndef OPENSSL_IS_BORINGSSL + OPENSSL_INIT_SETTINGS* settings = OPENSSL_INIT_new(); + + // --openssl-config=... + if (!per_process::cli_options->openssl_config.empty()) { + const char* conf = per_process::cli_options->openssl_config.c_str(); + OPENSSL_INIT_set_config_filename(settings, conf); + } + + OPENSSL_init_ssl(0, settings); + OPENSSL_INIT_free(settings); + settings = nullptr; +#endif + +#ifdef NODE_FIPS_MODE + /* Override FIPS settings in cnf file, if needed. */ + unsigned long err = 0; // NOLINT(runtime/int) + if (per_process::cli_options->enable_fips_crypto || + per_process::cli_options->force_fips_crypto) { + if (0 == FIPS_mode() && !FIPS_mode_set(1)) { + err = ERR_get_error(); + } + } + if (0 != err) { + fprintf(stderr, + "openssl fips failed: %s\n", + ERR_error_string(err, nullptr)); + UNREACHABLE(); + } +#endif // NODE_FIPS_MODE + + // Turn off compression. Saves memory and protects against CRIME attacks. + // No-op with OPENSSL_NO_COMP builds of OpenSSL. + sk_SSL_COMP_zero(SSL_COMP_get_compression_methods()); + +#ifndef OPENSSL_NO_ENGINE + ERR_load_ENGINE_strings(); + ENGINE_load_builtin_engines(); +#endif // !OPENSSL_NO_ENGINE + + NodeBIO::GetMethod(); +} + +#ifdef NODE_FIPS_MODE +void GetFipsCrypto(const FunctionCallbackInfo& args) { + args.GetReturnValue().Set(FIPS_mode() ? 1 : 0); +} + +void SetFipsCrypto(const FunctionCallbackInfo& args) { + CHECK(!per_process::cli_options->force_fips_crypto); + Environment* env = Environment::GetCurrent(args); + bool enable = args[0]->BooleanValue(env->isolate()); + + if (enable == FIPS_mode()) + return; // No action needed. + + if (!FIPS_mode_set(enable)) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + return ThrowCryptoError(env, err); + } +} +#endif /* NODE_FIPS_MODE */ + +void CryptoErrorVector::Capture() { + clear(); + while (auto err = ERR_get_error()) { + char buf[256]; + ERR_error_string_n(err, buf, sizeof(buf)); + push_back(buf); + } + std::reverse(begin(), end()); +} + +MaybeLocal CryptoErrorVector::ToException( + Environment* env, + Local exception_string) const { + if (exception_string.IsEmpty()) { + CryptoErrorVector copy(*this); + if (copy.empty()) copy.push_back("no error"); // But possibly a bug... + // Use last element as the error message, everything else goes + // into the .opensslErrorStack property on the exception object. + auto exception_string = + String::NewFromUtf8(env->isolate(), copy.back().data(), + NewStringType::kNormal, copy.back().size()) + .ToLocalChecked(); + copy.pop_back(); + return copy.ToException(env, exception_string); + } + + Local exception_v = Exception::Error(exception_string); + CHECK(!exception_v.IsEmpty()); + + if (!empty()) { + CHECK(exception_v->IsObject()); + Local exception = exception_v.As(); + Maybe ok = exception->Set(env->context(), + env->openssl_error_stack(), + ToV8Value(env->context(), *this).ToLocalChecked()); + if (ok.IsNothing()) + return MaybeLocal(); + } + + return exception_v; +} + +ByteSource::ByteSource(ByteSource&& other) noexcept + : data_(other.data_), + allocated_data_(other.allocated_data_), + size_(other.size_) { + other.allocated_data_ = nullptr; +} + +ByteSource::~ByteSource() { + OPENSSL_clear_free(allocated_data_, size_); +} + +void ByteSource::reset() { + OPENSSL_clear_free(allocated_data_, size_); + data_ = nullptr; + size_ = 0; +} + +ByteSource& ByteSource::operator=(ByteSource&& other) noexcept { + if (&other != this) { + OPENSSL_clear_free(allocated_data_, size_); + data_ = other.data_; + allocated_data_ = other.allocated_data_; + other.allocated_data_ = nullptr; + size_ = other.size_; + } + return *this; +} + +std::unique_ptr ByteSource::ReleaseToBackingStore() { + CHECK_NOT_NULL(allocated_data_); + std::unique_ptr ptr = ArrayBuffer::NewBackingStore( + allocated_data_, + size(), + [](void* data, size_t length, void* deleter_data) { + OPENSSL_clear_free(deleter_data, length); + }, allocated_data_); + CHECK(ptr); + allocated_data_ = nullptr; + data_ = nullptr; + size_ = 0; + return ptr; +} + +Local ByteSource::ToArrayBuffer(Environment* env) { + std::unique_ptr store = ReleaseToBackingStore(); + return ArrayBuffer::New(env->isolate(), std::move(store)); +} + +const char* ByteSource::get() const { + return data_; +} + +size_t ByteSource::size() const { + return size_; +} + +ByteSource ByteSource::FromBIO(const BIOPointer& bio) { + CHECK(bio); + BUF_MEM* bptr; + BIO_get_mem_ptr(bio.get(), &bptr); + char* data = MallocOpenSSL(bptr->length); + memcpy(data, bptr->data, bptr->length); + return Allocated(data, bptr->length); +} + +ByteSource ByteSource::FromEncodedString(Environment* env, + Local key, + enum encoding enc) { + size_t length = 0; + size_t actual = 0; + char* data = nullptr; + + if (StringBytes::Size(env->isolate(), key, enc).To(&length) && length > 0) { + data = MallocOpenSSL(length); + actual = StringBytes::Write(env->isolate(), data, length, key, enc); + + CHECK(actual <= length); + + if (actual == 0) { + OPENSSL_clear_free(data, length); + data = nullptr; + } else if (actual < length) { + data = reinterpret_cast(OPENSSL_realloc(data, actual)); + } + } + + return Allocated(data, actual); +} + +ByteSource ByteSource::FromStringOrBuffer(Environment* env, + Local value) { + return IsAnyByteSource(value) ? FromBuffer(value) + : FromString(env, value.As()); +} + +ByteSource ByteSource::FromString(Environment* env, Local str, + bool ntc) { + CHECK(str->IsString()); + size_t size = str->Utf8Length(env->isolate()); + size_t alloc_size = ntc ? size + 1 : size; + char* data = MallocOpenSSL(alloc_size); + int opts = String::NO_OPTIONS; + if (!ntc) opts |= String::NO_NULL_TERMINATION; + str->WriteUtf8(env->isolate(), data, alloc_size, nullptr, opts); + return Allocated(data, size); +} + +ByteSource ByteSource::FromBuffer(Local buffer, bool ntc) { + ArrayBufferOrViewContents buf(buffer); + return ntc ? buf.ToNullTerminatedCopy() : buf.ToByteSource(); +} + +ByteSource ByteSource::FromSecretKeyBytes( + Environment* env, + Local value) { + // A key can be passed as a string, buffer or KeyObject with type 'secret'. + // If it is a string, we need to convert it to a buffer. We are not doing that + // in JS to avoid creating an unprotected copy on the heap. + return value->IsString() || IsAnyByteSource(value) ? + ByteSource::FromStringOrBuffer(env, value) : + ByteSource::FromSymmetricKeyObjectHandle(value); +} + +ByteSource ByteSource::NullTerminatedCopy(Environment* env, + Local value) { + return Buffer::HasInstance(value) ? FromBuffer(value, true) + : FromString(env, value.As(), true); +} + +ByteSource ByteSource::FromSymmetricKeyObjectHandle(Local handle) { + CHECK(handle->IsObject()); + KeyObjectHandle* key = Unwrap(handle.As()); + CHECK_NOT_NULL(key); + return Foreign(key->Data()->GetSymmetricKey(), + key->Data()->GetSymmetricKeySize()); +} + +ByteSource::ByteSource(const char* data, char* allocated_data, size_t size) + : data_(data), + allocated_data_(allocated_data), + size_(size) {} + +ByteSource ByteSource::Allocated(char* data, size_t size) { + return ByteSource(data, data, size); +} + +ByteSource ByteSource::Foreign(const char* data, size_t size) { + return ByteSource(data, nullptr, size); +} + +namespace error { +Maybe Decorate(Environment* env, Local obj, + unsigned long err) { // NOLINT(runtime/int) + if (err == 0) return Just(true); // No decoration necessary. + + const char* ls = ERR_lib_error_string(err); + const char* fs = ERR_func_error_string(err); + const char* rs = ERR_reason_error_string(err); + + Isolate* isolate = env->isolate(); + Local context = isolate->GetCurrentContext(); + + if (ls != nullptr) { + if (obj->Set(context, env->library_string(), + OneByteString(isolate, ls)).IsNothing()) { + return Nothing(); + } + } + if (fs != nullptr) { + if (obj->Set(context, env->function_string(), + OneByteString(isolate, fs)).IsNothing()) { + return Nothing(); + } + } + if (rs != nullptr) { + if (obj->Set(context, env->reason_string(), + OneByteString(isolate, rs)).IsNothing()) { + return Nothing(); + } + + // SSL has no API to recover the error name from the number, so we + // transform reason strings like "this error" to "ERR_SSL_THIS_ERROR", + // which ends up being close to the original error macro name. + std::string reason(rs); + + for (auto& c : reason) { + if (c == ' ') + c = '_'; + else + c = ToUpper(c); + } + +#define OSSL_ERROR_CODES_MAP(V) \ + V(SYS) \ + V(BN) \ + V(RSA) \ + V(DH) \ + V(EVP) \ + V(BUF) \ + V(OBJ) \ + V(PEM) \ + V(DSA) \ + V(X509) \ + V(ASN1) \ + V(CONF) \ + V(CRYPTO) \ + V(EC) \ + V(SSL) \ + V(BIO) \ + V(PKCS7) \ + V(X509V3) \ + V(PKCS12) \ + V(RAND) \ + V(DSO) \ + V(ENGINE) \ + V(OCSP) \ + V(UI) \ + V(COMP) \ + V(ECDSA) \ + V(ECDH) \ + V(OSSL_STORE) \ + V(FIPS) \ + V(CMS) \ + V(TS) \ + V(HMAC) \ + V(CT) \ + V(ASYNC) \ + V(KDF) \ + V(SM2) \ + V(USER) \ + +#define V(name) case ERR_LIB_##name: lib = #name "_"; break; + const char* lib = ""; + const char* prefix = "OSSL_"; + switch (ERR_GET_LIB(err)) { OSSL_ERROR_CODES_MAP(V) } +#undef V +#undef OSSL_ERROR_CODES_MAP + // Don't generate codes like "ERR_OSSL_SSL_". + if (lib && strcmp(lib, "SSL_") == 0) + prefix = ""; + + // All OpenSSL reason strings fit in a single 80-column macro definition, + // all prefix lengths are <= 10, and ERR_OSSL_ is 9, so 128 is more than + // sufficient. + char code[128]; + snprintf(code, sizeof(code), "ERR_%s%s%s", prefix, lib, reason.c_str()); + + if (obj->Set(env->isolate()->GetCurrentContext(), + env->code_string(), + OneByteString(env->isolate(), code)).IsNothing()) + return Nothing(); + } + + return Just(true); +} +} // namespace error + +void ThrowCryptoError(Environment* env, + unsigned long err, // NOLINT(runtime/int) + // Default, only used if there is no SSL `err` which can + // be used to create a long-style message string. + const char* message) { + char message_buffer[128] = {0}; + if (err != 0 || message == nullptr) { + ERR_error_string_n(err, message_buffer, sizeof(message_buffer)); + message = message_buffer; + } + HandleScope scope(env->isolate()); + Local exception_string = + String::NewFromUtf8(env->isolate(), message).ToLocalChecked(); + CryptoErrorVector errors; + errors.Capture(); + Local exception; + if (!errors.ToException(env, exception_string).ToLocal(&exception)) + return; + Local obj; + if (!exception->ToObject(env->context()).ToLocal(&obj)) + return; + if (error::Decorate(env, obj, err).IsNothing()) + return; + env->isolate()->ThrowException(exception); +} + +#ifndef OPENSSL_NO_ENGINE +EnginePointer LoadEngineById(const char* id, CryptoErrorVector* errors) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + EnginePointer engine(ENGINE_by_id(id)); + if (!engine) { + // Engine not found, try loading dynamically. + engine = EnginePointer(ENGINE_by_id("dynamic")); + if (engine) { + if (!ENGINE_ctrl_cmd_string(engine.get(), "SO_PATH", id, 0) || + !ENGINE_ctrl_cmd_string(engine.get(), "LOAD", nullptr, 0)) { + engine.reset(); + } + } + } + + if (!engine && errors != nullptr) { + if (ERR_get_error() != 0) { + errors->Capture(); + } else { + errors->push_back(std::string("Engine \"") + id + "\" was not found"); + } + } + + return engine; +} + +bool SetEngine(const char* id, uint32_t flags, CryptoErrorVector* errors) { + ClearErrorOnReturn clear_error_on_return; + EnginePointer engine = LoadEngineById(id, errors); + if (!engine) + return false; + + if (!ENGINE_set_default(engine.get(), flags)) { + if (errors != nullptr) + errors->Capture(); + return false; + } + + return true; +} + +void SetEngine(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.Length() >= 2 && args[0]->IsString()); + uint32_t flags; + if (!args[1]->Uint32Value(env->context()).To(&flags)) return; + + const node::Utf8Value engine_id(env->isolate(), args[0]); + + args.GetReturnValue().Set(SetEngine(*engine_id, flags)); +} +#endif // !OPENSSL_NO_ENGINE + +MaybeLocal EncodeBignum( + Environment* env, + const BIGNUM* bn, + int size, + Local* error) { + std::vector buf(size); + CHECK_EQ(BN_bn2binpad(bn, buf.data(), size), size); + return StringBytes::Encode( + env->isolate(), + reinterpret_cast(buf.data()), + buf.size(), + BASE64URL, + error); +} + +Maybe SetEncodedValue( + Environment* env, + Local target, + Local name, + const BIGNUM* bn, + int size) { + Local value; + Local error; + CHECK_NOT_NULL(bn); + if (size == 0) + size = BN_num_bytes(bn); + if (!EncodeBignum(env, bn, size, &error).ToLocal(&value)) { + if (!error.IsEmpty()) + env->isolate()->ThrowException(error); + return Nothing(); + } + return target->Set(env->context(), name, value); +} + +CryptoJobMode GetCryptoJobMode(v8::Local args) { + CHECK(args->IsUint32()); + uint32_t mode = args.As()->Value(); + CHECK_LE(mode, kCryptoJobSync); + return static_cast(mode); +} + +namespace Util { +void Initialize(Environment* env, Local target) { +#ifndef OPENSSL_NO_ENGINE + env->SetMethod(target, "setEngine", SetEngine); +#endif // !OPENSSL_NO_ENGINE + +#ifdef NODE_FIPS_MODE + env->SetMethodNoSideEffect(target, "getFipsCrypto", GetFipsCrypto); + env->SetMethod(target, "setFipsCrypto", SetFipsCrypto); +#endif + + NODE_DEFINE_CONSTANT(target, kCryptoJobAsync); + NODE_DEFINE_CONSTANT(target, kCryptoJobSync); +} +} // namespace Util + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h new file mode 100644 index 00000000000000..a8aa4a707f423a --- /dev/null +++ b/src/crypto/crypto_util.h @@ -0,0 +1,674 @@ +#ifndef SRC_CRYPTO_CRYPTO_UTIL_H_ +#define SRC_CRYPTO_CRYPTO_UTIL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "env.h" +#include "async_wrap.h" +#include "allocated_buffer.h" +#include "node_errors.h" +#include "node_internals.h" +#include "util.h" +#include "v8.h" +#include "string_bytes.h" + +#include +#include +#include +#include +#include +#include +#include +#ifndef OPENSSL_NO_ENGINE +# include +#endif // !OPENSSL_NO_ENGINE + +#include +#include +#include +#include +#include +#include + +namespace node { +namespace crypto { +// Currently known sizes of commonly used OpenSSL struct sizes. +// OpenSSL considers it's various structs to be opaque and the +// sizes may change from one version of OpenSSL to another, so +// these values should not be trusted to remain static. These +// are provided to allow for some close to reasonable memory +// tracking. +constexpr size_t kSizeOf_DH = 144; +constexpr size_t kSizeOf_EC_KEY = 80; +constexpr size_t kSizeOf_EVP_CIPHER_CTX = 168; +constexpr size_t kSizeOf_EVP_MD_CTX = 48; +constexpr size_t kSizeOf_EVP_PKEY = 72; +constexpr size_t kSizeOf_EVP_PKEY_CTX = 80; +constexpr size_t kSizeOf_HMAC_CTX = 32; + +// Define smart pointers for the most commonly used OpenSSL types: +using X509Pointer = DeleteFnPtr; +using BIOPointer = DeleteFnPtr; +using SSLCtxPointer = DeleteFnPtr; +using SSLSessionPointer = DeleteFnPtr; +using SSLPointer = DeleteFnPtr; +using PKCS8Pointer = DeleteFnPtr; +using EVPKeyPointer = DeleteFnPtr; +using EVPKeyCtxPointer = DeleteFnPtr; +using EVPMDPointer = DeleteFnPtr; +using RSAPointer = DeleteFnPtr; +using ECPointer = DeleteFnPtr; +using BignumPointer = DeleteFnPtr; +using NetscapeSPKIPointer = DeleteFnPtr; +using ECGroupPointer = DeleteFnPtr; +using ECPointPointer = DeleteFnPtr; +using ECKeyPointer = DeleteFnPtr; +using DHPointer = DeleteFnPtr; +using ECDSASigPointer = DeleteFnPtr; +using HMACCtxPointer = DeleteFnPtr; +using CipherCtxPointer = DeleteFnPtr; +using RsaPointer = DeleteFnPtr; +using DsaPointer = DeleteFnPtr; +using EcdsaSigPointer = DeleteFnPtr; + +// Our custom implementation of the certificate verify callback +// used when establishing a TLS handshake. Because we cannot perform +// I/O quickly enough with X509_STORE_CTX_ APIs in this callback, +// we ignore preverify_ok errors here and let the handshake continue. +// In other words, this VerifyCallback is a non-op. It is imperative +// that the user user Connection::VerifyError after the `secure` +// callback has been made. +extern int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx); + +void InitCryptoOnce(); + +void InitCrypto(v8::Local target); + +extern void UseExtraCaCerts(const std::string& file); + +// Forcibly clear OpenSSL's error stack on return. This stops stale errors +// from popping up later in the lifecycle of crypto operations where they +// would cause spurious failures. It's a rather blunt method, though. +// ERR_clear_error() isn't necessarily cheap either. +struct ClearErrorOnReturn { + ~ClearErrorOnReturn() { ERR_clear_error(); } +}; + +// Pop errors from OpenSSL's error stack that were added +// between when this was constructed and destructed. +struct MarkPopErrorOnReturn { + MarkPopErrorOnReturn() { ERR_set_mark(); } + ~MarkPopErrorOnReturn() { ERR_pop_to_mark(); } +}; + +// Ensure that OpenSSL has enough entropy (at least 256 bits) for its PRNG. +// The entropy pool starts out empty and needs to fill up before the PRNG +// can be used securely. Once the pool is filled, it never dries up again; +// its contents is stirred and reused when necessary. +// +// OpenSSL normally fills the pool automatically but not when someone starts +// generating random numbers before the pool is full: in that case OpenSSL +// keeps lowering the entropy estimate to thwart attackers trying to guess +// the initial state of the PRNG. +// +// When that happens, we will have to wait until enough entropy is available. +// That should normally never take longer than a few milliseconds. +// +// OpenSSL draws from /dev/random and /dev/urandom. While /dev/random may +// block pending "true" randomness, /dev/urandom is a CSPRNG that doesn't +// block under normal circumstances. +// +// The only time when /dev/urandom may conceivably block is right after boot, +// when the whole system is still low on entropy. That's not something we can +// do anything about. +void CheckEntropy(); + +// Generate length bytes of random data. If this returns false, the data +// may not be truly random but it's still generally good enough. +bool EntropySource(unsigned char* buffer, size_t length); + +int PasswordCallback(char* buf, int size, int rwflag, void* u); + +int NoPasswordCallback(char* buf, int size, int rwflag, void* u); + +// Decode is used by the various stream-based crypto utilities to decode +// string input. +template +void Decode(const v8::FunctionCallbackInfo& args, + void (*callback)(T*, const v8::FunctionCallbackInfo&, + const char*, size_t)) { + T* ctx; + ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); + + if (args[0]->IsString()) { + StringBytes::InlineDecoder decoder; + Environment* env = Environment::GetCurrent(args); + enum encoding enc = ParseEncoding(env->isolate(), args[1], UTF8); + if (decoder.Decode(env, args[0].As(), enc).IsNothing()) + return; + callback(ctx, args, decoder.out(), decoder.size()); + } else { + ArrayBufferViewContents buf(args[0]); + callback(ctx, args, buf.data(), buf.length()); + } +} + +// Utility struct used to harvest error information from openssl's error stack +struct CryptoErrorVector : public std::vector { + void Capture(); + + v8::MaybeLocal ToException( + Environment* env, + v8::Local exception_string = v8::Local()) const; +}; + +template +T* MallocOpenSSL(size_t count) { + void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); + CHECK_IMPLIES(mem == nullptr, count == 0); + return static_cast(mem); +} + +template +T* ReallocOpenSSL(T* buf, size_t count) { + void* mem = OPENSSL_realloc(buf, MultiplyWithOverflowCheck(count, sizeof(T))); + CHECK_IMPLIES(mem == nullptr, count == 0); + return static_cast(mem); +} + +// A helper class representing a read-only byte array. When deallocated, its +// contents are zeroed. +class ByteSource { + public: + ByteSource() = default; + ByteSource(ByteSource&& other) noexcept; + ~ByteSource(); + + ByteSource& operator=(ByteSource&& other) noexcept; + + const char* get() const; + + template + const T* data() const { return reinterpret_cast(get()); } + + size_t size() const; + + operator bool() const { return data_ != nullptr; } + + BignumPointer ToBN() const { + return BignumPointer(BN_bin2bn( + reinterpret_cast(get()), + size(), + nullptr)); + } + + // Creates a v8::BackingStore that takes over responsibility for + // any allocated data. The ByteSource will be reset with size = 0 + // after being called. + std::unique_ptr ReleaseToBackingStore(); + + v8::Local ToArrayBuffer(Environment* env); + + void reset(); + + // Allows an Allocated ByteSource to be truncated. + void Resize(size_t newsize) { + CHECK_LE(newsize, size_); + CHECK_NOT_NULL(allocated_data_); + char* new_data_ = ReallocOpenSSL(allocated_data_, newsize); + data_ = allocated_data_ = new_data_; + size_ = newsize; + } + + static ByteSource Allocated(char* data, size_t size); + static ByteSource Foreign(const char* data, size_t size); + + static ByteSource FromEncodedString(Environment* env, + v8::Local value, + enum encoding enc = BASE64); + + static ByteSource FromStringOrBuffer(Environment* env, + v8::Local value); + + static ByteSource FromString(Environment* env, + v8::Local str, + bool ntc = false); + + static ByteSource FromBuffer(v8::Local buffer, + bool ntc = false); + + static ByteSource FromBIO(const BIOPointer& bio); + + static ByteSource NullTerminatedCopy(Environment* env, + v8::Local value); + + static ByteSource FromSymmetricKeyObjectHandle(v8::Local handle); + + ByteSource(const ByteSource&) = delete; + ByteSource& operator=(const ByteSource&) = delete; + + static ByteSource FromSecretKeyBytes( + Environment* env, v8::Local value); + + private: + const char* data_ = nullptr; + char* allocated_data_ = nullptr; + size_t size_ = 0; + + ByteSource(const char* data, char* allocated_data, size_t size); +}; + +enum CryptoJobMode { + kCryptoJobAsync, + kCryptoJobSync +}; + +CryptoJobMode GetCryptoJobMode(v8::Local args); + +template +class CryptoJob : public AsyncWrap, public ThreadPoolWork { + public: + using AdditionalParams = typename CryptoJobTraits::AdditionalParameters; + + explicit CryptoJob( + Environment* env, + v8::Local object, + AsyncWrap::ProviderType type, + CryptoJobMode mode, + AdditionalParams&& params) + : AsyncWrap(env, object, type), + ThreadPoolWork(env), + mode_(mode), + params_(std::move(params)) { + // If the CryptoJob is async, then the instance will be + // cleaned up when AfterThreadPoolWork is called. + if (mode == kCryptoJobSync) MakeWeak(); + } + + bool IsNotIndicativeOfMemoryLeakAtExit() const override { + // CryptoJobs run a work in the libuv thread pool and may still + // exist when the event loop empties and starts to exit. + return true; + } + + void AfterThreadPoolWork(int status) override { + Environment* env = AsyncWrap::env(); + CHECK_EQ(mode_, kCryptoJobAsync); + CHECK(status == 0 || status == UV_ECANCELED); + std::unique_ptr ptr(this); + // If the job was canceled do not execute the callback. + // TODO(@jasnell): We should likely revisit skipping the + // callback on cancel as that could leave the JS in a pending + // state (e.g. unresolved promises...) + if (status == UV_ECANCELED) return; + v8::HandleScope handle_scope(env->isolate()); + v8::Context::Scope context_scope(env->context()); + v8::Local args[2]; + if (ptr->ToResult(&args[0], &args[1]).FromJust()) + ptr->MakeCallback(env->ondone_string(), arraysize(args), args); + } + + virtual v8::Maybe ToResult( + v8::Local* err, + v8::Local* result) = 0; + + CryptoJobMode mode() const { return mode_; } + + CryptoErrorVector* errors() { return &errors_; } + + AdditionalParams* params() { return ¶ms_; } + + std::string MemoryInfoName() const override { + return CryptoJobTraits::JobName; + } + + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackField("params", params_); + tracker->TrackField("errors", errors_); + } + + static void Run(const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CryptoJob* job; + ASSIGN_OR_RETURN_UNWRAP(&job, args.Holder()); + if (job->mode() == kCryptoJobAsync) + return job->ScheduleWork(); + + v8::Local ret[2]; + env->PrintSyncTrace(); + job->DoThreadPoolWork(); + if (job->ToResult(&ret[0], &ret[1]).FromJust()) { + args.GetReturnValue().Set( + v8::Array::New(env->isolate(), ret, arraysize(ret))); + } + } + + static void Initialize( + v8::FunctionCallback new_fn, + Environment* env, + v8::Local target) { + v8::Local job = env->NewFunctionTemplate(new_fn); + v8::Local class_name = + OneByteString(env->isolate(), CryptoJobTraits::JobName); + job->SetClassName(class_name); + job->Inherit(AsyncWrap::GetConstructorTemplate(env)); + job->InstanceTemplate()->SetInternalFieldCount( + AsyncWrap::kInternalFieldCount); + env->SetProtoMethod(job, "run", Run); + target->Set( + env->context(), + class_name, + job->GetFunction(env->context()).ToLocalChecked()).Check(); + } + + private: + const CryptoJobMode mode_; + CryptoErrorVector errors_; + AdditionalParams params_; +}; + +template +class DeriveBitsJob final : public CryptoJob { + public: + using AdditionalParams = typename DeriveBitsTraits::AdditionalParameters; + + static void New(const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CryptoJobMode mode = GetCryptoJobMode(args[0]); + + AdditionalParams params; + if (DeriveBitsTraits::AdditionalConfig(mode, args, 1, ¶ms) + .IsNothing()) { + // The DeriveBitsTraits::AdditionalConfig is responsible for + // calling an appropriate THROW_CRYPTO_* variant reporting + // whatever error caused initialization to fail. + return; + } + + new DeriveBitsJob(env, args.This(), mode, std::move(params)); + } + + static void Initialize( + Environment* env, + v8::Local target) { + CryptoJob::Initialize(New, env, target); + } + + DeriveBitsJob( + Environment* env, + v8::Local object, + CryptoJobMode mode, + AdditionalParams&& params) + : CryptoJob( + env, + object, + DeriveBitsTraits::Provider, + mode, + std::move(params)) {} + + void DoThreadPoolWork() override { + if (!DeriveBitsTraits::DeriveBits( + AsyncWrap::env(), + *CryptoJob::params(), &out_)) { + CryptoErrorVector* errors = CryptoJob::errors(); + errors->Capture(); + if (errors->empty()) + errors->push_back("Deriving bits failed"); + return; + } + success_ = true; + } + + v8::Maybe ToResult( + v8::Local* err, + v8::Local* result) override { + Environment* env = AsyncWrap::env(); + CryptoErrorVector* errors = CryptoJob::errors(); + if (success_) { + CHECK(errors->empty()); + *err = v8::Undefined(env->isolate()); + return DeriveBitsTraits::EncodeOutput( + env, + *CryptoJob::params(), + &out_, + result); + } + + if (errors->empty()) + errors->Capture(); + CHECK(!errors->empty()); + *result = v8::Undefined(env->isolate()); + return v8::Just(errors->ToException(env).ToLocal(err)); + } + + SET_SELF_SIZE(DeriveBitsJob); + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackFieldWithSize("out", out_.size()); + CryptoJob::MemoryInfo(tracker); + } + + private: + ByteSource out_; + bool success_ = false; +}; + +void ThrowCryptoError(Environment* env, + unsigned long err, // NOLINT(runtime/int) + const char* message = nullptr); + +#ifndef OPENSSL_NO_ENGINE +struct EnginePointer { + ENGINE* engine = nullptr; + bool finish_on_exit = false; + + inline EnginePointer() = default; + + inline explicit EnginePointer(ENGINE* engine_, bool finish_on_exit_ = false) + : engine(engine_), + finish_on_exit(finish_on_exit_) {} + + inline EnginePointer(EnginePointer&& other) noexcept + : engine(other.engine), + finish_on_exit(other.finish_on_exit) { + other.release(); + } + + inline ~EnginePointer() { reset(); } + + inline EnginePointer& operator=(EnginePointer&& other) noexcept { + if (this == &other) return *this; + this->~EnginePointer(); + return *new (this) EnginePointer(std::move(other)); + } + + inline operator bool() const { return engine != nullptr; } + + inline ENGINE* get() { return engine; } + + inline void reset(ENGINE* engine_ = nullptr, bool finish_on_exit_ = false) { + if (engine != nullptr) { + if (finish_on_exit) + ENGINE_finish(engine); + ENGINE_free(engine); + } + engine = engine_; + finish_on_exit = finish_on_exit_; + } + + inline ENGINE* release() { + ENGINE* ret = engine; + engine = nullptr; + finish_on_exit = false; + return ret; + } +}; + +EnginePointer LoadEngineById(const char* id, CryptoErrorVector* errors); + +bool SetEngine( + const char* id, + uint32_t flags, + CryptoErrorVector* errors = nullptr); + +void SetEngine(const v8::FunctionCallbackInfo& args); +#endif // !OPENSSL_NO_ENGINE + +#ifdef NODE_FIPS_MODE +void GetFipsCrypto(const v8::FunctionCallbackInfo& args); + +void SetFipsCrypto(const v8::FunctionCallbackInfo& args); +#endif /* NODE_FIPS_MODE */ + +class CipherPushContext { + public: + inline explicit CipherPushContext(Environment* env) : env_(env) {} + + inline void push_back(const char* str) { + list_.emplace_back(OneByteString(env_->isolate(), str)); + } + + inline v8::Local ToJSArray() { + return v8::Array::New(env_->isolate(), list_.data(), list_.size()); + } + + private: + std::vector> list_; + Environment* env_; +}; + +template +void array_push_back(const TypeName* md, + const char* from, + const char* to, + void* arg) { + static_cast(arg)->push_back(from); +} + +inline bool IsAnyByteSource(v8::Local arg) { + return arg->IsArrayBufferView() || + arg->IsArrayBuffer() || + arg->IsSharedArrayBuffer(); +} + +template +class ArrayBufferOrViewContents { + public: + ArrayBufferOrViewContents() = default; + + inline explicit ArrayBufferOrViewContents(v8::Local buf) { + CHECK(IsAnyByteSource(buf)); + if (buf->IsArrayBufferView()) { + auto view = buf.As(); + offset_ = view->ByteOffset(); + length_ = view->ByteLength(); + store_ = view->Buffer()->GetBackingStore(); + } else if (buf->IsArrayBuffer()) { + auto ab = buf.As(); + offset_ = 0; + length_ = ab->ByteLength(); + store_ = ab->GetBackingStore(); + } else { + auto sab = buf.As(); + offset_ = 0; + length_ = sab->ByteLength(); + store_ = sab->GetBackingStore(); + } + } + + inline const T* data() const { + // Ideally, these would return nullptr if IsEmpty() or length_ is zero, + // but some of the openssl API react badly if given a nullptr even when + // length is zero, so we have to return something. + if (size() == 0) + return &buf; + return reinterpret_cast(store_->Data()) + offset_; + } + + inline T* data() { + // Ideally, these would return nullptr if IsEmpty() or length_ is zero, + // but some of the openssl API react badly if given a nullptr even when + // length is zero, so we have to return something. + if (size() == 0) + return &buf; + return reinterpret_cast(store_->Data()) + offset_; + } + + inline size_t size() const { return length_; } + + // In most cases, input buffer sizes passed in to openssl need to + // be limited to <= INT_MAX. This utility method helps us check. + inline bool CheckSizeInt32() { return size() <= INT_MAX; } + + inline ByteSource ToByteSource() const { + return ByteSource::Foreign(data(), size()); + } + + inline ByteSource ToCopy() const { + if (size() == 0) return ByteSource(); + char* buf = MallocOpenSSL(size()); + CHECK_NOT_NULL(buf); + memcpy(buf, data(), size()); + return ByteSource::Allocated(buf, size()); + } + + inline ByteSource ToNullTerminatedCopy() const { + if (size() == 0) return ByteSource(); + char* buf = MallocOpenSSL(size() + 1); + CHECK_NOT_NULL(buf); + buf[size()] = 0; + memcpy(buf, data(), size()); + return ByteSource::Allocated(buf, size()); + } + + template + void CopyTo(M* dest, size_t len) const { + static_assert(sizeof(M) == 1, "sizeof(M) must equal 1"); + len = std::min(len, size()); + if (len > 0 && data() != nullptr) + memcpy(dest, data(), len); + } + + private: + T buf = 0; + size_t offset_ = 0; + size_t length_ = 0; + std::shared_ptr store_; +}; + +template +std::vector CopyBuffer(const ArrayBufferOrViewContents& buf) { + std::vector vec; + vec->resize(buf.size()); + if (vec->size() > 0 && buf.data() != nullptr) + memcpy(vec->data(), buf.data(), vec->size()); + return vec; +} + +template +std::vector CopyBuffer(v8::Local buf) { + return CopyBuffer(ArrayBufferOrViewContents(buf)); +} + +v8::MaybeLocal EncodeBignum( + Environment* env, + const BIGNUM* bn, + v8::Local* error); + +v8::Maybe SetEncodedValue( + Environment* env, + v8::Local target, + v8::Local name, + const BIGNUM* bn, + int size = 0); + +namespace Util { +void Initialize(Environment* env, v8::Local target); +} // namespace Util + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_UTIL_H_ diff --git a/src/env.h b/src/env.h index 6021f0d9940798..db86bce8a1b6d8 100644 --- a/src/env.h +++ b/src/env.h @@ -215,6 +215,7 @@ constexpr size_t kFsStatsBufferLength = V(destroyed_string, "destroyed") \ V(detached_string, "detached") \ V(dh_string, "DH") \ + V(divisor_length_string, "divisorLength") \ V(dns_a_string, "A") \ V(dns_aaaa_string, "AAAA") \ V(dns_cname_string, "CNAME") \ @@ -277,6 +278,23 @@ constexpr size_t kFsStatsBufferLength = V(isclosing_string, "isClosing") \ V(issuer_string, "issuer") \ V(issuercert_string, "issuerCertificate") \ + V(jwk_d_string, "d") \ + V(jwk_dp_string, "dp") \ + V(jwk_dq_string, "dq") \ + V(jwk_dsa_string, "DSA") \ + V(jwk_e_string, "e") \ + V(jwk_ec_string, "EC") \ + V(jwk_g_string, "g") \ + V(jwk_k_string, "k") \ + V(jwk_p_string, "p") \ + V(jwk_q_string, "q") \ + V(jwk_qi_string, "qi") \ + V(jwk_kty_string, "kty") \ + V(jwk_n_string, "n") \ + V(jwk_oct_string, "oct") \ + V(jwk_rsa_string, "RSA") \ + V(jwk_x_string, "x") \ + V(jwk_y_string, "y") \ V(kill_signal_string, "killSignal") \ V(kind_string, "kind") \ V(length_string, "length") \ @@ -290,7 +308,9 @@ constexpr size_t kFsStatsBufferLength = V(minttl_string, "minttl") \ V(module_string, "module") \ V(modulus_string, "modulus") \ + V(modulus_length_string, "modulusLength") \ V(name_string, "name") \ + V(named_curve_string, "namedCurve") \ V(netmask_string, "netmask") \ V(next_string, "next") \ V(nistcurve_string, "nistCurve") \ @@ -339,6 +359,7 @@ constexpr size_t kFsStatsBufferLength = V(promise_string, "promise") \ V(psk_string, "psk") \ V(pubkey_string, "pubkey") \ + V(public_exponent_string, "publicExponent") \ V(query_string, "query") \ V(http3_alpn_string, "h3-29") \ V(rate_string, "rate") \ diff --git a/src/inspector_io.cc b/src/inspector_io.cc index d3bd1911214f83..5868289ae67f79 100644 --- a/src/inspector_io.cc +++ b/src/inspector_io.cc @@ -3,11 +3,10 @@ #include "inspector_socket_server.h" #include "inspector/main_thread_interface.h" #include "inspector/node_string.h" -#include "allocated_buffer-inl.h" // Inlined functions needed by node_crypto.h. +#include "crypto/crypto_util.h" #include "base_object-inl.h" #include "debug_utils-inl.h" #include "node.h" -#include "node_crypto.h" #include "node_internals.h" #include "node_mutex.h" #include "v8-inspector.h" diff --git a/src/node.h b/src/node.h index c23cf45d564848..6cd383cb790641 100644 --- a/src/node.h +++ b/src/node.h @@ -635,7 +635,18 @@ inline void NODE_SET_PROTOTYPE_METHOD(v8::Local recv, #define NODE_SET_PROTOTYPE_METHOD node::NODE_SET_PROTOTYPE_METHOD // BINARY is a deprecated alias of LATIN1. -enum encoding {ASCII, UTF8, BASE64, UCS2, BINARY, HEX, BUFFER, LATIN1 = BINARY}; +// BASE64URL is not currently exposed to the JavaScript side. +enum encoding { + ASCII, + UTF8, + BASE64, + UCS2, + BINARY, + HEX, + BUFFER, + BASE64URL, + LATIN1 = BINARY +}; NODE_EXTERN enum encoding ParseEncoding( v8::Isolate* isolate, diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 61ad1fcc0b6eef..0c1dbcc52d748c 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -20,6930 +20,20 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. #include "node_crypto.h" -#include "node_buffer.h" -#include "crypto/crypto_bio.h" -#include "crypto/crypto_common.h" -#include "crypto/crypto_clienthello-inl.h" -#include "crypto/crypto_groups.h" -#include "node_errors.h" -#include "node_mutex.h" -#include "node_process.h" -#include "allocated_buffer-inl.h" -#include "tls_wrap.h" // TLSWrap - -#include "async_wrap-inl.h" -#include "base_object-inl.h" -#include "env-inl.h" -#include "memory_tracker-inl.h" -#include "string_bytes.h" -#include "threadpoolwork-inl.h" -#include "util-inl.h" -#include "v8.h" - -#include -#include -#ifndef OPENSSL_NO_ENGINE -# include -#endif // !OPENSSL_NO_ENGINE -#include -#include -#include -#include -#include -#include - -#include -#include // INT_MAX -#include - -#include -#include -#include -#include - -namespace node { -namespace crypto { - -using node::THROW_ERR_TLS_INVALID_PROTOCOL_METHOD; - -using v8::Array; -using v8::ArrayBufferView; -using v8::Boolean; -using v8::ConstructorBehavior; -using v8::Context; -using v8::DontDelete; -using v8::Exception; -using v8::External; -using v8::False; -using v8::Function; -using v8::FunctionCallback; -using v8::FunctionCallbackInfo; -using v8::FunctionTemplate; -using v8::HandleScope; -using v8::Int32; -using v8::Integer; -using v8::Isolate; -using v8::Just; -using v8::Local; -using v8::Maybe; -using v8::MaybeLocal; -using v8::NewStringType; -using v8::Nothing; -using v8::Null; -using v8::Object; -using v8::PropertyAttribute; -using v8::ReadOnly; -using v8::SideEffectType; -using v8::Signature; -using v8::String; -using v8::Uint32; -using v8::Undefined; -using v8::Value; - -#ifdef OPENSSL_NO_OCB -# define IS_OCB_MODE(mode) false -#else -# define IS_OCB_MODE(mode) ((mode) == EVP_CIPH_OCB_MODE) -#endif - -static const char* const root_certs[] = { -#include "node_root_certs.h" // NOLINT(build/include_order) -}; - -static const char system_cert_path[] = NODE_OPENSSL_SYSTEM_CERT_PATH; - -static X509_STORE* root_cert_store; - -static bool extra_root_certs_loaded = false; - -// Just to generate static methods -template void SSLWrap::AddMethods(Environment* env, - Local t); -template void SSLWrap::ConfigureSecureContext(SecureContext* sc); -template int SSLWrap::SetCACerts(SecureContext* sc); -template void SSLWrap::MemoryInfo(MemoryTracker* tracker) const; -template SSL_SESSION* SSLWrap::GetSessionCallback( - SSL* s, - const unsigned char* key, - int len, - int* copy); -template int SSLWrap::NewSessionCallback(SSL* s, - SSL_SESSION* sess); -template void SSLWrap::KeylogCallback(const SSL* s, - const char* line); -template void SSLWrap::OnClientHello( - void* arg, - const ClientHelloParser::ClientHello& hello); -template int SSLWrap::TLSExtStatusCallback(SSL* s, void* arg); -template void SSLWrap::DestroySSL(); -template int SSLWrap::SSLCertCallback(SSL* s, void* arg); -template void SSLWrap::WaitForCertCb(CertCb cb, void* arg); -template int SSLWrap::SelectALPNCallback( - SSL* s, - const unsigned char** out, - unsigned char* outlen, - const unsigned char* in, - unsigned int inlen, - void* arg); - -template -void Decode(const FunctionCallbackInfo& args, - void (*callback)(T*, const FunctionCallbackInfo&, - const char*, size_t)) { - T* ctx; - ASSIGN_OR_RETURN_UNWRAP(&ctx, args.Holder()); - - if (args[0]->IsString()) { - StringBytes::InlineDecoder decoder; - Environment* env = Environment::GetCurrent(args); - enum encoding enc = ParseEncoding(env->isolate(), args[1], UTF8); - if (decoder.Decode(env, args[0].As(), enc).IsNothing()) - return; - callback(ctx, args, decoder.out(), decoder.size()); - } else { - ArrayBufferViewContents buf(args[0]); - callback(ctx, args, buf.data(), buf.length()); - } -} - -static int PasswordCallback(char* buf, int size, int rwflag, void* u) { - const char* passphrase = static_cast(u); - if (passphrase != nullptr) { - size_t buflen = static_cast(size); - size_t len = strlen(passphrase); - if (buflen < len) - return -1; - memcpy(buf, passphrase, len); - return len; - } - - return -1; -} - -// Loads OpenSSL engine by engine id and returns it. The loaded engine -// gets a reference so remember the corresponding call to ENGINE_free. -// In case of error the appropriate js exception is scheduled -// and nullptr is returned. -#ifndef OPENSSL_NO_ENGINE -static ENGINE* LoadEngineById(const char* engine_id, char (*errmsg)[1024]) { - MarkPopErrorOnReturn mark_pop_error_on_return; - - ENGINE* engine = ENGINE_by_id(engine_id); - - if (engine == nullptr) { - // Engine not found, try loading dynamically. - engine = ENGINE_by_id("dynamic"); - if (engine != nullptr) { - if (!ENGINE_ctrl_cmd_string(engine, "SO_PATH", engine_id, 0) || - !ENGINE_ctrl_cmd_string(engine, "LOAD", nullptr, 0)) { - ENGINE_free(engine); - engine = nullptr; - } - } - } - - if (engine == nullptr) { - int err = ERR_get_error(); - if (err != 0) { - ERR_error_string_n(err, *errmsg, sizeof(*errmsg)); - } else { - snprintf(*errmsg, sizeof(*errmsg), - "Engine \"%s\" was not found", engine_id); - } - } - - return engine; -} -#endif // !OPENSSL_NO_ENGINE - -// This callback is used to avoid the default passphrase callback in OpenSSL -// which will typically prompt for the passphrase. The prompting is designed -// for the OpenSSL CLI, but works poorly for Node.js because it involves -// synchronous interaction with the controlling terminal, something we never -// want, and use this function to avoid it. -static int NoPasswordCallback(char* buf, int size, int rwflag, void* u) { - return 0; -} - - -// namespace node::crypto::error -namespace error { -Maybe Decorate(Environment* env, Local obj, - unsigned long err) { // NOLINT(runtime/int) - if (err == 0) return Just(true); // No decoration necessary. - - const char* ls = ERR_lib_error_string(err); - const char* fs = ERR_func_error_string(err); - const char* rs = ERR_reason_error_string(err); - - Isolate* isolate = env->isolate(); - Local context = isolate->GetCurrentContext(); - - if (ls != nullptr) { - if (obj->Set(context, env->library_string(), - OneByteString(isolate, ls)).IsNothing()) { - return Nothing(); - } - } - if (fs != nullptr) { - if (obj->Set(context, env->function_string(), - OneByteString(isolate, fs)).IsNothing()) { - return Nothing(); - } - } - if (rs != nullptr) { - if (obj->Set(context, env->reason_string(), - OneByteString(isolate, rs)).IsNothing()) { - return Nothing(); - } - - // SSL has no API to recover the error name from the number, so we - // transform reason strings like "this error" to "ERR_SSL_THIS_ERROR", - // which ends up being close to the original error macro name. - std::string reason(rs); - - for (auto& c : reason) { - if (c == ' ') - c = '_'; - else - c = ToUpper(c); - } - -#define OSSL_ERROR_CODES_MAP(V) \ - V(SYS) \ - V(BN) \ - V(RSA) \ - V(DH) \ - V(EVP) \ - V(BUF) \ - V(OBJ) \ - V(PEM) \ - V(DSA) \ - V(X509) \ - V(ASN1) \ - V(CONF) \ - V(CRYPTO) \ - V(EC) \ - V(SSL) \ - V(BIO) \ - V(PKCS7) \ - V(X509V3) \ - V(PKCS12) \ - V(RAND) \ - V(DSO) \ - V(ENGINE) \ - V(OCSP) \ - V(UI) \ - V(COMP) \ - V(ECDSA) \ - V(ECDH) \ - V(OSSL_STORE) \ - V(FIPS) \ - V(CMS) \ - V(TS) \ - V(HMAC) \ - V(CT) \ - V(ASYNC) \ - V(KDF) \ - V(SM2) \ - V(USER) \ - -#define V(name) case ERR_LIB_##name: lib = #name "_"; break; - const char* lib = ""; - const char* prefix = "OSSL_"; - switch (ERR_GET_LIB(err)) { OSSL_ERROR_CODES_MAP(V) } -#undef V -#undef OSSL_ERROR_CODES_MAP - // Don't generate codes like "ERR_OSSL_SSL_". - if (lib && strcmp(lib, "SSL_") == 0) - prefix = ""; - - // All OpenSSL reason strings fit in a single 80-column macro definition, - // all prefix lengths are <= 10, and ERR_OSSL_ is 9, so 128 is more than - // sufficient. - char code[128]; - snprintf(code, sizeof(code), "ERR_%s%s%s", prefix, lib, reason.c_str()); - - if (obj->Set(env->isolate()->GetCurrentContext(), - env->code_string(), - OneByteString(env->isolate(), code)).IsNothing()) - return Nothing(); - } - - return Just(true); -} -} // namespace error - - -struct CryptoErrorVector : public std::vector { - inline void Capture() { - clear(); - while (auto err = ERR_get_error()) { - char buf[256]; - ERR_error_string_n(err, buf, sizeof(buf)); - push_back(buf); - } - std::reverse(begin(), end()); - } - - inline MaybeLocal ToException( - Environment* env, - Local exception_string = Local()) const { - if (exception_string.IsEmpty()) { - CryptoErrorVector copy(*this); - if (copy.empty()) copy.push_back("no error"); // But possibly a bug... - // Use last element as the error message, everything else goes - // into the .opensslErrorStack property on the exception object. - auto exception_string = - String::NewFromUtf8(env->isolate(), copy.back().data(), - NewStringType::kNormal, copy.back().size()) - .ToLocalChecked(); - copy.pop_back(); - return copy.ToException(env, exception_string); - } - - Local exception_v = Exception::Error(exception_string); - CHECK(!exception_v.IsEmpty()); - - if (!empty()) { - CHECK(exception_v->IsObject()); - Local exception = exception_v.As(); - Maybe ok = exception->Set(env->context(), - env->openssl_error_stack(), - ToV8Value(env->context(), *this).ToLocalChecked()); - if (ok.IsNothing()) - return MaybeLocal(); - } - - return exception_v; - } -}; - - -void ThrowCryptoError(Environment* env, - unsigned long err, // NOLINT(runtime/int) - // Default, only used if there is no SSL `err` which can - // be used to create a long-style message string. - const char* message) { - char message_buffer[128] = {0}; - if (err != 0 || message == nullptr) { - ERR_error_string_n(err, message_buffer, sizeof(message_buffer)); - message = message_buffer; - } - HandleScope scope(env->isolate()); - Local exception_string = - String::NewFromUtf8(env->isolate(), message).ToLocalChecked(); - CryptoErrorVector errors; - errors.Capture(); - Local exception; - if (!errors.ToException(env, exception_string).ToLocal(&exception)) - return; - Local obj; - if (!exception->ToObject(env->context()).ToLocal(&obj)) - return; - if (error::Decorate(env, obj, err).IsNothing()) - return; - env->isolate()->ThrowException(exception); -} - - -// Ensure that OpenSSL has enough entropy (at least 256 bits) for its PRNG. -// The entropy pool starts out empty and needs to fill up before the PRNG -// can be used securely. Once the pool is filled, it never dries up again; -// its contents is stirred and reused when necessary. -// -// OpenSSL normally fills the pool automatically but not when someone starts -// generating random numbers before the pool is full: in that case OpenSSL -// keeps lowering the entropy estimate to thwart attackers trying to guess -// the initial state of the PRNG. -// -// When that happens, we will have to wait until enough entropy is available. -// That should normally never take longer than a few milliseconds. -// -// OpenSSL draws from /dev/random and /dev/urandom. While /dev/random may -// block pending "true" randomness, /dev/urandom is a CSPRNG that doesn't -// block under normal circumstances. -// -// The only time when /dev/urandom may conceivably block is right after boot, -// when the whole system is still low on entropy. That's not something we can -// do anything about. -inline void CheckEntropy() { - for (;;) { - int status = RAND_status(); - CHECK_GE(status, 0); // Cannot fail. - if (status != 0) - break; - - // Give up, RAND_poll() not supported. - if (RAND_poll() == 0) - break; - } -} - - -bool EntropySource(unsigned char* buffer, size_t length) { - // Ensure that OpenSSL's PRNG is properly seeded. - CheckEntropy(); - // RAND_bytes() can return 0 to indicate that the entropy data is not truly - // random. That's okay, it's still better than V8's stock source of entropy, - // which is /dev/urandom on UNIX platforms and the current time on Windows. - return RAND_bytes(buffer, length) != -1; -} - -void SecureContext::Initialize(Environment* env, Local target) { - Local t = env->NewFunctionTemplate(New); - t->InstanceTemplate()->SetInternalFieldCount( - SecureContext::kInternalFieldCount); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - Local secureContextString = - FIXED_ONE_BYTE_STRING(env->isolate(), "SecureContext"); - t->SetClassName(secureContextString); - - env->SetProtoMethod(t, "init", Init); - env->SetProtoMethod(t, "setKey", SetKey); -#ifndef OPENSSL_NO_ENGINE - env->SetProtoMethod(t, "setEngineKey", SetEngineKey); -#endif // !OPENSSL_NO_ENGINE - env->SetProtoMethod(t, "setCert", SetCert); - env->SetProtoMethod(t, "addCACert", AddCACert); - env->SetProtoMethod(t, "addCRL", AddCRL); - env->SetProtoMethod(t, "addRootCerts", AddRootCerts); - env->SetProtoMethod(t, "setCipherSuites", SetCipherSuites); - env->SetProtoMethod(t, "setCiphers", SetCiphers); - env->SetProtoMethod(t, "setSigalgs", SetSigalgs); - env->SetProtoMethod(t, "setECDHCurve", SetECDHCurve); - env->SetProtoMethod(t, "setDHParam", SetDHParam); - env->SetProtoMethod(t, "setMaxProto", SetMaxProto); - env->SetProtoMethod(t, "setMinProto", SetMinProto); - env->SetProtoMethod(t, "getMaxProto", GetMaxProto); - env->SetProtoMethod(t, "getMinProto", GetMinProto); - env->SetProtoMethod(t, "setOptions", SetOptions); - env->SetProtoMethod(t, "setSessionIdContext", SetSessionIdContext); - env->SetProtoMethod(t, "setSessionTimeout", SetSessionTimeout); - env->SetProtoMethod(t, "close", Close); - env->SetProtoMethod(t, "loadPKCS12", LoadPKCS12); -#ifndef OPENSSL_NO_ENGINE - env->SetProtoMethod(t, "setClientCertEngine", SetClientCertEngine); -#endif // !OPENSSL_NO_ENGINE - env->SetProtoMethodNoSideEffect(t, "getTicketKeys", GetTicketKeys); - env->SetProtoMethod(t, "setTicketKeys", SetTicketKeys); - env->SetProtoMethod(t, "setFreeListLength", SetFreeListLength); - env->SetProtoMethod(t, "enableTicketKeyCallback", EnableTicketKeyCallback); - env->SetProtoMethodNoSideEffect(t, "getCertificate", GetCertificate); - env->SetProtoMethodNoSideEffect(t, "getIssuer", GetCertificate); - -#define SET_INTEGER_CONSTANTS(name, value) \ - t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), name), \ - Integer::NewFromUnsigned(env->isolate(), value)); - SET_INTEGER_CONSTANTS("kTicketKeyReturnIndex", kTicketKeyReturnIndex); - SET_INTEGER_CONSTANTS("kTicketKeyHMACIndex", kTicketKeyHMACIndex); - SET_INTEGER_CONSTANTS("kTicketKeyAESIndex", kTicketKeyAESIndex); - SET_INTEGER_CONSTANTS("kTicketKeyNameIndex", kTicketKeyNameIndex); - SET_INTEGER_CONSTANTS("kTicketKeyIVIndex", kTicketKeyIVIndex); - -#undef SET_INTEGER_CONSTANTS - - Local ctx_getter_templ = - FunctionTemplate::New(env->isolate(), - CtxGetter, - Local(), - Signature::New(env->isolate(), t)); - - - t->PrototypeTemplate()->SetAccessorProperty( - FIXED_ONE_BYTE_STRING(env->isolate(), "_external"), - ctx_getter_templ, - Local(), - static_cast(ReadOnly | DontDelete)); - - target->Set(env->context(), secureContextString, - t->GetFunction(env->context()).ToLocalChecked()).Check(); - env->set_secure_context_constructor_template(t); -} - -SecureContext::SecureContext(Environment* env, Local wrap) - : BaseObject(env, wrap) { - MakeWeak(); - env->isolate()->AdjustAmountOfExternalAllocatedMemory(kExternalSize); -} - -inline void SecureContext::Reset() { - if (ctx_ != nullptr) { - env()->isolate()->AdjustAmountOfExternalAllocatedMemory(-kExternalSize); - } - ctx_.reset(); - cert_.reset(); - issuer_.reset(); -} - -SecureContext::~SecureContext() { - Reset(); -} - -void SecureContext::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - new SecureContext(env, args.This()); -} - -// A maxVersion of 0 means "any", but OpenSSL may support TLS versions that -// Node.js doesn't, so pin the max to what we do support. -const int MAX_SUPPORTED_VERSION = TLS1_3_VERSION; - -void SecureContext::Init(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - Environment* env = sc->env(); - - CHECK_EQ(args.Length(), 3); - CHECK(args[1]->IsInt32()); - CHECK(args[2]->IsInt32()); - - int min_version = args[1].As()->Value(); - int max_version = args[2].As()->Value(); - const SSL_METHOD* method = TLS_method(); - - if (max_version == 0) - max_version = MAX_SUPPORTED_VERSION; - - if (args[0]->IsString()) { - const node::Utf8Value sslmethod(env->isolate(), args[0]); - - // Note that SSLv2 and SSLv3 are disallowed but SSLv23_method and friends - // are still accepted. They are OpenSSL's way of saying that all known - // protocols below TLS 1.3 are supported unless explicitly disabled (which - // we do below for SSLv2 and SSLv3.) - if (sslmethod == "SSLv2_method" || - sslmethod == "SSLv2_server_method" || - sslmethod == "SSLv2_client_method") { - THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv2 methods disabled"); - return; - } else if (sslmethod == "SSLv3_method" || - sslmethod == "SSLv3_server_method" || - sslmethod == "SSLv3_client_method") { - THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, "SSLv3 methods disabled"); - return; - } else if (sslmethod == "SSLv23_method") { - max_version = TLS1_2_VERSION; - } else if (sslmethod == "SSLv23_server_method") { - max_version = TLS1_2_VERSION; - method = TLS_server_method(); - } else if (sslmethod == "SSLv23_client_method") { - max_version = TLS1_2_VERSION; - method = TLS_client_method(); - } else if (sslmethod == "TLS_method") { - min_version = 0; - max_version = MAX_SUPPORTED_VERSION; - } else if (sslmethod == "TLS_server_method") { - min_version = 0; - max_version = MAX_SUPPORTED_VERSION; - method = TLS_server_method(); - } else if (sslmethod == "TLS_client_method") { - min_version = 0; - max_version = MAX_SUPPORTED_VERSION; - method = TLS_client_method(); - } else if (sslmethod == "TLSv1_method") { - min_version = TLS1_VERSION; - max_version = TLS1_VERSION; - } else if (sslmethod == "TLSv1_server_method") { - min_version = TLS1_VERSION; - max_version = TLS1_VERSION; - method = TLS_server_method(); - } else if (sslmethod == "TLSv1_client_method") { - min_version = TLS1_VERSION; - max_version = TLS1_VERSION; - method = TLS_client_method(); - } else if (sslmethod == "TLSv1_1_method") { - min_version = TLS1_1_VERSION; - max_version = TLS1_1_VERSION; - } else if (sslmethod == "TLSv1_1_server_method") { - min_version = TLS1_1_VERSION; - max_version = TLS1_1_VERSION; - method = TLS_server_method(); - } else if (sslmethod == "TLSv1_1_client_method") { - min_version = TLS1_1_VERSION; - max_version = TLS1_1_VERSION; - method = TLS_client_method(); - } else if (sslmethod == "TLSv1_2_method") { - min_version = TLS1_2_VERSION; - max_version = TLS1_2_VERSION; - } else if (sslmethod == "TLSv1_2_server_method") { - min_version = TLS1_2_VERSION; - max_version = TLS1_2_VERSION; - method = TLS_server_method(); - } else if (sslmethod == "TLSv1_2_client_method") { - min_version = TLS1_2_VERSION; - max_version = TLS1_2_VERSION; - method = TLS_client_method(); - } else { - const std::string msg("Unknown method: "); - THROW_ERR_TLS_INVALID_PROTOCOL_METHOD(env, (msg + * sslmethod).c_str()); - return; - } - } - - sc->ctx_.reset(SSL_CTX_new(method)); - SSL_CTX_set_app_data(sc->ctx_.get(), sc); - - // Disable SSLv2 in the case when method == TLS_method() and the - // cipher list contains SSLv2 ciphers (not the default, should be rare.) - // The bundled OpenSSL doesn't have SSLv2 support but the system OpenSSL may. - // SSLv3 is disabled because it's susceptible to downgrade attacks (POODLE.) - SSL_CTX_set_options(sc->ctx_.get(), SSL_OP_NO_SSLv2); - SSL_CTX_set_options(sc->ctx_.get(), SSL_OP_NO_SSLv3); - - // Enable automatic cert chaining. This is enabled by default in OpenSSL, but - // disabled by default in BoringSSL. Enable it explicitly to make the - // behavior match when Node is built with BoringSSL. - SSL_CTX_clear_mode(sc->ctx_.get(), SSL_MODE_NO_AUTO_CHAIN); - - // SSL session cache configuration - SSL_CTX_set_session_cache_mode(sc->ctx_.get(), - SSL_SESS_CACHE_CLIENT | - SSL_SESS_CACHE_SERVER | - SSL_SESS_CACHE_NO_INTERNAL | - SSL_SESS_CACHE_NO_AUTO_CLEAR); - - SSL_CTX_set_min_proto_version(sc->ctx_.get(), min_version); - SSL_CTX_set_max_proto_version(sc->ctx_.get(), max_version); - - // OpenSSL 1.1.0 changed the ticket key size, but the OpenSSL 1.0.x size was - // exposed in the public API. To retain compatibility, install a callback - // which restores the old algorithm. - if (RAND_bytes(sc->ticket_key_name_, sizeof(sc->ticket_key_name_)) <= 0 || - RAND_bytes(sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_)) <= 0 || - RAND_bytes(sc->ticket_key_aes_, sizeof(sc->ticket_key_aes_)) <= 0) { - return env->ThrowError("Error generating ticket keys"); - } - SSL_CTX_set_tlsext_ticket_key_cb(sc->ctx_.get(), TicketCompatibilityCallback); -} - - -// Takes a string or buffer and loads it into a BIO. -// Caller responsible for BIO_free_all-ing the returned object. -static BIOPointer LoadBIO(Environment* env, Local v) { - HandleScope scope(env->isolate()); - - if (v->IsString()) { - const node::Utf8Value s(env->isolate(), v); - return NodeBIO::NewFixed(*s, s.length()); - } - - if (v->IsArrayBufferView()) { - ArrayBufferViewContents buf(v.As()); - return NodeBIO::NewFixed(buf.data(), buf.length()); - } - - return nullptr; -} - - -void SecureContext::SetKey(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - unsigned int len = args.Length(); - if (len < 1) { - return THROW_ERR_MISSING_ARGS(env, "Private key argument is mandatory"); - } - - if (len > 2) { - return env->ThrowError("Only private key and pass phrase are expected"); - } - - if (len == 2) { - if (args[1]->IsUndefined() || args[1]->IsNull()) - len = 1; - else - THROW_AND_RETURN_IF_NOT_STRING(env, args[1], "Pass phrase"); - } - - BIOPointer bio(LoadBIO(env, args[0])); - if (!bio) - return; - - node::Utf8Value passphrase(env->isolate(), args[1]); - - EVPKeyPointer key( - PEM_read_bio_PrivateKey(bio.get(), - nullptr, - PasswordCallback, - *passphrase)); - - if (!key) { - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - return ThrowCryptoError(env, err, "PEM_read_bio_PrivateKey"); - } - - int rv = SSL_CTX_use_PrivateKey(sc->ctx_.get(), key.get()); - - if (!rv) { - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - return ThrowCryptoError(env, err, "SSL_CTX_use_PrivateKey"); - } -} - -void SecureContext::SetSigalgs(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - Environment* env = sc->env(); - ClearErrorOnReturn clear_error_on_return; - - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsString()); - - const node::Utf8Value sigalgs(env->isolate(), args[0]); - - int rv = SSL_CTX_set1_sigalgs_list(sc->ctx_.get(), *sigalgs); - - if (rv == 0) { - return ThrowCryptoError(env, ERR_get_error()); - } -} - -#ifndef OPENSSL_NO_ENGINE -// Helpers for the smart pointer. -void ENGINE_free_fn(ENGINE* engine) { ENGINE_free(engine); } - -void ENGINE_finish_and_free_fn(ENGINE* engine) { - ENGINE_finish(engine); - ENGINE_free(engine); -} - -void SecureContext::SetEngineKey(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - CHECK_EQ(args.Length(), 2); - - char errmsg[1024]; - const node::Utf8Value engine_id(env->isolate(), args[1]); - std::unique_ptr> e = - { LoadEngineById(*engine_id, &errmsg), - ENGINE_free_fn }; - if (e.get() == nullptr) { - return env->ThrowError(errmsg); - } - - if (!ENGINE_init(e.get())) { - return env->ThrowError("ENGINE_init"); - } - - e.get_deleter() = ENGINE_finish_and_free_fn; - - const node::Utf8Value key_name(env->isolate(), args[0]); - EVPKeyPointer key(ENGINE_load_private_key(e.get(), *key_name, - nullptr, nullptr)); - - if (!key) { - return ThrowCryptoError(env, ERR_get_error(), "ENGINE_load_private_key"); - } - - int rv = SSL_CTX_use_PrivateKey(sc->ctx_.get(), key.get()); - - if (rv == 0) { - return ThrowCryptoError(env, ERR_get_error(), "SSL_CTX_use_PrivateKey"); - } - - sc->private_key_engine_ = std::move(e); -} -#endif // !OPENSSL_NO_ENGINE - -int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, - X509Pointer&& x, - STACK_OF(X509)* extra_certs, - X509Pointer* cert, - X509Pointer* issuer_) { - CHECK(!*issuer_); - CHECK(!*cert); - X509* issuer = nullptr; - - int ret = SSL_CTX_use_certificate(ctx, x.get()); - - if (ret) { - // If we could set up our certificate, now proceed to - // the CA certificates. - SSL_CTX_clear_extra_chain_certs(ctx); - - for (int i = 0; i < sk_X509_num(extra_certs); i++) { - X509* ca = sk_X509_value(extra_certs, i); - - // NOTE: Increments reference count on `ca` - if (!SSL_CTX_add1_chain_cert(ctx, ca)) { - ret = 0; - issuer = nullptr; - break; - } - // Note that we must not free r if it was successfully - // added to the chain (while we must free the main - // certificate, since its reference count is increased - // by SSL_CTX_use_certificate). - - // Find issuer - if (issuer != nullptr || X509_check_issued(ca, x.get()) != X509_V_OK) - continue; - - issuer = ca; - } - } - - // Try getting issuer from a cert store - if (ret) { - if (issuer == nullptr) { - ret = SSL_CTX_get_issuer(ctx, x.get(), &issuer); - ret = ret < 0 ? 0 : 1; - // NOTE: get_cert_store doesn't increment reference count, - // no need to free `store` - } else { - // Increment issuer reference count - issuer = X509_dup(issuer); - if (issuer == nullptr) { - ret = 0; - } - } - } - - issuer_->reset(issuer); - - if (ret && x != nullptr) { - cert->reset(X509_dup(x.get())); - if (!*cert) - ret = 0; - } - return ret; -} - - -// Read a file that contains our certificate in "PEM" format, -// possibly followed by a sequence of CA certificates that should be -// sent to the peer in the Certificate message. -// -// Taken from OpenSSL - edited for style. -int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, - BIOPointer&& in, - X509Pointer* cert, - X509Pointer* issuer) { - // Just to ensure that `ERR_peek_last_error` below will return only errors - // that we are interested in - ERR_clear_error(); - - X509Pointer x( - PEM_read_bio_X509_AUX(in.get(), nullptr, NoPasswordCallback, nullptr)); - - if (!x) - return 0; - - unsigned long err = 0; // NOLINT(runtime/int) - - StackOfX509 extra_certs(sk_X509_new_null()); - if (!extra_certs) - return 0; - - while (X509Pointer extra {PEM_read_bio_X509(in.get(), - nullptr, - NoPasswordCallback, - nullptr)}) { - if (sk_X509_push(extra_certs.get(), extra.get())) { - extra.release(); - continue; - } - - return 0; - } - - // When the while loop ends, it's usually just EOF. - err = ERR_peek_last_error(); - if (ERR_GET_LIB(err) == ERR_LIB_PEM && - ERR_GET_REASON(err) == PEM_R_NO_START_LINE) { - ERR_clear_error(); - } else { - // some real error - return 0; - } - - return SSL_CTX_use_certificate_chain(ctx, - std::move(x), - extra_certs.get(), - cert, - issuer); -} - - -void SecureContext::SetCert(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - if (args.Length() != 1) { - return THROW_ERR_MISSING_ARGS(env, "Certificate argument is mandatory"); - } - - BIOPointer bio(LoadBIO(env, args[0])); - if (!bio) - return; - - sc->cert_.reset(); - sc->issuer_.reset(); - - int rv = SSL_CTX_use_certificate_chain(sc->ctx_.get(), - std::move(bio), - &sc->cert_, - &sc->issuer_); - - if (!rv) { - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - return ThrowCryptoError(env, err, "SSL_CTX_use_certificate_chain"); - } -} - - -static X509_STORE* NewRootCertStore() { - static std::vector root_certs_vector; - static Mutex root_certs_vector_mutex; - Mutex::ScopedLock lock(root_certs_vector_mutex); - - if (root_certs_vector.empty()) { - for (size_t i = 0; i < arraysize(root_certs); i++) { - X509* x509 = - PEM_read_bio_X509(NodeBIO::NewFixed(root_certs[i], - strlen(root_certs[i])).get(), - nullptr, // no re-use of X509 structure - NoPasswordCallback, - nullptr); // no callback data - - // Parse errors from the built-in roots are fatal. - CHECK_NOT_NULL(x509); - - root_certs_vector.push_back(x509); - } - } - - X509_STORE* store = X509_STORE_new(); - if (*system_cert_path != '\0') { - X509_STORE_load_locations(store, system_cert_path, nullptr); - } - - Mutex::ScopedLock cli_lock(node::per_process::cli_options_mutex); - if (per_process::cli_options->ssl_openssl_cert_store) { - X509_STORE_set_default_paths(store); - } else { - for (X509* cert : root_certs_vector) { - X509_up_ref(cert); - X509_STORE_add_cert(store, cert); - } - } - - return store; -} - - -void GetRootCertificates(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Local result[arraysize(root_certs)]; - - for (size_t i = 0; i < arraysize(root_certs); i++) { - if (!String::NewFromOneByte( - env->isolate(), - reinterpret_cast(root_certs[i])) - .ToLocal(&result[i])) { - return; - } - } - - args.GetReturnValue().Set( - Array::New(env->isolate(), result, arraysize(root_certs))); -} - - -void SecureContext::AddCACert(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - ClearErrorOnReturn clear_error_on_return; - - if (args.Length() != 1) { - return THROW_ERR_MISSING_ARGS(env, "CA certificate argument is mandatory"); - } - - BIOPointer bio(LoadBIO(env, args[0])); - if (!bio) - return; - - X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get()); - while (X509* x509 = PEM_read_bio_X509_AUX( - bio.get(), nullptr, NoPasswordCallback, nullptr)) { - if (cert_store == root_cert_store) { - cert_store = NewRootCertStore(); - SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store); - } - X509_STORE_add_cert(cert_store, x509); - SSL_CTX_add_client_CA(sc->ctx_.get(), x509); - X509_free(x509); - } -} - - -void SecureContext::AddCRL(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - if (args.Length() != 1) { - return THROW_ERR_MISSING_ARGS(env, "CRL argument is mandatory"); - } - - ClearErrorOnReturn clear_error_on_return; - - BIOPointer bio(LoadBIO(env, args[0])); - if (!bio) - return; - - DeleteFnPtr crl( - PEM_read_bio_X509_CRL(bio.get(), nullptr, NoPasswordCallback, nullptr)); - - if (!crl) - return env->ThrowError("Failed to parse CRL"); - - X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get()); - if (cert_store == root_cert_store) { - cert_store = NewRootCertStore(); - SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store); - } - - X509_STORE_add_crl(cert_store, crl.get()); - X509_STORE_set_flags(cert_store, - X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); -} - - -static unsigned long AddCertsFromFile( // NOLINT(runtime/int) - X509_STORE* store, - const char* file) { - ERR_clear_error(); - MarkPopErrorOnReturn mark_pop_error_on_return; - - BIOPointer bio(BIO_new_file(file, "r")); - if (!bio) - return ERR_get_error(); - - while (X509* x509 = - PEM_read_bio_X509(bio.get(), nullptr, NoPasswordCallback, nullptr)) { - X509_STORE_add_cert(store, x509); - X509_free(x509); - } - - unsigned long err = ERR_peek_error(); // NOLINT(runtime/int) - // Ignore error if its EOF/no start line found. - if (ERR_GET_LIB(err) == ERR_LIB_PEM && - ERR_GET_REASON(err) == PEM_R_NO_START_LINE) { - return 0; - } - - return err; -} - - -void UseExtraCaCerts(const std::string& file) { - ClearErrorOnReturn clear_error_on_return; - - if (root_cert_store == nullptr) { - root_cert_store = NewRootCertStore(); - - if (!file.empty()) { - unsigned long err = AddCertsFromFile( // NOLINT(runtime/int) - root_cert_store, - file.c_str()); - if (err) { - fprintf(stderr, - "Warning: Ignoring extra certs from `%s`, load failed: %s\n", - file.c_str(), - ERR_error_string(err, nullptr)); - } else { - extra_root_certs_loaded = true; - } - } - } -} - - -static void IsExtraRootCertsFileLoaded( - const FunctionCallbackInfo& args) { - return args.GetReturnValue().Set(extra_root_certs_loaded); -} - - -void SecureContext::AddRootCerts(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - ClearErrorOnReturn clear_error_on_return; - - if (root_cert_store == nullptr) { - root_cert_store = NewRootCertStore(); - } - - // Increment reference count so global store is not deleted along with CTX. - X509_STORE_up_ref(root_cert_store); - SSL_CTX_set_cert_store(sc->ctx_.get(), root_cert_store); -} - - -void SecureContext::SetCipherSuites(const FunctionCallbackInfo& args) { - // BoringSSL doesn't allow API config of TLS1.3 cipher suites. -#ifndef OPENSSL_IS_BORINGSSL - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - Environment* env = sc->env(); - ClearErrorOnReturn clear_error_on_return; - - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsString()); - - const node::Utf8Value ciphers(args.GetIsolate(), args[0]); - if (!SSL_CTX_set_ciphersuites(sc->ctx_.get(), *ciphers)) { - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - return ThrowCryptoError(env, err, "Failed to set ciphers"); - } -#endif -} - - -void SecureContext::SetCiphers(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - Environment* env = sc->env(); - ClearErrorOnReturn clear_error_on_return; - - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsString()); - - const node::Utf8Value ciphers(args.GetIsolate(), args[0]); - if (!SSL_CTX_set_cipher_list(sc->ctx_.get(), *ciphers)) { - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - - if (strlen(*ciphers) == 0 && ERR_GET_REASON(err) == SSL_R_NO_CIPHER_MATCH) { - // TLS1.2 ciphers were deliberately cleared, so don't consider - // SSL_R_NO_CIPHER_MATCH to be an error (this is how _set_cipher_suites() - // works). If the user actually sets a value (like "no-such-cipher"), then - // that's actually an error. - return; - } - return ThrowCryptoError(env, err, "Failed to set ciphers"); - } -} - - -void SecureContext::SetECDHCurve(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - Environment* env = sc->env(); - - if (args.Length() != 1) - return THROW_ERR_MISSING_ARGS(env, "ECDH curve name argument is mandatory"); - - THROW_AND_RETURN_IF_NOT_STRING(env, args[0], "ECDH curve name"); - - node::Utf8Value curve(env->isolate(), args[0]); - - if (strcmp(*curve, "auto") == 0) - return; - - if (!SSL_CTX_set1_curves_list(sc->ctx_.get(), *curve)) - return env->ThrowError("Failed to set ECDH curve"); -} - - -void SecureContext::SetDHParam(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.This()); - Environment* env = sc->env(); - ClearErrorOnReturn clear_error_on_return; - - // Auto DH is not supported in openssl 1.0.1, so dhparam needs - // to be specified explicitly - if (args.Length() != 1) - return THROW_ERR_MISSING_ARGS(env, "DH argument is mandatory"); - - DHPointer dh; - { - BIOPointer bio(LoadBIO(env, args[0])); - if (!bio) - return; - - dh.reset(PEM_read_bio_DHparams(bio.get(), nullptr, nullptr, nullptr)); - } - - // Invalid dhparam is silently discarded and DHE is no longer used. - if (!dh) - return; - - const BIGNUM* p; - DH_get0_pqg(dh.get(), &p, nullptr, nullptr); - const int size = BN_num_bits(p); - if (size < 1024) { - return THROW_ERR_INVALID_ARG_VALUE( - env, "DH parameter is less than 1024 bits"); - } else if (size < 2048) { - args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING( - env->isolate(), "DH parameter is less than 2048 bits")); - } - - SSL_CTX_set_options(sc->ctx_.get(), SSL_OP_SINGLE_DH_USE); - int r = SSL_CTX_set_tmp_dh(sc->ctx_.get(), dh.get()); - - if (!r) - return env->ThrowTypeError("Error setting temp DH parameter"); -} - - -void SecureContext::SetMinProto(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsInt32()); - - int version = args[0].As()->Value(); - - CHECK(SSL_CTX_set_min_proto_version(sc->ctx_.get(), version)); -} - - -void SecureContext::SetMaxProto(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsInt32()); - - int version = args[0].As()->Value(); - - CHECK(SSL_CTX_set_max_proto_version(sc->ctx_.get(), version)); -} - - -void SecureContext::GetMinProto(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - CHECK_EQ(args.Length(), 0); - - long version = // NOLINT(runtime/int) - SSL_CTX_get_min_proto_version(sc->ctx_.get()); - args.GetReturnValue().Set(static_cast(version)); -} - - -void SecureContext::GetMaxProto(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - CHECK_EQ(args.Length(), 0); - - long version = // NOLINT(runtime/int) - SSL_CTX_get_max_proto_version(sc->ctx_.get()); - args.GetReturnValue().Set(static_cast(version)); -} - - -void SecureContext::SetOptions(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - int64_t val; - - if (args.Length() != 1 || - !args[0]->IntegerValue(args.GetIsolate()->GetCurrentContext()).To(&val)) { - return THROW_ERR_INVALID_ARG_TYPE( - sc->env(), "Options must be an integer value"); - } - - SSL_CTX_set_options(sc->ctx_.get(), - static_cast(val)); // NOLINT(runtime/int) -} - - -void SecureContext::SetSessionIdContext( - const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - Environment* env = sc->env(); - - if (args.Length() != 1) { - return THROW_ERR_MISSING_ARGS( - env, "Session ID context argument is mandatory"); - } - - THROW_AND_RETURN_IF_NOT_STRING(env, args[0], "Session ID context"); - - const node::Utf8Value sessionIdContext(args.GetIsolate(), args[0]); - const unsigned char* sid_ctx = - reinterpret_cast(*sessionIdContext); - unsigned int sid_ctx_len = sessionIdContext.length(); - - int r = SSL_CTX_set_session_id_context(sc->ctx_.get(), sid_ctx, sid_ctx_len); - if (r == 1) - return; - - BUF_MEM* mem; - Local message; - - BIOPointer bio(BIO_new(BIO_s_mem())); - if (!bio) { - message = FIXED_ONE_BYTE_STRING(args.GetIsolate(), - "SSL_CTX_set_session_id_context error"); - } else { - ERR_print_errors(bio.get()); - BIO_get_mem_ptr(bio.get(), &mem); - message = OneByteString(args.GetIsolate(), mem->data, mem->length); - } - - args.GetIsolate()->ThrowException(Exception::TypeError(message)); -} - - -void SecureContext::SetSessionTimeout(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - if (args.Length() != 1 || !args[0]->IsInt32()) { - return THROW_ERR_INVALID_ARG_TYPE( - sc->env(), "Session timeout must be a 32-bit integer"); - } - - int32_t sessionTimeout = args[0].As()->Value(); - SSL_CTX_set_timeout(sc->ctx_.get(), sessionTimeout); -} - - -void SecureContext::Close(const FunctionCallbackInfo& args) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - sc->Reset(); -} - - -// Takes .pfx or .p12 and password in string or buffer format -void SecureContext::LoadPKCS12(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - std::vector pass; - bool ret = false; - - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - ClearErrorOnReturn clear_error_on_return; - - if (args.Length() < 1) { - return THROW_ERR_MISSING_ARGS(env, "PFX certificate argument is mandatory"); - } - - BIOPointer in(LoadBIO(env, args[0])); - if (!in) - return env->ThrowError("Unable to load BIO"); - - if (args.Length() >= 2) { - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[1], "Pass phrase"); - Local abv = args[1].As(); - size_t passlen = abv->ByteLength(); - pass.resize(passlen + 1); - abv->CopyContents(pass.data(), passlen); - pass[passlen] = '\0'; - } - - // Free previous certs - sc->issuer_.reset(); - sc->cert_.reset(); - - X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get()); - - DeleteFnPtr p12; - EVPKeyPointer pkey; - X509Pointer cert; - StackOfX509 extra_certs; - - PKCS12* p12_ptr = nullptr; - EVP_PKEY* pkey_ptr = nullptr; - X509* cert_ptr = nullptr; - STACK_OF(X509)* extra_certs_ptr = nullptr; - if (d2i_PKCS12_bio(in.get(), &p12_ptr) && - (p12.reset(p12_ptr), true) && // Move ownership to the smart pointer. - PKCS12_parse(p12.get(), pass.data(), - &pkey_ptr, - &cert_ptr, - &extra_certs_ptr) && - (pkey.reset(pkey_ptr), cert.reset(cert_ptr), - extra_certs.reset(extra_certs_ptr), true) && // Move ownership. - SSL_CTX_use_certificate_chain(sc->ctx_.get(), - std::move(cert), - extra_certs.get(), - &sc->cert_, - &sc->issuer_) && - SSL_CTX_use_PrivateKey(sc->ctx_.get(), pkey.get())) { - // Add CA certs too - for (int i = 0; i < sk_X509_num(extra_certs.get()); i++) { - X509* ca = sk_X509_value(extra_certs.get(), i); - - if (cert_store == root_cert_store) { - cert_store = NewRootCertStore(); - SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store); - } - X509_STORE_add_cert(cert_store, ca); - SSL_CTX_add_client_CA(sc->ctx_.get(), ca); - } - ret = true; - } - - if (!ret) { - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - const char* str = ERR_reason_error_string(err); - return env->ThrowError(str); - } -} - - -#ifndef OPENSSL_NO_ENGINE -void SecureContext::SetClientCertEngine( - const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsString()); - - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder()); - - MarkPopErrorOnReturn mark_pop_error_on_return; - - // SSL_CTX_set_client_cert_engine does not itself support multiple - // calls by cleaning up before overwriting the client_cert_engine - // internal context variable. - // Instead of trying to fix up this problem we in turn also do not - // support multiple calls to SetClientCertEngine. - if (sc->client_cert_engine_provided_) { - return env->ThrowError( - "Multiple calls to SetClientCertEngine are not allowed"); - } - - const node::Utf8Value engine_id(env->isolate(), args[0]); - char errmsg[1024]; - DeleteFnPtr engine( - LoadEngineById(*engine_id, &errmsg)); - - if (!engine) - return env->ThrowError(errmsg); - - // Note that this takes another reference to `engine`. - int r = SSL_CTX_set_client_cert_engine(sc->ctx_.get(), engine.get()); - if (r == 0) - return ThrowCryptoError(env, ERR_get_error()); - sc->client_cert_engine_provided_ = true; -} -#endif // !OPENSSL_NO_ENGINE - - -void SecureContext::GetTicketKeys(const FunctionCallbackInfo& args) { -#if !defined(OPENSSL_NO_TLSEXT) && defined(SSL_CTX_get_tlsext_ticket_keys) - - SecureContext* wrap; - ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); - - Local buff = Buffer::New(wrap->env(), 48).ToLocalChecked(); - memcpy(Buffer::Data(buff), wrap->ticket_key_name_, 16); - memcpy(Buffer::Data(buff) + 16, wrap->ticket_key_hmac_, 16); - memcpy(Buffer::Data(buff) + 32, wrap->ticket_key_aes_, 16); - - args.GetReturnValue().Set(buff); -#endif // !def(OPENSSL_NO_TLSEXT) && def(SSL_CTX_get_tlsext_ticket_keys) -} - - -void SecureContext::SetTicketKeys(const FunctionCallbackInfo& args) { -#if !defined(OPENSSL_NO_TLSEXT) && defined(SSL_CTX_get_tlsext_ticket_keys) - SecureContext* wrap; - ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); - Environment* env = wrap->env(); - - // TODO(@sam-github) Move type and len check to js, and CHECK() in C++. - if (args.Length() < 1) { - return THROW_ERR_MISSING_ARGS(env, "Ticket keys argument is mandatory"); - } - - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "Ticket keys"); - ArrayBufferViewContents buf(args[0].As()); - - if (buf.length() != 48) { - return THROW_ERR_INVALID_ARG_VALUE( - env, "Ticket keys length must be 48 bytes"); - } - - memcpy(wrap->ticket_key_name_, buf.data(), 16); - memcpy(wrap->ticket_key_hmac_, buf.data() + 16, 16); - memcpy(wrap->ticket_key_aes_, buf.data() + 32, 16); - - args.GetReturnValue().Set(true); -#endif // !def(OPENSSL_NO_TLSEXT) && def(SSL_CTX_get_tlsext_ticket_keys) -} - - -void SecureContext::SetFreeListLength(const FunctionCallbackInfo& args) { -} - - -// Currently, EnableTicketKeyCallback and TicketKeyCallback are only present for -// the regression test in test/parallel/test-https-resume-after-renew.js. -void SecureContext::EnableTicketKeyCallback( - const FunctionCallbackInfo& args) { - SecureContext* wrap; - ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); - - SSL_CTX_set_tlsext_ticket_key_cb(wrap->ctx_.get(), TicketKeyCallback); -} - - -int SecureContext::TicketKeyCallback(SSL* ssl, - unsigned char* name, - unsigned char* iv, - EVP_CIPHER_CTX* ectx, - HMAC_CTX* hctx, - int enc) { - static const int kTicketPartSize = 16; - - SecureContext* sc = static_cast( - SSL_CTX_get_app_data(SSL_get_SSL_CTX(ssl))); - - Environment* env = sc->env(); - HandleScope handle_scope(env->isolate()); - Context::Scope context_scope(env->context()); - - Local argv[] = { - Buffer::Copy(env, - reinterpret_cast(name), - kTicketPartSize).ToLocalChecked(), - Buffer::Copy(env, - reinterpret_cast(iv), - kTicketPartSize).ToLocalChecked(), - Boolean::New(env->isolate(), enc != 0) - }; - - Local ret = node::MakeCallback(env->isolate(), - sc->object(), - env->ticketkeycallback_string(), - arraysize(argv), - argv, - {0, 0}).ToLocalChecked(); - Local arr = ret.As(); - - int r = - arr->Get(env->context(), - kTicketKeyReturnIndex).ToLocalChecked() - ->Int32Value(env->context()).FromJust(); - if (r < 0) - return r; - - Local hmac = arr->Get(env->context(), - kTicketKeyHMACIndex).ToLocalChecked(); - Local aes = arr->Get(env->context(), - kTicketKeyAESIndex).ToLocalChecked(); - if (Buffer::Length(aes) != kTicketPartSize) - return -1; - - if (enc) { - Local name_val = arr->Get(env->context(), - kTicketKeyNameIndex).ToLocalChecked(); - Local iv_val = arr->Get(env->context(), - kTicketKeyIVIndex).ToLocalChecked(); - - if (Buffer::Length(name_val) != kTicketPartSize || - Buffer::Length(iv_val) != kTicketPartSize) { - return -1; - } - - name_val.As()->CopyContents(name, kTicketPartSize); - iv_val.As()->CopyContents(iv, kTicketPartSize); - } - - ArrayBufferViewContents hmac_buf(hmac); - HMAC_Init_ex(hctx, - hmac_buf.data(), - hmac_buf.length(), - EVP_sha256(), - nullptr); - - ArrayBufferViewContents aes_key(aes.As()); - if (enc) { - EVP_EncryptInit_ex(ectx, - EVP_aes_128_cbc(), - nullptr, - aes_key.data(), - iv); - } else { - EVP_DecryptInit_ex(ectx, - EVP_aes_128_cbc(), - nullptr, - aes_key.data(), - iv); - } - - return r; -} - - -int SecureContext::TicketCompatibilityCallback(SSL* ssl, - unsigned char* name, - unsigned char* iv, - EVP_CIPHER_CTX* ectx, - HMAC_CTX* hctx, - int enc) { - SecureContext* sc = static_cast( - SSL_CTX_get_app_data(SSL_get_SSL_CTX(ssl))); - - if (enc) { - memcpy(name, sc->ticket_key_name_, sizeof(sc->ticket_key_name_)); - if (RAND_bytes(iv, 16) <= 0 || - EVP_EncryptInit_ex(ectx, EVP_aes_128_cbc(), nullptr, - sc->ticket_key_aes_, iv) <= 0 || - HMAC_Init_ex(hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_), - EVP_sha256(), nullptr) <= 0) { - return -1; - } - return 1; - } - - if (memcmp(name, sc->ticket_key_name_, sizeof(sc->ticket_key_name_)) != 0) { - // The ticket key name does not match. Discard the ticket. - return 0; - } - - if (EVP_DecryptInit_ex(ectx, EVP_aes_128_cbc(), nullptr, sc->ticket_key_aes_, - iv) <= 0 || - HMAC_Init_ex(hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_), - EVP_sha256(), nullptr) <= 0) { - return -1; - } - return 1; -} - - -void SecureContext::CtxGetter(const FunctionCallbackInfo& info) { - SecureContext* sc; - ASSIGN_OR_RETURN_UNWRAP(&sc, info.This()); - Local ext = External::New(info.GetIsolate(), sc->ctx_.get()); - info.GetReturnValue().Set(ext); -} - - -template -void SecureContext::GetCertificate(const FunctionCallbackInfo& args) { - SecureContext* wrap; - ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); - Environment* env = wrap->env(); - X509* cert; - - if (primary) - cert = wrap->cert_.get(); - else - cert = wrap->issuer_.get(); - if (cert == nullptr) - return args.GetReturnValue().SetNull(); - - int size = i2d_X509(cert, nullptr); - Local buff = Buffer::New(env, size).ToLocalChecked(); - unsigned char* serialized = reinterpret_cast( - Buffer::Data(buff)); - i2d_X509(cert, &serialized); - - args.GetReturnValue().Set(buff); -} - - -template -void SSLWrap::AddMethods(Environment* env, Local t) { - HandleScope scope(env->isolate()); - - env->SetProtoMethodNoSideEffect(t, "getPeerCertificate", GetPeerCertificate); - env->SetProtoMethodNoSideEffect(t, "getCertificate", GetCertificate); - env->SetProtoMethodNoSideEffect(t, "getFinished", GetFinished); - env->SetProtoMethodNoSideEffect(t, "getPeerFinished", GetPeerFinished); - env->SetProtoMethodNoSideEffect(t, "getSession", GetSession); - env->SetProtoMethod(t, "setSession", SetSession); - env->SetProtoMethod(t, "loadSession", LoadSession); - env->SetProtoMethodNoSideEffect(t, "isSessionReused", IsSessionReused); - env->SetProtoMethodNoSideEffect(t, "verifyError", VerifyError); - env->SetProtoMethodNoSideEffect(t, "getCipher", GetCipher); - env->SetProtoMethodNoSideEffect(t, "getSharedSigalgs", GetSharedSigalgs); - env->SetProtoMethodNoSideEffect( - t, "exportKeyingMaterial", ExportKeyingMaterial); - env->SetProtoMethod(t, "endParser", EndParser); - env->SetProtoMethod(t, "certCbDone", CertCbDone); - env->SetProtoMethod(t, "renegotiate", Renegotiate); - env->SetProtoMethodNoSideEffect(t, "getTLSTicket", GetTLSTicket); - env->SetProtoMethod(t, "newSessionDone", NewSessionDone); - env->SetProtoMethod(t, "setOCSPResponse", SetOCSPResponse); - env->SetProtoMethod(t, "requestOCSP", RequestOCSP); - env->SetProtoMethodNoSideEffect(t, "getEphemeralKeyInfo", - GetEphemeralKeyInfo); - env->SetProtoMethodNoSideEffect(t, "getProtocol", GetProtocol); - - env->SetProtoMethod(t, "setMaxSendFragment", SetMaxSendFragment); - - env->SetProtoMethodNoSideEffect(t, "getALPNNegotiatedProtocol", - GetALPNNegotiatedProto); - env->SetProtoMethod(t, "setALPNProtocols", SetALPNProtocols); -} - - -template -void SSLWrap::ConfigureSecureContext(SecureContext* sc) { - // OCSP stapling - SSL_CTX_set_tlsext_status_cb(sc->ctx_.get(), TLSExtStatusCallback); - SSL_CTX_set_tlsext_status_arg(sc->ctx_.get(), nullptr); -} - - -template -SSL_SESSION* SSLWrap::GetSessionCallback(SSL* s, - const unsigned char* key, - int len, - int* copy) { - Base* w = static_cast(SSL_get_app_data(s)); - - *copy = 0; - return w->next_sess_.release(); -} - - -template -int SSLWrap::NewSessionCallback(SSL* s, SSL_SESSION* sess) { - Base* w = static_cast(SSL_get_app_data(s)); - Environment* env = w->ssl_env(); - HandleScope handle_scope(env->isolate()); - Context::Scope context_scope(env->context()); - - if (!w->session_callbacks_) - return 0; - - // Check if session is small enough to be stored - int size = i2d_SSL_SESSION(sess, nullptr); - if (size > SecureContext::kMaxSessionSize) - return 0; - - // Serialize session - Local session = Buffer::New(env, size).ToLocalChecked(); - unsigned char* session_data = reinterpret_cast( - Buffer::Data(session)); - memset(session_data, 0, size); - i2d_SSL_SESSION(sess, &session_data); - - unsigned int session_id_length; - const unsigned char* session_id_data = SSL_SESSION_get_id(sess, - &session_id_length); - Local session_id = Buffer::Copy( - env, - reinterpret_cast(session_id_data), - session_id_length).ToLocalChecked(); - Local argv[] = { session_id, session }; - // On servers, we pause the handshake until callback of 'newSession', which - // calls NewSessionDoneCb(). On clients, there is no callback to wait for. - if (w->is_server()) - w->awaiting_new_session_ = true; - w->MakeCallback(env->onnewsession_string(), arraysize(argv), argv); - - return 0; -} - - -template -void SSLWrap::KeylogCallback(const SSL* s, const char* line) { - Base* w = static_cast(SSL_get_app_data(s)); - Environment* env = w->ssl_env(); - HandleScope handle_scope(env->isolate()); - Context::Scope context_scope(env->context()); - - const size_t size = strlen(line); - Local line_bf = Buffer::Copy(env, line, 1 + size).ToLocalChecked(); - char* data = Buffer::Data(line_bf); - data[size] = '\n'; - w->MakeCallback(env->onkeylog_string(), 1, &line_bf); -} - - -template -void SSLWrap::OnClientHello(void* arg, - const ClientHelloParser::ClientHello& hello) { - Base* w = static_cast(arg); - Environment* env = w->ssl_env(); - HandleScope handle_scope(env->isolate()); - Local context = env->context(); - Context::Scope context_scope(context); - - Local hello_obj = Object::New(env->isolate()); - Local buff = Buffer::Copy( - env, - reinterpret_cast(hello.session_id()), - hello.session_size()).ToLocalChecked(); - hello_obj->Set(context, env->session_id_string(), buff).Check(); - if (hello.servername() == nullptr) { - hello_obj->Set(context, - env->servername_string(), - String::Empty(env->isolate())).Check(); - } else { - Local servername = OneByteString(env->isolate(), - hello.servername(), - hello.servername_size()); - hello_obj->Set(context, env->servername_string(), servername).Check(); - } - hello_obj->Set(context, - env->tls_ticket_string(), - Boolean::New(env->isolate(), hello.has_ticket())).Check(); - - Local argv[] = { hello_obj }; - w->MakeCallback(env->onclienthello_string(), arraysize(argv), argv); -} - -template -void SSLWrap::GetPeerCertificate( - const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->ssl_env(); - - bool abbreviated = args.Length() < 1 || !args[0]->IsTrue(); - - Local ret; - if (GetPeerCert(env, w->ssl_, abbreviated, w->is_server()).ToLocal(&ret)) - args.GetReturnValue().Set(ret); -} - - -template -void SSLWrap::GetCertificate( - const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->ssl_env(); - - Local ret; - if (GetCert(env, w->ssl_).ToLocal(&ret)) - args.GetReturnValue().Set(ret); -} - - -template -void SSLWrap::GetFinished(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - // We cannot just pass nullptr to SSL_get_finished() - // because it would further be propagated to memcpy(), - // where the standard requirements as described in ISO/IEC 9899:2011 - // sections 7.21.2.1, 7.21.1.2, and 7.1.4, would be violated. - // Thus, we use a dummy byte. - char dummy[1]; - size_t len = SSL_get_finished(w->ssl_.get(), dummy, sizeof dummy); - if (len == 0) - return; - - AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, len); - CHECK_EQ(len, SSL_get_finished(w->ssl_.get(), buf.data(), len)); - args.GetReturnValue().Set(buf.ToBuffer().ToLocalChecked()); -} - - -template -void SSLWrap::GetPeerFinished(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - // We cannot just pass nullptr to SSL_get_peer_finished() - // because it would further be propagated to memcpy(), - // where the standard requirements as described in ISO/IEC 9899:2011 - // sections 7.21.2.1, 7.21.1.2, and 7.1.4, would be violated. - // Thus, we use a dummy byte. - char dummy[1]; - size_t len = SSL_get_peer_finished(w->ssl_.get(), dummy, sizeof dummy); - if (len == 0) - return; - - AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, len); - CHECK_EQ(len, SSL_get_peer_finished(w->ssl_.get(), buf.data(), len)); - args.GetReturnValue().Set(buf.ToBuffer().ToLocalChecked()); -} - - -template -void SSLWrap::GetSession(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - SSL_SESSION* sess = SSL_get_session(w->ssl_.get()); - if (sess == nullptr) - return; - - int slen = i2d_SSL_SESSION(sess, nullptr); - if (slen <= 0) - return; // Invalid or malformed session. - - AllocatedBuffer sbuf = AllocatedBuffer::AllocateManaged(env, slen); - unsigned char* p = reinterpret_cast(sbuf.data()); - CHECK_LT(0, i2d_SSL_SESSION(sess, &p)); - args.GetReturnValue().Set(sbuf.ToBuffer().ToLocalChecked()); -} - - -template -void SSLWrap::SetSession(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - if (args.Length() < 1) - return THROW_ERR_MISSING_ARGS(env, "Session argument is mandatory"); - - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "Session"); - - SSLSessionPointer sess = GetTLSSession(args[0]); - if (sess == nullptr) - return; - - if (!SetTLSSession(w->ssl_, sess)) - return env->ThrowError("SSL_set_session error"); -} - - -template -void SSLWrap::LoadSession(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - // TODO(@sam-github) check arg length and types in js, and CHECK in c++ - if (args.Length() >= 1 && Buffer::HasInstance(args[0])) { - ArrayBufferViewContents sbuf(args[0]); - - const unsigned char* p = sbuf.data(); - SSL_SESSION* sess = d2i_SSL_SESSION(nullptr, &p, sbuf.length()); - - // Setup next session and move hello to the BIO buffer - w->next_sess_.reset(sess); - } -} - - -template -void SSLWrap::IsSessionReused(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - bool yes = SSL_session_reused(w->ssl_.get()); - args.GetReturnValue().Set(yes); -} - - -template -void SSLWrap::EndParser(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - w->hello_parser_.End(); -} - - -template -void SSLWrap::Renegotiate(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - ClearErrorOnReturn clear_error_on_return; - - if (SSL_renegotiate(w->ssl_.get()) != 1) { - return ThrowCryptoError(w->ssl_env(), ERR_get_error()); - } -} - - -template -void SSLWrap::GetTLSTicket(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->ssl_env(); - - SSL_SESSION* sess = SSL_get_session(w->ssl_.get()); - if (sess == nullptr) - return; - - const unsigned char* ticket; - size_t length; - SSL_SESSION_get0_ticket(sess, &ticket, &length); - - if (ticket == nullptr) - return; - - Local buff = Buffer::Copy( - env, reinterpret_cast(ticket), length).ToLocalChecked(); - - args.GetReturnValue().Set(buff); -} - - -template -void SSLWrap::NewSessionDone(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - w->awaiting_new_session_ = false; - w->NewSessionDoneCb(); -} - - -template -void SSLWrap::SetOCSPResponse(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->env(); - - if (args.Length() < 1) - return THROW_ERR_MISSING_ARGS(env, "OCSP response argument is mandatory"); - - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "OCSP response"); - - w->ocsp_response_.Reset(args.GetIsolate(), args[0].As()); -} - - -template -void SSLWrap::RequestOCSP(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - SSL_set_tlsext_status_type(w->ssl_.get(), TLSEXT_STATUSTYPE_ocsp); -} - - -template -void SSLWrap::GetEphemeralKeyInfo( - const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = Environment::GetCurrent(args); - - CHECK(w->ssl_); - - // tmp key is available on only client - if (w->is_server()) - return args.GetReturnValue().SetNull(); - - Local ret; - if (GetEphemeralKey(env, w->ssl_).ToLocal(&ret)) - args.GetReturnValue().Set(ret); - - // TODO(@sam-github) semver-major: else return ThrowCryptoError(env, - // ERR_get_error()) -} - -template -void SSLWrap::SetMaxSendFragment( - const FunctionCallbackInfo& args) { - CHECK(args.Length() >= 1 && args[0]->IsNumber()); - - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - int rv = SSL_set_max_send_fragment( - w->ssl_.get(), - args[0]->Int32Value(w->ssl_env()->context()).FromJust()); - args.GetReturnValue().Set(rv); -} - - -template -void SSLWrap::VerifyError(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - // XXX(bnoordhuis) The UNABLE_TO_GET_ISSUER_CERT error when there is no - // peer certificate is questionable but it's compatible with what was - // here before. - long x509_verify_error = // NOLINT(runtime/int) - VerifyPeerCertificate(w->ssl_, X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT); - - if (x509_verify_error == X509_V_OK) - return args.GetReturnValue().SetNull(); - - const char* reason = X509_verify_cert_error_string(x509_verify_error); - const char* code = reason; - code = X509ErrorCode(x509_verify_error); - - Isolate* isolate = args.GetIsolate(); - Local reason_string = OneByteString(isolate, reason); - Local exception_value = Exception::Error(reason_string); - Local exception_object = - exception_value->ToObject(isolate->GetCurrentContext()).ToLocalChecked(); - exception_object->Set(w->env()->context(), w->env()->code_string(), - OneByteString(isolate, code)).Check(); - args.GetReturnValue().Set(exception_object); -} - - -template -void SSLWrap::GetCipher(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->ssl_env(); - - const SSL_CIPHER* c = SSL_get_current_cipher(w->ssl_.get()); - if (c == nullptr) - return; - - Local ret; - if (GetCipherInfo(env, w->ssl_).ToLocal(&ret)) - args.GetReturnValue().Set(ret); -} - - -template -void SSLWrap::GetSharedSigalgs(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->ssl_env(); - - SSL* ssl = w->ssl_.get(); - int nsig = SSL_get_shared_sigalgs(ssl, 0, nullptr, nullptr, nullptr, nullptr, - nullptr); - MaybeStackBuffer, 16> ret_arr(nsig); - - for (int i = 0; i < nsig; i++) { - int hash_nid; - int sign_nid; - std::string sig_with_md; - - SSL_get_shared_sigalgs(ssl, i, &sign_nid, &hash_nid, nullptr, nullptr, - nullptr); - - switch (sign_nid) { - case EVP_PKEY_RSA: - sig_with_md = "RSA+"; - break; - - case EVP_PKEY_RSA_PSS: - sig_with_md = "RSA-PSS+"; - break; - - case EVP_PKEY_DSA: - sig_with_md = "DSA+"; - break; - - case EVP_PKEY_EC: - sig_with_md = "ECDSA+"; - break; - - case NID_ED25519: - sig_with_md = "Ed25519+"; - break; - - case NID_ED448: - sig_with_md = "Ed448+"; - break; -#ifndef OPENSSL_NO_GOST - case NID_id_GostR3410_2001: - sig_with_md = "gost2001+"; - break; - - case NID_id_GostR3410_2012_256: - sig_with_md = "gost2012_256+"; - break; - - case NID_id_GostR3410_2012_512: - sig_with_md = "gost2012_512+"; - break; -#endif // !OPENSSL_NO_GOST - default: - const char* sn = OBJ_nid2sn(sign_nid); - - if (sn != nullptr) { - sig_with_md = std::string(sn) + "+"; - } else { - sig_with_md = "UNDEF+"; - } - break; - } - - const char* sn_hash = OBJ_nid2sn(hash_nid); - if (sn_hash != nullptr) { - sig_with_md += std::string(sn_hash); - } else { - sig_with_md += "UNDEF"; - } - ret_arr[i] = OneByteString(env->isolate(), sig_with_md.c_str()); - } - - args.GetReturnValue().Set( - Array::New(env->isolate(), ret_arr.out(), ret_arr.length())); -} - -template -void SSLWrap::ExportKeyingMaterial( - const FunctionCallbackInfo& args) { - CHECK(args[0]->IsInt32()); - CHECK(args[1]->IsString()); - - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->ssl_env(); - - uint32_t olen = args[0].As()->Value(); - node::Utf8Value label(env->isolate(), args[1]); - - AllocatedBuffer out = AllocatedBuffer::AllocateManaged(env, olen); - - ByteSource context; - bool use_context = !args[2]->IsUndefined(); - if (use_context) - context = ByteSource::FromBuffer(args[2]); - - if (SSL_export_keying_material(w->ssl_.get(), - reinterpret_cast(out.data()), - olen, - *label, - label.length(), - reinterpret_cast( - context.get()), - context.size(), - use_context) != 1) { - return ThrowCryptoError(env, ERR_get_error(), "SSL_export_keying_material"); - } - - args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); -} - -template -void SSLWrap::GetProtocol(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - const char* tls_version = SSL_get_version(w->ssl_.get()); - args.GetReturnValue().Set(OneByteString(args.GetIsolate(), tls_version)); -} - - -template -int SSLWrap::SelectALPNCallback(SSL* s, - const unsigned char** out, - unsigned char* outlen, - const unsigned char* in, - unsigned int inlen, - void* arg) { - Base* w = static_cast(SSL_get_app_data(s)); - Environment* env = w->env(); - HandleScope handle_scope(env->isolate()); - Context::Scope context_scope(env->context()); - - Local alpn_buffer = - w->object()->GetPrivate( - env->context(), - env->alpn_buffer_private_symbol()).ToLocalChecked(); - ArrayBufferViewContents alpn_protos(alpn_buffer); - int status = SSL_select_next_proto(const_cast(out), outlen, - alpn_protos.data(), alpn_protos.length(), - in, inlen); - // According to 3.2. Protocol Selection of RFC7301, fatal - // no_application_protocol alert shall be sent but OpenSSL 1.0.2 does not - // support it yet. See - // https://rt.openssl.org/Ticket/Display.html?id=3463&user=guest&pass=guest - return status == OPENSSL_NPN_NEGOTIATED ? SSL_TLSEXT_ERR_OK - : SSL_TLSEXT_ERR_NOACK; -} - - -template -void SSLWrap::GetALPNNegotiatedProto( - const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - - const unsigned char* alpn_proto; - unsigned int alpn_proto_len; - - SSL_get0_alpn_selected(w->ssl_.get(), &alpn_proto, &alpn_proto_len); - - Local result; - if (alpn_proto_len == 0) { - result = False(args.GetIsolate()); - } else if (alpn_proto_len == sizeof("h2") - 1 && - 0 == memcmp(alpn_proto, "h2", sizeof("h2") - 1)) { - result = w->env()->h2_string(); - } else if (alpn_proto_len == sizeof("http/1.1") - 1 && - 0 == memcmp(alpn_proto, "http/1.1", sizeof("http/1.1") - 1)) { - result = w->env()->http_1_1_string(); - } else { - result = OneByteString(args.GetIsolate(), alpn_proto, alpn_proto_len); - } - - args.GetReturnValue().Set(result); -} - - -template -void SSLWrap::SetALPNProtocols(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->env(); - if (args.Length() < 1 || !Buffer::HasInstance(args[0])) - return env->ThrowTypeError("Must give a Buffer as first argument"); - - if (w->is_client()) { - CHECK(SetALPN(w->ssl_, args[0])); - } else { - CHECK( - w->object()->SetPrivate( - env->context(), - env->alpn_buffer_private_symbol(), - args[0]).FromJust()); - // Server should select ALPN protocol from list of advertised by client - SSL_CTX_set_alpn_select_cb(SSL_get_SSL_CTX(w->ssl_.get()), - SelectALPNCallback, - nullptr); - } -} - - -template -int SSLWrap::TLSExtStatusCallback(SSL* s, void* arg) { - Base* w = static_cast(SSL_get_app_data(s)); - Environment* env = w->env(); - HandleScope handle_scope(env->isolate()); - - if (w->is_client()) { - // Incoming response - Local arg; - MaybeLocal ret = GetSSLOCSPResponse(env, s, Null(env->isolate())); - if (ret.ToLocal(&arg)) - w->MakeCallback(env->onocspresponse_string(), 1, &arg); - - // No async acceptance is possible, so always return 1 to accept the - // response. The listener for 'OCSPResponse' event has no control over - // return value, but it can .destroy() the connection if the response is not - // acceptable. - return 1; - } else { - // Outgoing response - if (w->ocsp_response_.IsEmpty()) - return SSL_TLSEXT_ERR_NOACK; - - Local obj = PersistentToLocal::Default(env->isolate(), - w->ocsp_response_); - size_t len = obj->ByteLength(); - - // OpenSSL takes control of the pointer after accepting it - unsigned char* data = MallocOpenSSL(len); - obj->CopyContents(data, len); - - if (!SSL_set_tlsext_status_ocsp_resp(s, data, len)) - OPENSSL_free(data); - w->ocsp_response_.Reset(); - - return SSL_TLSEXT_ERR_OK; - } -} - - -template -void SSLWrap::WaitForCertCb(CertCb cb, void* arg) { - cert_cb_ = cb; - cert_cb_arg_ = arg; -} - - -template -int SSLWrap::SSLCertCallback(SSL* s, void* arg) { - Base* w = static_cast(SSL_get_app_data(s)); - - if (!w->is_server()) - return 1; - - if (!w->is_waiting_cert_cb()) - return 1; - - if (w->cert_cb_running_) - // Not an error. Suspend handshake with SSL_ERROR_WANT_X509_LOOKUP, and - // handshake will continue after certcb is done. - return -1; - - Environment* env = w->env(); - Local context = env->context(); - HandleScope handle_scope(env->isolate()); - Context::Scope context_scope(context); - w->cert_cb_running_ = true; - - Local info = Object::New(env->isolate()); - - const char* servername = GetServerName(s); - if (servername == nullptr) { - info->Set(context, - env->servername_string(), - String::Empty(env->isolate())).Check(); - } else { - Local str = OneByteString(env->isolate(), servername, - strlen(servername)); - info->Set(context, env->servername_string(), str).Check(); - } - - const bool ocsp = (SSL_get_tlsext_status_type(s) == TLSEXT_STATUSTYPE_ocsp); - info->Set(context, env->ocsp_request_string(), - Boolean::New(env->isolate(), ocsp)).Check(); - - Local argv[] = { info }; - w->MakeCallback(env->oncertcb_string(), arraysize(argv), argv); - - if (!w->cert_cb_running_) - return 1; - - // Performing async action, wait... - return -1; -} - - -template -void SSLWrap::CertCbDone(const FunctionCallbackInfo& args) { - Base* w; - ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); - Environment* env = w->env(); - - CHECK(w->is_waiting_cert_cb() && w->cert_cb_running_); - - Local object = w->object(); - Local ctx = object->Get(env->context(), - env->sni_context_string()).ToLocalChecked(); - Local cons = env->secure_context_constructor_template(); - - if (cons->HasInstance(ctx)) { - SecureContext* sc = Unwrap(ctx.As()); - CHECK_NOT_NULL(sc); - // Store the SNI context for later use. - w->sni_context_ = BaseObjectPtr(sc); - - if (UseSNIContext(w->ssl_, w->sni_context_) && !w->SetCACerts(sc)) { - // Not clear why sometimes we throw error, and sometimes we call - // onerror(). Both cause .destroy(), but onerror does a bit more. - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - return ThrowCryptoError(env, err, "CertCbDone"); - } - } else if (ctx->IsObject()) { - // Failure: incorrect SNI context object - Local err = Exception::TypeError(env->sni_context_err_string()); - w->MakeCallback(env->onerror_string(), 1, &err); - return; - } - - CertCb cb; - void* arg; - - cb = w->cert_cb_; - arg = w->cert_cb_arg_; - - w->cert_cb_running_ = false; - w->cert_cb_ = nullptr; - w->cert_cb_arg_ = nullptr; - - cb(arg); -} - - -template -void SSLWrap::DestroySSL() { - if (!ssl_) - return; - - env_->isolate()->AdjustAmountOfExternalAllocatedMemory(-kExternalSize); - ssl_.reset(); -} - - -template -int SSLWrap::SetCACerts(SecureContext* sc) { - int err = SSL_set1_verify_cert_store(ssl_.get(), - SSL_CTX_get_cert_store(sc->ctx_.get())); - if (err != 1) - return err; - - STACK_OF(X509_NAME)* list = SSL_dup_CA_list( - SSL_CTX_get_client_CA_list(sc->ctx_.get())); - - // NOTE: `SSL_set_client_CA_list` takes the ownership of `list` - SSL_set_client_CA_list(ssl_.get(), list); - return 1; -} - -template -void SSLWrap::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackField("ocsp_response", ocsp_response_); - tracker->TrackField("sni_context", sni_context_); -} - -int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx) { - // From https://www.openssl.org/docs/man1.1.1/man3/SSL_verify_cb: - // - // If VerifyCallback returns 1, the verification process is continued. If - // VerifyCallback always returns 1, the TLS/SSL handshake will not be - // terminated with respect to verification failures and the connection will - // be established. The calling process can however retrieve the error code - // of the last verification error using SSL_get_verify_result(3) or by - // maintaining its own error storage managed by VerifyCallback. - // - // Since we cannot perform I/O quickly enough with X509_STORE_CTX_ APIs in - // this callback, we ignore all preverify_ok errors and let the handshake - // continue. It is imperative that the user use Connection::VerifyError after - // the 'secure' callback has been made. - return 1; -} - -static bool IsSupportedAuthenticatedMode(const EVP_CIPHER* cipher) { - const int mode = EVP_CIPHER_mode(cipher); - // Check `chacha20-poly1305` separately, it is also an AEAD cipher, - // but its mode is 0 which doesn't indicate - return EVP_CIPHER_nid(cipher) == NID_chacha20_poly1305 || - mode == EVP_CIPH_CCM_MODE || - mode == EVP_CIPH_GCM_MODE || - IS_OCB_MODE(mode); -} - -static bool IsSupportedAuthenticatedMode(const EVP_CIPHER_CTX* ctx) { - const EVP_CIPHER* cipher = EVP_CIPHER_CTX_cipher(ctx); - return IsSupportedAuthenticatedMode(cipher); -} - -enum class ParseKeyResult { - kParseKeyOk, - kParseKeyNotRecognized, - kParseKeyNeedPassphrase, - kParseKeyFailed -}; - -static ParseKeyResult TryParsePublicKey( - EVPKeyPointer* pkey, - const BIOPointer& bp, - const char* name, - // NOLINTNEXTLINE(runtime/int) - const std::function& parse) { - unsigned char* der_data; - long der_len; // NOLINT(runtime/int) - - // This skips surrounding data and decodes PEM to DER. - { - MarkPopErrorOnReturn mark_pop_error_on_return; - if (PEM_bytes_read_bio(&der_data, &der_len, nullptr, name, - bp.get(), nullptr, nullptr) != 1) - return ParseKeyResult::kParseKeyNotRecognized; - } - - // OpenSSL might modify the pointer, so we need to make a copy before parsing. - const unsigned char* p = der_data; - pkey->reset(parse(&p, der_len)); - OPENSSL_clear_free(der_data, der_len); - - return *pkey ? ParseKeyResult::kParseKeyOk : - ParseKeyResult::kParseKeyFailed; -} - -static ParseKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey, - const char* key_pem, - int key_pem_len) { - BIOPointer bp(BIO_new_mem_buf(const_cast(key_pem), key_pem_len)); - if (!bp) - return ParseKeyResult::kParseKeyFailed; - - ParseKeyResult ret; - - // Try parsing as a SubjectPublicKeyInfo first. - ret = TryParsePublicKey(pkey, bp, "PUBLIC KEY", - [](const unsigned char** p, long l) { // NOLINT(runtime/int) - return d2i_PUBKEY(nullptr, p, l); - }); - if (ret != ParseKeyResult::kParseKeyNotRecognized) - return ret; - - // Maybe it is PKCS#1. - CHECK(BIO_reset(bp.get())); - ret = TryParsePublicKey(pkey, bp, "RSA PUBLIC KEY", - [](const unsigned char** p, long l) { // NOLINT(runtime/int) - return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l); - }); - if (ret != ParseKeyResult::kParseKeyNotRecognized) - return ret; - - // X.509 fallback. - CHECK(BIO_reset(bp.get())); - return TryParsePublicKey(pkey, bp, "CERTIFICATE", - [](const unsigned char** p, long l) { // NOLINT(runtime/int) - X509Pointer x509(d2i_X509(nullptr, p, l)); - return x509 ? X509_get_pubkey(x509.get()) : nullptr; - }); -} - -static ParseKeyResult ParsePublicKey(EVPKeyPointer* pkey, - const PublicKeyEncodingConfig& config, - const char* key, - size_t key_len) { - if (config.format_ == kKeyFormatPEM) { - return ParsePublicKeyPEM(pkey, key, key_len); - } else { - CHECK_EQ(config.format_, kKeyFormatDER); - - const unsigned char* p = reinterpret_cast(key); - if (config.type_.ToChecked() == kKeyEncodingPKCS1) { - pkey->reset(d2i_PublicKey(EVP_PKEY_RSA, nullptr, &p, key_len)); - } else { - CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSPKI); - pkey->reset(d2i_PUBKEY(nullptr, &p, key_len)); - } - - return *pkey ? ParseKeyResult::kParseKeyOk : - ParseKeyResult::kParseKeyFailed; - } -} - -static inline Local BIOToStringOrBuffer(Environment* env, - BIO* bio, - PKFormatType format) { - BUF_MEM* bptr; - BIO_get_mem_ptr(bio, &bptr); - if (format == kKeyFormatPEM) { - // PEM is an ASCII format, so we will return it as a string. - return String::NewFromUtf8(env->isolate(), bptr->data, - NewStringType::kNormal, - bptr->length).ToLocalChecked(); - } else { - CHECK_EQ(format, kKeyFormatDER); - // DER is binary, return it as a buffer. - return Buffer::Copy(env, bptr->data, bptr->length).ToLocalChecked(); - } -} - -static bool WritePublicKeyInner(EVP_PKEY* pkey, - const BIOPointer& bio, - const PublicKeyEncodingConfig& config) { - if (config.type_.ToChecked() == kKeyEncodingPKCS1) { - // PKCS#1 is only valid for RSA keys. - CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA); - RSAPointer rsa(EVP_PKEY_get1_RSA(pkey)); - if (config.format_ == kKeyFormatPEM) { - // Encode PKCS#1 as PEM. - return PEM_write_bio_RSAPublicKey(bio.get(), rsa.get()) == 1; - } else { - // Encode PKCS#1 as DER. - CHECK_EQ(config.format_, kKeyFormatDER); - return i2d_RSAPublicKey_bio(bio.get(), rsa.get()) == 1; - } - } else { - CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSPKI); - if (config.format_ == kKeyFormatPEM) { - // Encode SPKI as PEM. - return PEM_write_bio_PUBKEY(bio.get(), pkey) == 1; - } else { - // Encode SPKI as DER. - CHECK_EQ(config.format_, kKeyFormatDER); - return i2d_PUBKEY_bio(bio.get(), pkey) == 1; - } - } -} - -static MaybeLocal WritePublicKey(Environment* env, - EVP_PKEY* pkey, - const PublicKeyEncodingConfig& config) { - BIOPointer bio(BIO_new(BIO_s_mem())); - CHECK(bio); - - if (!WritePublicKeyInner(pkey, bio, config)) { - ThrowCryptoError(env, ERR_get_error(), "Failed to encode public key"); - return MaybeLocal(); - } - return BIOToStringOrBuffer(env, bio.get(), config.format_); -} - -static bool IsASN1Sequence(const unsigned char* data, size_t size, - size_t* data_offset, size_t* data_size) { - if (size < 2 || data[0] != 0x30) - return false; - - if (data[1] & 0x80) { - // Long form. - size_t n_bytes = data[1] & ~0x80; - if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) - return false; - size_t length = 0; - for (size_t i = 0; i < n_bytes; i++) - length = (length << 8) | data[i + 2]; - *data_offset = 2 + n_bytes; - *data_size = std::min(size - 2 - n_bytes, length); - } else { - // Short form. - *data_offset = 2; - *data_size = std::min(size - 2, data[1]); - } - - return true; -} - -static bool IsRSAPrivateKey(const unsigned char* data, size_t size) { - // Both RSAPrivateKey and RSAPublicKey structures start with a SEQUENCE. - size_t offset, len; - if (!IsASN1Sequence(data, size, &offset, &len)) - return false; - - // An RSAPrivateKey sequence always starts with a single-byte integer whose - // value is either 0 or 1, whereas an RSAPublicKey starts with the modulus - // (which is the product of two primes and therefore at least 4), so we can - // decide the type of the structure based on the first three bytes of the - // sequence. - return len >= 3 && - data[offset] == 2 && - data[offset + 1] == 1 && - !(data[offset + 2] & 0xfe); -} - -static bool IsEncryptedPrivateKeyInfo(const unsigned char* data, size_t size) { - // Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE. - size_t offset, len; - if (!IsASN1Sequence(data, size, &offset, &len)) - return false; - - // A PrivateKeyInfo sequence always starts with an integer whereas an - // EncryptedPrivateKeyInfo starts with an AlgorithmIdentifier. - return len >= 1 && - data[offset] != 2; -} - -static ParseKeyResult ParsePrivateKey(EVPKeyPointer* pkey, - const PrivateKeyEncodingConfig& config, - const char* key, - size_t key_len) { - // OpenSSL needs a non-const pointer, that's why the const_cast is required. - char* const passphrase = const_cast(config.passphrase_.get()); - - if (config.format_ == kKeyFormatPEM) { - BIOPointer bio(BIO_new_mem_buf(key, key_len)); - if (!bio) - return ParseKeyResult::kParseKeyFailed; - - pkey->reset(PEM_read_bio_PrivateKey(bio.get(), - nullptr, - PasswordCallback, - passphrase)); - } else { - CHECK_EQ(config.format_, kKeyFormatDER); - - if (config.type_.ToChecked() == kKeyEncodingPKCS1) { - const unsigned char* p = reinterpret_cast(key); - pkey->reset(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, key_len)); - } else if (config.type_.ToChecked() == kKeyEncodingPKCS8) { - BIOPointer bio(BIO_new_mem_buf(key, key_len)); - if (!bio) - return ParseKeyResult::kParseKeyFailed; - - if (IsEncryptedPrivateKeyInfo( - reinterpret_cast(key), key_len)) { - pkey->reset(d2i_PKCS8PrivateKey_bio(bio.get(), - nullptr, - PasswordCallback, - passphrase)); - } else { - PKCS8Pointer p8inf(d2i_PKCS8_PRIV_KEY_INFO_bio(bio.get(), nullptr)); - if (p8inf) - pkey->reset(EVP_PKCS82PKEY(p8inf.get())); - } - } else { - CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSEC1); - const unsigned char* p = reinterpret_cast(key); - pkey->reset(d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, key_len)); - } - } - - // OpenSSL can fail to parse the key but still return a non-null pointer. - unsigned long err = ERR_peek_error(); // NOLINT(runtime/int) - if (err != 0) - pkey->reset(); - - if (*pkey) - return ParseKeyResult::kParseKeyOk; - if (ERR_GET_LIB(err) == ERR_LIB_PEM && - ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ) { - if (config.passphrase_.get() == nullptr) - return ParseKeyResult::kParseKeyNeedPassphrase; - } - return ParseKeyResult::kParseKeyFailed; -} - -ByteSource::ByteSource(ByteSource&& other) - : data_(other.data_), - allocated_data_(other.allocated_data_), - size_(other.size_) { - other.allocated_data_ = nullptr; -} - -ByteSource::~ByteSource() { - OPENSSL_clear_free(allocated_data_, size_); -} - -ByteSource& ByteSource::operator=(ByteSource&& other) { - if (&other != this) { - OPENSSL_clear_free(allocated_data_, size_); - data_ = other.data_; - allocated_data_ = other.allocated_data_; - other.allocated_data_ = nullptr; - size_ = other.size_; - } - return *this; -} - -const char* ByteSource::get() const { - return data_; -} - -size_t ByteSource::size() const { - return size_; -} - -ByteSource ByteSource::FromStringOrBuffer(Environment* env, - Local value) { - return Buffer::HasInstance(value) ? FromBuffer(value) - : FromString(env, value.As()); -} - -ByteSource ByteSource::FromString(Environment* env, Local str, - bool ntc) { - CHECK(str->IsString()); - size_t size = str->Utf8Length(env->isolate()); - size_t alloc_size = ntc ? size + 1 : size; - char* data = MallocOpenSSL(alloc_size); - int opts = String::NO_OPTIONS; - if (!ntc) opts |= String::NO_NULL_TERMINATION; - str->WriteUtf8(env->isolate(), data, alloc_size, nullptr, opts); - return Allocated(data, size); -} - -ByteSource ByteSource::FromBuffer(Local buffer, bool ntc) { - CHECK(buffer->IsArrayBufferView()); - Local abv = buffer.As(); - size_t size = abv->ByteLength(); - if (ntc) { - char* data = MallocOpenSSL(size + 1); - abv->CopyContents(data, size); - data[size] = 0; - return Allocated(data, size); - } - return Foreign(Buffer::Data(buffer), size); -} - -ByteSource ByteSource::NullTerminatedCopy(Environment* env, - Local value) { - return Buffer::HasInstance(value) ? FromBuffer(value, true) - : FromString(env, value.As(), true); -} - -ByteSource ByteSource::FromSymmetricKeyObjectHandle(Local handle) { - CHECK(handle->IsObject()); - KeyObjectHandle* key = Unwrap(handle.As()); - CHECK_NOT_NULL(key); - return Foreign(key->Data()->GetSymmetricKey(), - key->Data()->GetSymmetricKeySize()); -} - -ByteSource::ByteSource(const char* data, char* allocated_data, size_t size) - : data_(data), - allocated_data_(allocated_data), - size_(size) {} - -ByteSource ByteSource::Allocated(char* data, size_t size) { - return ByteSource(data, data, size); -} - -ByteSource ByteSource::Foreign(const char* data, size_t size) { - return ByteSource(data, nullptr, size); -} - -enum KeyEncodingContext { - kKeyContextInput, - kKeyContextExport, - kKeyContextGenerate -}; - -static void GetKeyFormatAndTypeFromJs( - AsymmetricKeyEncodingConfig* config, - const FunctionCallbackInfo& args, - unsigned int* offset, - KeyEncodingContext context) { - // During key pair generation, it is possible not to specify a key encoding, - // which will lead to a key object being returned. - if (args[*offset]->IsUndefined()) { - CHECK_EQ(context, kKeyContextGenerate); - CHECK(args[*offset + 1]->IsUndefined()); - config->output_key_object_ = true; - } else { - config->output_key_object_ = false; - - CHECK(args[*offset]->IsInt32()); - config->format_ = static_cast( - args[*offset].As()->Value()); - - if (args[*offset + 1]->IsInt32()) { - config->type_ = Just(static_cast( - args[*offset + 1].As()->Value())); - } else { - CHECK(context == kKeyContextInput && config->format_ == kKeyFormatPEM); - CHECK(args[*offset + 1]->IsNullOrUndefined()); - config->type_ = Nothing(); - } - } - - *offset += 2; -} - -static PublicKeyEncodingConfig GetPublicKeyEncodingFromJs( - const FunctionCallbackInfo& args, - unsigned int* offset, - KeyEncodingContext context) { - PublicKeyEncodingConfig result; - GetKeyFormatAndTypeFromJs(&result, args, offset, context); - return result; -} - -static inline ManagedEVPPKey GetParsedKey(Environment* env, - EVPKeyPointer&& pkey, - ParseKeyResult ret, - const char* default_msg) { - switch (ret) { - case ParseKeyResult::kParseKeyOk: - CHECK(pkey); - break; - case ParseKeyResult::kParseKeyNeedPassphrase: - THROW_ERR_MISSING_PASSPHRASE(env, - "Passphrase required for encrypted key"); - break; - default: - ThrowCryptoError(env, ERR_get_error(), default_msg); - } - - return ManagedEVPPKey(std::move(pkey)); -} - -static NonCopyableMaybe GetPrivateKeyEncodingFromJs( - const FunctionCallbackInfo& args, - unsigned int* offset, - KeyEncodingContext context) { - Environment* env = Environment::GetCurrent(args); - - PrivateKeyEncodingConfig result; - GetKeyFormatAndTypeFromJs(&result, args, offset, context); - - if (result.output_key_object_) { - if (context != kKeyContextInput) - (*offset)++; - } else { - bool needs_passphrase = false; - if (context != kKeyContextInput) { - if (args[*offset]->IsString()) { - String::Utf8Value cipher_name(env->isolate(), - args[*offset].As()); - result.cipher_ = EVP_get_cipherbyname(*cipher_name); - if (result.cipher_ == nullptr) { - THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env); - return NonCopyableMaybe(); - } - needs_passphrase = true; - } else { - CHECK(args[*offset]->IsNullOrUndefined()); - result.cipher_ = nullptr; - } - (*offset)++; - } - - if (args[*offset]->IsString() || Buffer::HasInstance(args[*offset])) { - CHECK_IMPLIES(context != kKeyContextInput, result.cipher_ != nullptr); - - result.passphrase_ = ByteSource::NullTerminatedCopy(env, args[*offset]); - } else { - CHECK(args[*offset]->IsNullOrUndefined() && !needs_passphrase); - } - } - - (*offset)++; - return NonCopyableMaybe(std::move(result)); -} - -static ManagedEVPPKey GetPrivateKeyFromJs( - const FunctionCallbackInfo& args, - unsigned int* offset, - bool allow_key_object) { - if (args[*offset]->IsString() || Buffer::HasInstance(args[*offset])) { - Environment* env = Environment::GetCurrent(args); - ByteSource key = ByteSource::FromStringOrBuffer(env, args[(*offset)++]); - NonCopyableMaybe config = - GetPrivateKeyEncodingFromJs(args, offset, kKeyContextInput); - if (config.IsEmpty()) - return ManagedEVPPKey(); - - EVPKeyPointer pkey; - ParseKeyResult ret = - ParsePrivateKey(&pkey, config.Release(), key.get(), key.size()); - return GetParsedKey(env, std::move(pkey), ret, - "Failed to read private key"); - } else { - CHECK(args[*offset]->IsObject() && allow_key_object); - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args[*offset].As(), ManagedEVPPKey()); - CHECK_EQ(key->Data()->GetKeyType(), kKeyTypePrivate); - (*offset) += 4; - return key->Data()->GetAsymmetricKey(); - } -} - -static ManagedEVPPKey GetPublicOrPrivateKeyFromJs( - const FunctionCallbackInfo& args, - unsigned int* offset) { - if (args[*offset]->IsString() || Buffer::HasInstance(args[*offset])) { - Environment* env = Environment::GetCurrent(args); - ByteSource data = ByteSource::FromStringOrBuffer(env, args[(*offset)++]); - NonCopyableMaybe config_ = - GetPrivateKeyEncodingFromJs(args, offset, kKeyContextInput); - if (config_.IsEmpty()) - return ManagedEVPPKey(); - - ParseKeyResult ret; - PrivateKeyEncodingConfig config = config_.Release(); - EVPKeyPointer pkey; - if (config.format_ == kKeyFormatPEM) { - // For PEM, we can easily determine whether it is a public or private key - // by looking for the respective PEM tags. - ret = ParsePublicKeyPEM(&pkey, data.get(), data.size()); - if (ret == ParseKeyResult::kParseKeyNotRecognized) { - ret = ParsePrivateKey(&pkey, config, data.get(), data.size()); - } - } else { - // For DER, the type determines how to parse it. SPKI, PKCS#8 and SEC1 are - // easy, but PKCS#1 can be a public key or a private key. - bool is_public; - switch (config.type_.ToChecked()) { - case kKeyEncodingPKCS1: - is_public = !IsRSAPrivateKey( - reinterpret_cast(data.get()), data.size()); - break; - case kKeyEncodingSPKI: - is_public = true; - break; - case kKeyEncodingPKCS8: - case kKeyEncodingSEC1: - is_public = false; - break; - default: - UNREACHABLE("Invalid key encoding type"); - } - - if (is_public) { - ret = ParsePublicKey(&pkey, config, data.get(), data.size()); - } else { - ret = ParsePrivateKey(&pkey, config, data.get(), data.size()); - } - } - - return GetParsedKey(env, std::move(pkey), ret, - "Failed to read asymmetric key"); - } else { - CHECK(args[*offset]->IsObject()); - KeyObjectHandle* key = Unwrap(args[*offset].As()); - CHECK_NOT_NULL(key); - CHECK_NE(key->Data()->GetKeyType(), kKeyTypeSecret); - (*offset) += 4; - return key->Data()->GetAsymmetricKey(); - } -} - -static MaybeLocal WritePrivateKey( - Environment* env, - EVP_PKEY* pkey, - const PrivateKeyEncodingConfig& config) { - BIOPointer bio(BIO_new(BIO_s_mem())); - CHECK(bio); - - bool err; - - PKEncodingType encoding_type = config.type_.ToChecked(); - if (encoding_type == kKeyEncodingPKCS1) { - // PKCS#1 is only permitted for RSA keys. - CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA); - - RSAPointer rsa(EVP_PKEY_get1_RSA(pkey)); - if (config.format_ == kKeyFormatPEM) { - // Encode PKCS#1 as PEM. - const char* pass = config.passphrase_.get(); - err = PEM_write_bio_RSAPrivateKey( - bio.get(), rsa.get(), - config.cipher_, - reinterpret_cast(const_cast(pass)), - config.passphrase_.size(), - nullptr, nullptr) != 1; - } else { - // Encode PKCS#1 as DER. This does not permit encryption. - CHECK_EQ(config.format_, kKeyFormatDER); - CHECK_NULL(config.cipher_); - err = i2d_RSAPrivateKey_bio(bio.get(), rsa.get()) != 1; - } - } else if (encoding_type == kKeyEncodingPKCS8) { - if (config.format_ == kKeyFormatPEM) { - // Encode PKCS#8 as PEM. - err = PEM_write_bio_PKCS8PrivateKey( - bio.get(), pkey, - config.cipher_, - const_cast(config.passphrase_.get()), - config.passphrase_.size(), - nullptr, nullptr) != 1; - } else { - // Encode PKCS#8 as DER. - CHECK_EQ(config.format_, kKeyFormatDER); - err = i2d_PKCS8PrivateKey_bio( - bio.get(), pkey, - config.cipher_, - const_cast(config.passphrase_.get()), - config.passphrase_.size(), - nullptr, nullptr) != 1; - } - } else { - CHECK_EQ(encoding_type, kKeyEncodingSEC1); - - // SEC1 is only permitted for EC keys. - CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_EC); - - ECKeyPointer ec_key(EVP_PKEY_get1_EC_KEY(pkey)); - if (config.format_ == kKeyFormatPEM) { - // Encode SEC1 as PEM. - const char* pass = config.passphrase_.get(); - err = PEM_write_bio_ECPrivateKey( - bio.get(), ec_key.get(), - config.cipher_, - reinterpret_cast(const_cast(pass)), - config.passphrase_.size(), - nullptr, nullptr) != 1; - } else { - // Encode SEC1 as DER. This does not permit encryption. - CHECK_EQ(config.format_, kKeyFormatDER); - CHECK_NULL(config.cipher_); - err = i2d_ECPrivateKey_bio(bio.get(), ec_key.get()) != 1; - } - } - - if (err) { - ThrowCryptoError(env, ERR_get_error(), "Failed to encode private key"); - return MaybeLocal(); - } - return BIOToStringOrBuffer(env, bio.get(), config.format_); -} - -ManagedEVPPKey::ManagedEVPPKey(EVPKeyPointer&& pkey) : pkey_(std::move(pkey)) {} - -ManagedEVPPKey::ManagedEVPPKey(const ManagedEVPPKey& that) { - *this = that; -} - -ManagedEVPPKey& ManagedEVPPKey::operator=(const ManagedEVPPKey& that) { - pkey_.reset(that.get()); - - if (pkey_) - EVP_PKEY_up_ref(pkey_.get()); - - return *this; -} - -ManagedEVPPKey::operator bool() const { - return !!pkey_; -} - -EVP_PKEY* ManagedEVPPKey::get() const { - return pkey_.get(); -} - -std::shared_ptr KeyObjectData::CreateSecret( - Local abv) { - size_t key_len = abv->ByteLength(); - char* mem = MallocOpenSSL(key_len); - abv->CopyContents(mem, key_len); - return std::shared_ptr(new KeyObjectData( - std::unique_ptr>(mem, - [key_len](char* p) { - OPENSSL_clear_free(p, key_len); - }), - key_len)); -} - -std::shared_ptr KeyObjectData::CreateAsymmetric( - KeyType key_type, - const ManagedEVPPKey& pkey) { - CHECK(pkey); - return std::shared_ptr(new KeyObjectData(key_type, pkey)); -} - -KeyType KeyObjectData::GetKeyType() const { - return key_type_; -} - -ManagedEVPPKey KeyObjectData::GetAsymmetricKey() const { - CHECK_NE(key_type_, kKeyTypeSecret); - return asymmetric_key_; -} - -const char* KeyObjectData::GetSymmetricKey() const { - CHECK_EQ(key_type_, kKeyTypeSecret); - return symmetric_key_.get(); -} - -size_t KeyObjectData::GetSymmetricKeySize() const { - CHECK_EQ(key_type_, kKeyTypeSecret); - return symmetric_key_len_; -} - -Local KeyObjectHandle::Initialize(Environment* env) { - Local templ = env->crypto_key_object_handle_constructor(); - if (!templ.IsEmpty()) { - return templ; - } - Local t = env->NewFunctionTemplate(New); - t->InstanceTemplate()->SetInternalFieldCount( - KeyObjectHandle::kInternalFieldCount); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - - env->SetProtoMethod(t, "init", Init); - env->SetProtoMethodNoSideEffect(t, "getSymmetricKeySize", - GetSymmetricKeySize); - env->SetProtoMethodNoSideEffect(t, "getAsymmetricKeyType", - GetAsymmetricKeyType); - env->SetProtoMethod(t, "export", Export); - - auto function = t->GetFunction(env->context()).ToLocalChecked(); - env->set_crypto_key_object_handle_constructor(function); - return KeyObjectHandle::Initialize(env); -} - -MaybeLocal KeyObjectHandle::Create( - Environment* env, - std::shared_ptr data) { - Local obj; - Local fctun = KeyObjectHandle::Initialize(env); - if (!fctun->NewInstance(env->context(), 0, nullptr).ToLocal(&obj)) { - return MaybeLocal(); - } - - KeyObjectHandle* key = Unwrap(obj); - CHECK_NOT_NULL(key); - key->data_ = data; - return obj; -} - -const std::shared_ptr& KeyObjectHandle::Data() { - return data_; -} - -void KeyObjectHandle::New(const FunctionCallbackInfo& args) { - CHECK(args.IsConstructCall()); - Environment* env = Environment::GetCurrent(args); - new KeyObjectHandle(env, args.This()); -} - -KeyObjectHandle::KeyObjectHandle(Environment* env, - Local wrap) - : BaseObject(env, wrap) { - MakeWeak(); -} - -void KeyObjectHandle::Init(const FunctionCallbackInfo& args) { - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); - MarkPopErrorOnReturn mark_pop_error_on_return; - - CHECK(args[0]->IsInt32()); - KeyType type = static_cast(args[0].As()->Value()); - - unsigned int offset; - ManagedEVPPKey pkey; - - switch (type) { - case kKeyTypeSecret: - CHECK_EQ(args.Length(), 2); - CHECK(args[1]->IsArrayBufferView()); - key->data_ = KeyObjectData::CreateSecret(args[1].As()); - break; - case kKeyTypePublic: - CHECK_EQ(args.Length(), 4); - - offset = 1; - pkey = GetPublicOrPrivateKeyFromJs(args, &offset); - if (!pkey) - return; - key->data_ = KeyObjectData::CreateAsymmetric(type, pkey); - break; - case kKeyTypePrivate: - CHECK_EQ(args.Length(), 5); - - offset = 1; - pkey = GetPrivateKeyFromJs(args, &offset, false); - if (!pkey) - return; - key->data_ = KeyObjectData::CreateAsymmetric(type, pkey); - break; - default: - CHECK(false); - } -} - -Local KeyObjectHandle::GetAsymmetricKeyType() const { - const ManagedEVPPKey& key = data_->GetAsymmetricKey(); - switch (EVP_PKEY_id(key.get())) { - case EVP_PKEY_RSA: - return env()->crypto_rsa_string(); - case EVP_PKEY_RSA_PSS: - return env()->crypto_rsa_pss_string(); - case EVP_PKEY_DSA: - return env()->crypto_dsa_string(); - case EVP_PKEY_DH: - return env()->crypto_dh_string(); - case EVP_PKEY_EC: - return env()->crypto_ec_string(); - case EVP_PKEY_ED25519: - return env()->crypto_ed25519_string(); - case EVP_PKEY_ED448: - return env()->crypto_ed448_string(); - case EVP_PKEY_X25519: - return env()->crypto_x25519_string(); - case EVP_PKEY_X448: - return env()->crypto_x448_string(); - default: - return Undefined(env()->isolate()); - } -} - -void KeyObjectHandle::GetAsymmetricKeyType( - const FunctionCallbackInfo& args) { - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); - - args.GetReturnValue().Set(key->GetAsymmetricKeyType()); -} - -void KeyObjectHandle::GetSymmetricKeySize( - const FunctionCallbackInfo& args) { - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); - args.GetReturnValue().Set( - static_cast(key->Data()->GetSymmetricKeySize())); -} - -void KeyObjectHandle::Export(const FunctionCallbackInfo& args) { - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder()); - - KeyType type = key->Data()->GetKeyType(); - - MaybeLocal result; - if (type == kKeyTypeSecret) { - result = key->ExportSecretKey(); - } else if (type == kKeyTypePublic) { - unsigned int offset = 0; - PublicKeyEncodingConfig config = - GetPublicKeyEncodingFromJs(args, &offset, kKeyContextExport); - CHECK_EQ(offset, static_cast(args.Length())); - result = key->ExportPublicKey(config); - } else { - CHECK_EQ(type, kKeyTypePrivate); - unsigned int offset = 0; - NonCopyableMaybe config = - GetPrivateKeyEncodingFromJs(args, &offset, kKeyContextExport); - if (config.IsEmpty()) - return; - CHECK_EQ(offset, static_cast(args.Length())); - result = key->ExportPrivateKey(config.Release()); - } - - if (!result.IsEmpty()) - args.GetReturnValue().Set(result.ToLocalChecked()); -} - -Local KeyObjectHandle::ExportSecretKey() const { - const char* buf = data_->GetSymmetricKey(); - unsigned int len = data_->GetSymmetricKeySize(); - return Buffer::Copy(env(), buf, len).ToLocalChecked(); -} - -MaybeLocal KeyObjectHandle::ExportPublicKey( - const PublicKeyEncodingConfig& config) const { - return WritePublicKey(env(), data_->GetAsymmetricKey().get(), config); -} - -MaybeLocal KeyObjectHandle::ExportPrivateKey( - const PrivateKeyEncodingConfig& config) const { - return WritePrivateKey(env(), data_->GetAsymmetricKey().get(), config); -} - -void NativeKeyObject::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsObject()); - KeyObjectHandle* handle = Unwrap(args[0].As()); - new NativeKeyObject(env, args.This(), handle->Data()); -} - -BaseObjectPtr NativeKeyObject::KeyObjectTransferData::Deserialize( - Environment* env, - Local context, - std::unique_ptr self) { - if (context != env->context()) { - THROW_ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE(env); - return {}; - } - - Local handle = KeyObjectHandle::Create(env, data_).ToLocalChecked(); - Local key_ctor; - Local arg = FIXED_ONE_BYTE_STRING(env->isolate(), - "internal/crypto/keys"); - if (env->native_module_require()-> - Call(context, Null(env->isolate()), 1, &arg).IsEmpty()) - return {}; - switch (data_->GetKeyType()) { - case kKeyTypeSecret: - key_ctor = env->crypto_key_object_secret_constructor(); - break; - case kKeyTypePublic: - key_ctor = env->crypto_key_object_public_constructor(); - break; - case kKeyTypePrivate: - key_ctor = env->crypto_key_object_private_constructor(); - break; - default: - CHECK(false); - } - - Local key = - key_ctor->NewInstance(context, 1, &handle).ToLocalChecked(); - return BaseObjectPtr(Unwrap(key.As())); -} - -BaseObject::TransferMode NativeKeyObject::GetTransferMode() const { - return BaseObject::TransferMode::kCloneable; -} - -std::unique_ptr NativeKeyObject::CloneForMessaging() - const { - return std::make_unique(handle_data_); -} - -static void CreateNativeKeyObjectClass( - const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - CHECK_EQ(args.Length(), 1); - Local callback = args[0]; - CHECK(callback->IsFunction()); - - Local t = env->NewFunctionTemplate(NativeKeyObject::New); - t->InstanceTemplate()->SetInternalFieldCount( - KeyObjectHandle::kInternalFieldCount); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - - Local ctor = t->GetFunction(env->context()).ToLocalChecked(); - - Local recv = Undefined(env->isolate()); - Local ret_v; - if (!callback.As()->Call( - env->context(), recv, 1, &ctor).ToLocal(&ret_v)) { - return; - } - Local ret = ret_v.As(); - if (!ret->Get(env->context(), 1).ToLocal(&ctor)) return; - env->set_crypto_key_object_secret_constructor(ctor.As()); - if (!ret->Get(env->context(), 2).ToLocal(&ctor)) return; - env->set_crypto_key_object_public_constructor(ctor.As()); - if (!ret->Get(env->context(), 3).ToLocal(&ctor)) return; - env->set_crypto_key_object_private_constructor(ctor.As()); - args.GetReturnValue().Set(ret); -} - -CipherBase::CipherBase(Environment* env, - Local wrap, - CipherKind kind) - : BaseObject(env, wrap), - ctx_(nullptr), - kind_(kind), - auth_tag_state_(kAuthTagUnknown), - auth_tag_len_(kNoAuthTagLength), - pending_auth_failed_(false) { - MakeWeak(); -} - -void CipherBase::Initialize(Environment* env, Local target) { - Local t = env->NewFunctionTemplate(New); - - t->InstanceTemplate()->SetInternalFieldCount( - CipherBase::kInternalFieldCount); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - - env->SetProtoMethod(t, "init", Init); - env->SetProtoMethod(t, "initiv", InitIv); - env->SetProtoMethod(t, "update", Update); - env->SetProtoMethod(t, "final", Final); - env->SetProtoMethod(t, "setAutoPadding", SetAutoPadding); - env->SetProtoMethodNoSideEffect(t, "getAuthTag", GetAuthTag); - env->SetProtoMethod(t, "setAuthTag", SetAuthTag); - env->SetProtoMethod(t, "setAAD", SetAAD); - - target->Set(env->context(), - FIXED_ONE_BYTE_STRING(env->isolate(), "CipherBase"), - t->GetFunction(env->context()).ToLocalChecked()).Check(); -} - - -void CipherBase::New(const FunctionCallbackInfo& args) { - CHECK(args.IsConstructCall()); - CipherKind kind = args[0]->IsTrue() ? kCipher : kDecipher; - Environment* env = Environment::GetCurrent(args); - new CipherBase(env, args.This(), kind); -} - -void CipherBase::CommonInit(const char* cipher_type, - const EVP_CIPHER* cipher, - const unsigned char* key, - int key_len, - const unsigned char* iv, - int iv_len, - unsigned int auth_tag_len) { - CHECK(!ctx_); - ctx_.reset(EVP_CIPHER_CTX_new()); - - const int mode = EVP_CIPHER_mode(cipher); - if (mode == EVP_CIPH_WRAP_MODE) - EVP_CIPHER_CTX_set_flags(ctx_.get(), EVP_CIPHER_CTX_FLAG_WRAP_ALLOW); - - const bool encrypt = (kind_ == kCipher); - if (1 != EVP_CipherInit_ex(ctx_.get(), cipher, nullptr, - nullptr, nullptr, encrypt)) { - return ThrowCryptoError(env(), ERR_get_error(), - "Failed to initialize cipher"); - } - - if (IsSupportedAuthenticatedMode(cipher)) { - CHECK_GE(iv_len, 0); - if (!InitAuthenticated(cipher_type, iv_len, auth_tag_len)) - return; - } - - if (!EVP_CIPHER_CTX_set_key_length(ctx_.get(), key_len)) { - ctx_.reset(); - return env()->ThrowError("Invalid key length"); - } - - if (1 != EVP_CipherInit_ex(ctx_.get(), nullptr, nullptr, key, iv, encrypt)) { - return ThrowCryptoError(env(), ERR_get_error(), - "Failed to initialize cipher"); - } -} - -void CipherBase::Init(const char* cipher_type, - const char* key_buf, - int key_buf_len, - unsigned int auth_tag_len) { - HandleScope scope(env()->isolate()); - MarkPopErrorOnReturn mark_pop_error_on_return; - -#ifdef NODE_FIPS_MODE - if (FIPS_mode()) { - return env()->ThrowError( - "crypto.createCipher() is not supported in FIPS mode."); - } -#endif // NODE_FIPS_MODE - - const EVP_CIPHER* const cipher = EVP_get_cipherbyname(cipher_type); - if (cipher == nullptr) - return THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env()); - - unsigned char key[EVP_MAX_KEY_LENGTH]; - unsigned char iv[EVP_MAX_IV_LENGTH]; - - int key_len = EVP_BytesToKey(cipher, - EVP_md5(), - nullptr, - reinterpret_cast(key_buf), - key_buf_len, - 1, - key, - iv); - CHECK_NE(key_len, 0); - - const int mode = EVP_CIPHER_mode(cipher); - if (kind_ == kCipher && (mode == EVP_CIPH_CTR_MODE || - mode == EVP_CIPH_GCM_MODE || - mode == EVP_CIPH_CCM_MODE)) { - // Ignore the return value (i.e. possible exception) because we are - // not calling back into JS anyway. - ProcessEmitWarning(env(), - "Use Cipheriv for counter mode of %s", - cipher_type); - } - - CommonInit(cipher_type, cipher, key, key_len, iv, - EVP_CIPHER_iv_length(cipher), auth_tag_len); -} - - -void CipherBase::Init(const FunctionCallbackInfo& args) { - CipherBase* cipher; - ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); - - CHECK_GE(args.Length(), 3); - - const node::Utf8Value cipher_type(args.GetIsolate(), args[0]); - ArrayBufferViewContents key_buf(args[1]); - - // Don't assign to cipher->auth_tag_len_ directly; the value might not - // represent a valid length at this point. - unsigned int auth_tag_len; - if (args[2]->IsUint32()) { - auth_tag_len = args[2].As()->Value(); - } else { - CHECK(args[2]->IsInt32() && args[2].As()->Value() == -1); - auth_tag_len = kNoAuthTagLength; - } - - cipher->Init(*cipher_type, key_buf.data(), key_buf.length(), auth_tag_len); -} - -void CipherBase::InitIv(const char* cipher_type, - const unsigned char* key, - int key_len, - const unsigned char* iv, - int iv_len, - unsigned int auth_tag_len) { - HandleScope scope(env()->isolate()); - MarkPopErrorOnReturn mark_pop_error_on_return; - - const EVP_CIPHER* const cipher = EVP_get_cipherbyname(cipher_type); - if (cipher == nullptr) { - return THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env()); - } - - const int expected_iv_len = EVP_CIPHER_iv_length(cipher); - const bool is_authenticated_mode = IsSupportedAuthenticatedMode(cipher); - const bool has_iv = iv_len >= 0; - - // Throw if no IV was passed and the cipher requires an IV - if (!has_iv && expected_iv_len != 0) { - char msg[128]; - snprintf(msg, sizeof(msg), "Missing IV for cipher %s", cipher_type); - return env()->ThrowError(msg); - } - - // Throw if an IV was passed which does not match the cipher's fixed IV length - if (!is_authenticated_mode && has_iv && iv_len != expected_iv_len) { - return env()->ThrowError("Invalid IV length"); - } - - if (EVP_CIPHER_nid(cipher) == NID_chacha20_poly1305) { - CHECK(has_iv); - // Check for invalid IV lengths, since OpenSSL does not under some - // conditions: - // https://www.openssl.org/news/secadv/20190306.txt. - if (iv_len > 12) { - return env()->ThrowError("Invalid IV length"); - } - } - - CommonInit(cipher_type, cipher, key, key_len, iv, iv_len, auth_tag_len); -} - - -static ByteSource GetSecretKeyBytes(Environment* env, Local value) { - // A key can be passed as a string, buffer or KeyObject with type 'secret'. - // If it is a string, we need to convert it to a buffer. We are not doing that - // in JS to avoid creating an unprotected copy on the heap. - return value->IsString() || Buffer::HasInstance(value) ? - ByteSource::FromStringOrBuffer(env, value) : - ByteSource::FromSymmetricKeyObjectHandle(value); -} - -void CipherBase::InitIv(const FunctionCallbackInfo& args) { - CipherBase* cipher; - ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); - Environment* env = cipher->env(); - - CHECK_GE(args.Length(), 4); - - const node::Utf8Value cipher_type(env->isolate(), args[0]); - const ByteSource key = GetSecretKeyBytes(env, args[1]); - - ArrayBufferViewContents iv_buf; - ssize_t iv_len = -1; - if (!args[2]->IsNull()) { - CHECK(args[2]->IsArrayBufferView()); - iv_buf.Read(args[2].As()); - iv_len = iv_buf.length(); - } - - // Don't assign to cipher->auth_tag_len_ directly; the value might not - // represent a valid length at this point. - unsigned int auth_tag_len; - if (args[3]->IsUint32()) { - auth_tag_len = args[3].As()->Value(); - } else { - CHECK(args[3]->IsInt32() && args[3].As()->Value() == -1); - auth_tag_len = kNoAuthTagLength; - } - - cipher->InitIv(*cipher_type, - reinterpret_cast(key.get()), - key.size(), - iv_buf.data(), - static_cast(iv_len), - auth_tag_len); -} - - -static bool IsValidGCMTagLength(unsigned int tag_len) { - return tag_len == 4 || tag_len == 8 || (tag_len >= 12 && tag_len <= 16); -} - -bool CipherBase::InitAuthenticated(const char* cipher_type, int iv_len, - unsigned int auth_tag_len) { - CHECK(IsAuthenticatedMode()); - MarkPopErrorOnReturn mark_pop_error_on_return; - - if (!EVP_CIPHER_CTX_ctrl(ctx_.get(), - EVP_CTRL_AEAD_SET_IVLEN, - iv_len, - nullptr)) { - env()->ThrowError("Invalid IV length"); - return false; - } - - const int mode = EVP_CIPHER_CTX_mode(ctx_.get()); - if (mode == EVP_CIPH_GCM_MODE) { - if (auth_tag_len != kNoAuthTagLength) { - if (!IsValidGCMTagLength(auth_tag_len)) { - char msg[50]; - snprintf(msg, sizeof(msg), - "Invalid authentication tag length: %u", auth_tag_len); - env()->ThrowError(msg); - return false; - } - - // Remember the given authentication tag length for later. - auth_tag_len_ = auth_tag_len; - } - } else { - if (auth_tag_len == kNoAuthTagLength) { - char msg[128]; - snprintf(msg, sizeof(msg), "authTagLength required for %s", cipher_type); - env()->ThrowError(msg); - return false; - } - -#ifdef NODE_FIPS_MODE - // TODO(tniessen) Support CCM decryption in FIPS mode - if (mode == EVP_CIPH_CCM_MODE && kind_ == kDecipher && FIPS_mode()) { - env()->ThrowError("CCM decryption not supported in FIPS mode"); - return false; - } -#endif - - // Tell OpenSSL about the desired length. - if (!EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_SET_TAG, auth_tag_len, - nullptr)) { - env()->ThrowError("Invalid authentication tag length"); - return false; - } - - // Remember the given authentication tag length for later. - auth_tag_len_ = auth_tag_len; - - if (mode == EVP_CIPH_CCM_MODE) { - // Restrict the message length to min(INT_MAX, 2^(8*(15-iv_len))-1) bytes. - CHECK(iv_len >= 7 && iv_len <= 13); - max_message_size_ = INT_MAX; - if (iv_len == 12) max_message_size_ = 16777215; - if (iv_len == 13) max_message_size_ = 65535; - } - } - - return true; -} - - -bool CipherBase::CheckCCMMessageLength(int message_len) { - CHECK(ctx_); - CHECK(EVP_CIPHER_CTX_mode(ctx_.get()) == EVP_CIPH_CCM_MODE); - - if (message_len > max_message_size_) { - env()->ThrowError("Message exceeds maximum size"); - return false; - } - - return true; -} - - -bool CipherBase::IsAuthenticatedMode() const { - // Check if this cipher operates in an AEAD mode that we support. - CHECK(ctx_); - return IsSupportedAuthenticatedMode(ctx_.get()); -} - - -void CipherBase::GetAuthTag(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - CipherBase* cipher; - ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); - - // Only callable after Final and if encrypting. - if (cipher->ctx_ || - cipher->kind_ != kCipher || - cipher->auth_tag_len_ == kNoAuthTagLength) { - return args.GetReturnValue().SetUndefined(); - } - - Local buf = - Buffer::Copy(env, cipher->auth_tag_, cipher->auth_tag_len_) - .ToLocalChecked(); - args.GetReturnValue().Set(buf); -} - - -void CipherBase::SetAuthTag(const FunctionCallbackInfo& args) { - CipherBase* cipher; - ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); - - if (!cipher->ctx_ || - !cipher->IsAuthenticatedMode() || - cipher->kind_ != kDecipher || - cipher->auth_tag_state_ != kAuthTagUnknown) { - return args.GetReturnValue().Set(false); - } - - unsigned int tag_len = Buffer::Length(args[0]); - const int mode = EVP_CIPHER_CTX_mode(cipher->ctx_.get()); - bool is_valid; - if (mode == EVP_CIPH_GCM_MODE) { - // Restrict GCM tag lengths according to NIST 800-38d, page 9. - is_valid = (cipher->auth_tag_len_ == kNoAuthTagLength || - cipher->auth_tag_len_ == tag_len) && - IsValidGCMTagLength(tag_len); - } else { - // At this point, the tag length is already known and must match the - // length of the given authentication tag. - CHECK(IsSupportedAuthenticatedMode(cipher->ctx_.get())); - CHECK_NE(cipher->auth_tag_len_, kNoAuthTagLength); - is_valid = cipher->auth_tag_len_ == tag_len; - } - - if (!is_valid) { - char msg[50]; - snprintf(msg, sizeof(msg), - "Invalid authentication tag length: %u", tag_len); - return cipher->env()->ThrowError(msg); - } - - cipher->auth_tag_len_ = tag_len; - cipher->auth_tag_state_ = kAuthTagKnown; - CHECK_LE(cipher->auth_tag_len_, sizeof(cipher->auth_tag_)); - - memset(cipher->auth_tag_, 0, sizeof(cipher->auth_tag_)); - args[0].As()->CopyContents( - cipher->auth_tag_, cipher->auth_tag_len_); - - args.GetReturnValue().Set(true); -} - - -bool CipherBase::MaybePassAuthTagToOpenSSL() { - if (auth_tag_state_ == kAuthTagKnown) { - if (!EVP_CIPHER_CTX_ctrl(ctx_.get(), - EVP_CTRL_AEAD_SET_TAG, - auth_tag_len_, - reinterpret_cast(auth_tag_))) { - return false; - } - auth_tag_state_ = kAuthTagPassedToOpenSSL; - } - return true; -} - - -bool CipherBase::SetAAD(const char* data, unsigned int len, int plaintext_len) { - if (!ctx_ || !IsAuthenticatedMode()) - return false; - MarkPopErrorOnReturn mark_pop_error_on_return; - - int outlen; - const int mode = EVP_CIPHER_CTX_mode(ctx_.get()); - - // When in CCM mode, we need to set the authentication tag and the plaintext - // length in advance. - if (mode == EVP_CIPH_CCM_MODE) { - if (plaintext_len < 0) { - env()->ThrowError("plaintextLength required for CCM mode with AAD"); - return false; - } - - if (!CheckCCMMessageLength(plaintext_len)) - return false; - - if (kind_ == kDecipher) { - if (!MaybePassAuthTagToOpenSSL()) - return false; - } - - // Specify the plaintext length. - if (!EVP_CipherUpdate(ctx_.get(), nullptr, &outlen, nullptr, plaintext_len)) - return false; - } - - return 1 == EVP_CipherUpdate(ctx_.get(), - nullptr, - &outlen, - reinterpret_cast(data), - len); -} - - -void CipherBase::SetAAD(const FunctionCallbackInfo& args) { - CipherBase* cipher; - ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); - - CHECK_EQ(args.Length(), 2); - CHECK(args[1]->IsInt32()); - int plaintext_len = args[1].As()->Value(); - ArrayBufferViewContents buf(args[0]); - - bool b = cipher->SetAAD(buf.data(), buf.length(), plaintext_len); - args.GetReturnValue().Set(b); // Possibly report invalid state failure -} - -CipherBase::UpdateResult CipherBase::Update(const char* data, - int len, - AllocatedBuffer* out) { - if (!ctx_) - return kErrorState; - MarkPopErrorOnReturn mark_pop_error_on_return; - - const int mode = EVP_CIPHER_CTX_mode(ctx_.get()); - - if (mode == EVP_CIPH_CCM_MODE) { - if (!CheckCCMMessageLength(len)) - return kErrorMessageSize; - } - - // Pass the authentication tag to OpenSSL if possible. This will only happen - // once, usually on the first update. - if (kind_ == kDecipher && IsAuthenticatedMode()) { - CHECK(MaybePassAuthTagToOpenSSL()); - } - - int buf_len = len + EVP_CIPHER_CTX_block_size(ctx_.get()); - // For key wrapping algorithms, get output size by calling - // EVP_CipherUpdate() with null output. - if (kind_ == kCipher && mode == EVP_CIPH_WRAP_MODE && - EVP_CipherUpdate(ctx_.get(), - nullptr, - &buf_len, - reinterpret_cast(data), - len) != 1) { - return kErrorState; - } - - *out = AllocatedBuffer::AllocateManaged(env(), buf_len); - int r = EVP_CipherUpdate(ctx_.get(), - reinterpret_cast(out->data()), - &buf_len, - reinterpret_cast(data), - len); - - CHECK_LE(static_cast(buf_len), out->size()); - out->Resize(buf_len); - - // When in CCM mode, EVP_CipherUpdate will fail if the authentication tag is - // invalid. In that case, remember the error and throw in final(). - if (!r && kind_ == kDecipher && mode == EVP_CIPH_CCM_MODE) { - pending_auth_failed_ = true; - return kSuccess; - } - return r == 1 ? kSuccess : kErrorState; -} - - -void CipherBase::Update(const FunctionCallbackInfo& args) { - Decode(args, [](CipherBase* cipher, - const FunctionCallbackInfo& args, - const char* data, size_t size) { - AllocatedBuffer out; - UpdateResult r = cipher->Update(data, size, &out); - - if (r != kSuccess) { - if (r == kErrorState) { - Environment* env = Environment::GetCurrent(args); - ThrowCryptoError(env, ERR_get_error(), - "Trying to add data in unsupported state"); - } - return; - } - - CHECK(out.data() != nullptr || out.size() == 0); - args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); - }); -} - - -bool CipherBase::SetAutoPadding(bool auto_padding) { - if (!ctx_) - return false; - MarkPopErrorOnReturn mark_pop_error_on_return; - return EVP_CIPHER_CTX_set_padding(ctx_.get(), auto_padding); -} - - -void CipherBase::SetAutoPadding(const FunctionCallbackInfo& args) { - CipherBase* cipher; - ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); - - bool b = cipher->SetAutoPadding(args.Length() < 1 || args[0]->IsTrue()); - args.GetReturnValue().Set(b); // Possibly report invalid state failure -} - -bool CipherBase::Final(AllocatedBuffer* out) { - if (!ctx_) - return false; - - const int mode = EVP_CIPHER_CTX_mode(ctx_.get()); - - *out = AllocatedBuffer::AllocateManaged( - env(), - static_cast(EVP_CIPHER_CTX_block_size(ctx_.get()))); - - if (kind_ == kDecipher && IsSupportedAuthenticatedMode(ctx_.get())) { - MaybePassAuthTagToOpenSSL(); - } - - // In CCM mode, final() only checks whether authentication failed in update(). - // EVP_CipherFinal_ex must not be called and will fail. - bool ok; - if (kind_ == kDecipher && mode == EVP_CIPH_CCM_MODE) { - ok = !pending_auth_failed_; - *out = AllocatedBuffer::AllocateManaged(env(), 0); // Empty buffer. - } else { - int out_len = out->size(); - ok = EVP_CipherFinal_ex(ctx_.get(), - reinterpret_cast(out->data()), - &out_len) == 1; - - if (out_len >= 0) - out->Resize(out_len); - else - *out = AllocatedBuffer(); // *out will not be used. - - if (ok && kind_ == kCipher && IsAuthenticatedMode()) { - // In GCM mode, the authentication tag length can be specified in advance, - // but defaults to 16 bytes when encrypting. In CCM and OCB mode, it must - // always be given by the user. - if (auth_tag_len_ == kNoAuthTagLength) { - CHECK(mode == EVP_CIPH_GCM_MODE); - auth_tag_len_ = sizeof(auth_tag_); - } - CHECK_EQ(1, EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_GET_TAG, - auth_tag_len_, - reinterpret_cast(auth_tag_))); - } - } - - ctx_.reset(); - - return ok; -} - - -void CipherBase::Final(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - CipherBase* cipher; - ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); - if (cipher->ctx_ == nullptr) return env->ThrowError("Unsupported state"); - - AllocatedBuffer out; - - // Check IsAuthenticatedMode() first, Final() destroys the EVP_CIPHER_CTX. - const bool is_auth_mode = cipher->IsAuthenticatedMode(); - bool r = cipher->Final(&out); - - if (!r) { - const char* msg = is_auth_mode - ? "Unsupported state or unable to authenticate data" - : "Unsupported state"; - - return ThrowCryptoError(env, ERR_get_error(), msg); - } - - args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); -} - -Hmac::Hmac(Environment* env, Local wrap) - : BaseObject(env, wrap), - ctx_(nullptr) { - MakeWeak(); -} - -void Hmac::Initialize(Environment* env, Local target) { - Local t = env->NewFunctionTemplate(New); - - t->InstanceTemplate()->SetInternalFieldCount( - Hmac::kInternalFieldCount); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - - env->SetProtoMethod(t, "init", HmacInit); - env->SetProtoMethod(t, "update", HmacUpdate); - env->SetProtoMethod(t, "digest", HmacDigest); - - target->Set(env->context(), - FIXED_ONE_BYTE_STRING(env->isolate(), "Hmac"), - t->GetFunction(env->context()).ToLocalChecked()).Check(); -} - - -void Hmac::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - new Hmac(env, args.This()); -} - - -void Hmac::HmacInit(const char* hash_type, const char* key, int key_len) { - HandleScope scope(env()->isolate()); - - const EVP_MD* md = EVP_get_digestbyname(hash_type); - if (md == nullptr) { - return env()->ThrowError("Unknown message digest"); - } - if (key_len == 0) { - key = ""; - } - ctx_.reset(HMAC_CTX_new()); - if (!ctx_ || !HMAC_Init_ex(ctx_.get(), key, key_len, md, nullptr)) { - ctx_.reset(); - return ThrowCryptoError(env(), ERR_get_error()); - } -} - - -void Hmac::HmacInit(const FunctionCallbackInfo& args) { - Hmac* hmac; - ASSIGN_OR_RETURN_UNWRAP(&hmac, args.Holder()); - Environment* env = hmac->env(); - - const node::Utf8Value hash_type(env->isolate(), args[0]); - ByteSource key = GetSecretKeyBytes(env, args[1]); - hmac->HmacInit(*hash_type, key.get(), key.size()); -} - - -bool Hmac::HmacUpdate(const char* data, int len) { - if (!ctx_) - return false; - int r = HMAC_Update(ctx_.get(), - reinterpret_cast(data), - len); - return r == 1; -} - - -void Hmac::HmacUpdate(const FunctionCallbackInfo& args) { - Decode(args, [](Hmac* hmac, const FunctionCallbackInfo& args, - const char* data, size_t size) { - bool r = hmac->HmacUpdate(data, size); - args.GetReturnValue().Set(r); - }); -} - - -void Hmac::HmacDigest(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - Hmac* hmac; - ASSIGN_OR_RETURN_UNWRAP(&hmac, args.Holder()); - - enum encoding encoding = BUFFER; - if (args.Length() >= 1) { - encoding = ParseEncoding(env->isolate(), args[0], BUFFER); - } - - unsigned char md_value[EVP_MAX_MD_SIZE]; - unsigned int md_len = 0; - - if (hmac->ctx_) { - HMAC_Final(hmac->ctx_.get(), md_value, &md_len); - hmac->ctx_.reset(); - } - - Local error; - MaybeLocal rc = - StringBytes::Encode(env->isolate(), - reinterpret_cast(md_value), - md_len, - encoding, - &error); - if (rc.IsEmpty()) { - CHECK(!error.IsEmpty()); - env->isolate()->ThrowException(error); - return; - } - args.GetReturnValue().Set(rc.ToLocalChecked()); -} - -Hash::Hash(Environment* env, Local wrap) - : BaseObject(env, wrap), - mdctx_(nullptr), - has_md_(false), - md_value_(nullptr) { - MakeWeak(); -} - -void Hash::Initialize(Environment* env, Local target) { - Local t = env->NewFunctionTemplate(New); - - t->InstanceTemplate()->SetInternalFieldCount( - Hash::kInternalFieldCount); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - - env->SetProtoMethod(t, "update", HashUpdate); - env->SetProtoMethod(t, "digest", HashDigest); - - target->Set(env->context(), - FIXED_ONE_BYTE_STRING(env->isolate(), "Hash"), - t->GetFunction(env->context()).ToLocalChecked()).Check(); -} - -Hash::~Hash() { - if (md_value_ != nullptr) - OPENSSL_clear_free(md_value_, md_len_); -} - -void Hash::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - const Hash* orig = nullptr; - const EVP_MD* md = nullptr; - - if (args[0]->IsObject()) { - ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As()); - md = EVP_MD_CTX_md(orig->mdctx_.get()); - } else { - const node::Utf8Value hash_type(env->isolate(), args[0]); - md = EVP_get_digestbyname(*hash_type); - } - - Maybe xof_md_len = Nothing(); - if (!args[1]->IsUndefined()) { - CHECK(args[1]->IsUint32()); - xof_md_len = Just(args[1].As()->Value()); - } - - Hash* hash = new Hash(env, args.This()); - if (md == nullptr || !hash->HashInit(md, xof_md_len)) { - return ThrowCryptoError(env, ERR_get_error(), - "Digest method not supported"); - } - - if (orig != nullptr && - 0 >= EVP_MD_CTX_copy(hash->mdctx_.get(), orig->mdctx_.get())) { - return ThrowCryptoError(env, ERR_get_error(), "Digest copy error"); - } -} - - -bool Hash::HashInit(const EVP_MD* md, Maybe xof_md_len) { - mdctx_.reset(EVP_MD_CTX_new()); - if (!mdctx_ || EVP_DigestInit_ex(mdctx_.get(), md, nullptr) <= 0) { - mdctx_.reset(); - return false; - } - - md_len_ = EVP_MD_size(md); - if (xof_md_len.IsJust() && xof_md_len.FromJust() != md_len_) { - // This is a little hack to cause createHash to fail when an incorrect - // hashSize option was passed for a non-XOF hash function. - if ((EVP_MD_flags(md) & EVP_MD_FLAG_XOF) == 0) { - EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH); - return false; - } - md_len_ = xof_md_len.FromJust(); - } - - return true; -} - - -bool Hash::HashUpdate(const char* data, int len) { - if (!mdctx_) - return false; - EVP_DigestUpdate(mdctx_.get(), data, len); - return true; -} - - -void Hash::HashUpdate(const FunctionCallbackInfo& args) { - Decode(args, [](Hash* hash, const FunctionCallbackInfo& args, - const char* data, size_t size) { - bool r = hash->HashUpdate(data, size); - args.GetReturnValue().Set(r); - }); -} - - -void Hash::HashDigest(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - Hash* hash; - ASSIGN_OR_RETURN_UNWRAP(&hash, args.Holder()); - - enum encoding encoding = BUFFER; - if (args.Length() >= 1) { - encoding = ParseEncoding(env->isolate(), args[0], BUFFER); - } - - // TODO(tniessen): SHA3_squeeze does not work for zero-length outputs on all - // platforms and will cause a segmentation fault if called. This workaround - // causes hash.digest() to correctly return an empty buffer / string. - // See https://github.com/openssl/openssl/issues/9431. - if (!hash->has_md_ && hash->md_len_ == 0) { - hash->has_md_ = true; - } - - if (!hash->has_md_) { - // Some hash algorithms such as SHA3 do not support calling - // EVP_DigestFinal_ex more than once, however, Hash._flush - // and Hash.digest can both be used to retrieve the digest, - // so we need to cache it. - // See https://github.com/nodejs/node/issues/28245. - - hash->md_value_ = MallocOpenSSL(hash->md_len_); - - size_t default_len = EVP_MD_CTX_size(hash->mdctx_.get()); - int ret; - if (hash->md_len_ == default_len) { - ret = EVP_DigestFinal_ex(hash->mdctx_.get(), hash->md_value_, - &hash->md_len_); - } else { - ret = EVP_DigestFinalXOF(hash->mdctx_.get(), hash->md_value_, - hash->md_len_); - } - - if (ret != 1) { - OPENSSL_free(hash->md_value_); - hash->md_value_ = nullptr; - return ThrowCryptoError(env, ERR_get_error()); - } - - hash->has_md_ = true; - } - - Local error; - MaybeLocal rc = - StringBytes::Encode(env->isolate(), - reinterpret_cast(hash->md_value_), - hash->md_len_, - encoding, - &error); - if (rc.IsEmpty()) { - CHECK(!error.IsEmpty()); - env->isolate()->ThrowException(error); - return; - } - args.GetReturnValue().Set(rc.ToLocalChecked()); -} - - -SignBase::Error SignBase::Init(const char* sign_type) { - CHECK_NULL(mdctx_); - // Historically, "dss1" and "DSS1" were DSA aliases for SHA-1 - // exposed through the public API. - if (strcmp(sign_type, "dss1") == 0 || - strcmp(sign_type, "DSS1") == 0) { - sign_type = "SHA1"; - } - const EVP_MD* md = EVP_get_digestbyname(sign_type); - if (md == nullptr) - return kSignUnknownDigest; - - mdctx_.reset(EVP_MD_CTX_new()); - if (!mdctx_ || !EVP_DigestInit_ex(mdctx_.get(), md, nullptr)) { - mdctx_.reset(); - return kSignInit; - } - - return kSignOk; -} - - -SignBase::Error SignBase::Update(const char* data, int len) { - if (mdctx_ == nullptr) - return kSignNotInitialised; - if (!EVP_DigestUpdate(mdctx_.get(), data, len)) - return kSignUpdate; - return kSignOk; -} - - -void CheckThrow(Environment* env, SignBase::Error error) { - HandleScope scope(env->isolate()); - - switch (error) { - case SignBase::Error::kSignUnknownDigest: - return env->ThrowError("Unknown message digest"); - - case SignBase::Error::kSignNotInitialised: - return env->ThrowError("Not initialised"); - - case SignBase::Error::kSignMalformedSignature: - return env->ThrowError("Malformed signature"); - - case SignBase::Error::kSignInit: - case SignBase::Error::kSignUpdate: - case SignBase::Error::kSignPrivateKey: - case SignBase::Error::kSignPublicKey: - { - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - if (err) - return ThrowCryptoError(env, err); - switch (error) { - case SignBase::Error::kSignInit: - return env->ThrowError("EVP_SignInit_ex failed"); - case SignBase::Error::kSignUpdate: - return env->ThrowError("EVP_SignUpdate failed"); - case SignBase::Error::kSignPrivateKey: - return env->ThrowError("PEM_read_bio_PrivateKey failed"); - case SignBase::Error::kSignPublicKey: - return env->ThrowError("PEM_read_bio_PUBKEY failed"); - default: - ABORT(); - } - } - - case SignBase::Error::kSignOk: - return; - } -} - -SignBase::SignBase(Environment* env, Local wrap) - : BaseObject(env, wrap) { -} - -void SignBase::CheckThrow(SignBase::Error error) { - node::crypto::CheckThrow(env(), error); -} - -static bool ApplyRSAOptions(const ManagedEVPPKey& pkey, - EVP_PKEY_CTX* pkctx, - int padding, - const Maybe& salt_len) { - if (EVP_PKEY_id(pkey.get()) == EVP_PKEY_RSA || - EVP_PKEY_id(pkey.get()) == EVP_PKEY_RSA2 || - EVP_PKEY_id(pkey.get()) == EVP_PKEY_RSA_PSS) { - if (EVP_PKEY_CTX_set_rsa_padding(pkctx, padding) <= 0) - return false; - if (padding == RSA_PKCS1_PSS_PADDING && salt_len.IsJust()) { - if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkctx, salt_len.FromJust()) <= 0) - return false; - } - } - - return true; -} - - -Sign::Sign(Environment* env, Local wrap) : SignBase(env, wrap) { - MakeWeak(); -} - -void Sign::Initialize(Environment* env, Local target) { - Local t = env->NewFunctionTemplate(New); - - t->InstanceTemplate()->SetInternalFieldCount( - SignBase::kInternalFieldCount); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - - env->SetProtoMethod(t, "init", SignInit); - env->SetProtoMethod(t, "update", SignUpdate); - env->SetProtoMethod(t, "sign", SignFinal); - - target->Set(env->context(), - FIXED_ONE_BYTE_STRING(env->isolate(), "Sign"), - t->GetFunction(env->context()).ToLocalChecked()).Check(); -} - - -void Sign::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - new Sign(env, args.This()); -} - - -void Sign::SignInit(const FunctionCallbackInfo& args) { - Sign* sign; - ASSIGN_OR_RETURN_UNWRAP(&sign, args.Holder()); - - const node::Utf8Value sign_type(args.GetIsolate(), args[0]); - sign->CheckThrow(sign->Init(*sign_type)); -} - - -void Sign::SignUpdate(const FunctionCallbackInfo& args) { - Decode(args, [](Sign* sign, const FunctionCallbackInfo& args, - const char* data, size_t size) { - Error err = sign->Update(data, size); - sign->CheckThrow(err); - }); -} - -static int GetDefaultSignPadding(const ManagedEVPPKey& key) { - return EVP_PKEY_id(key.get()) == EVP_PKEY_RSA_PSS ? RSA_PKCS1_PSS_PADDING : - RSA_PKCS1_PADDING; -} - -static const unsigned int kNoDsaSignature = static_cast(-1); - -// Returns the maximum size of each of the integers (r, s) of the DSA signature. -static unsigned int GetBytesOfRS(const ManagedEVPPKey& pkey) { - int bits, base_id = EVP_PKEY_base_id(pkey.get()); - - if (base_id == EVP_PKEY_DSA) { - DSA* dsa_key = EVP_PKEY_get0_DSA(pkey.get()); - // Both r and s are computed mod q, so their width is limited by that of q. - bits = BN_num_bits(DSA_get0_q(dsa_key)); - } else if (base_id == EVP_PKEY_EC) { - EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(pkey.get()); - const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key); - bits = EC_GROUP_order_bits(ec_group); - } else { - return kNoDsaSignature; - } - - return (bits + 7) / 8; -} - -static AllocatedBuffer ConvertSignatureToP1363(Environment* env, - const ManagedEVPPKey& pkey, - AllocatedBuffer&& signature) { - unsigned int n = GetBytesOfRS(pkey); - if (n == kNoDsaSignature) - return std::move(signature); - - const unsigned char* sig_data = - reinterpret_cast(signature.data()); - - ECDSASigPointer asn1_sig(d2i_ECDSA_SIG(nullptr, &sig_data, signature.size())); - if (!asn1_sig) - return AllocatedBuffer(); - - AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, 2 * n); - unsigned char* data = reinterpret_cast(buf.data()); - - const BIGNUM* r = ECDSA_SIG_get0_r(asn1_sig.get()); - const BIGNUM* s = ECDSA_SIG_get0_s(asn1_sig.get()); - CHECK_EQ(n, static_cast(BN_bn2binpad(r, data, n))); - CHECK_EQ(n, static_cast(BN_bn2binpad(s, data + n, n))); - - return buf; -} - -static ByteSource ConvertSignatureToDER( - const ManagedEVPPKey& pkey, - const ArrayBufferViewContents& signature) { - unsigned int n = GetBytesOfRS(pkey); - if (n == kNoDsaSignature) - return ByteSource::Foreign(signature.data(), signature.length()); - - const unsigned char* sig_data = - reinterpret_cast(signature.data()); - - if (signature.length() != 2 * n) - return ByteSource(); - - ECDSASigPointer asn1_sig(ECDSA_SIG_new()); - CHECK(asn1_sig); - BIGNUM* r = BN_new(); - CHECK_NOT_NULL(r); - BIGNUM* s = BN_new(); - CHECK_NOT_NULL(s); - CHECK_EQ(r, BN_bin2bn(sig_data, n, r)); - CHECK_EQ(s, BN_bin2bn(sig_data + n, n, s)); - CHECK_EQ(1, ECDSA_SIG_set0(asn1_sig.get(), r, s)); - - unsigned char* data = nullptr; - int len = i2d_ECDSA_SIG(asn1_sig.get(), &data); - - if (len <= 0) - return ByteSource(); - - CHECK_NOT_NULL(data); - - return ByteSource::Allocated(reinterpret_cast(data), len); -} - -static AllocatedBuffer Node_SignFinal(Environment* env, - EVPMDPointer&& mdctx, - const ManagedEVPPKey& pkey, - int padding, - Maybe pss_salt_len) { - unsigned char m[EVP_MAX_MD_SIZE]; - unsigned int m_len; - - if (!EVP_DigestFinal_ex(mdctx.get(), m, &m_len)) - return AllocatedBuffer(); - - int signed_sig_len = EVP_PKEY_size(pkey.get()); - CHECK_GE(signed_sig_len, 0); - size_t sig_len = static_cast(signed_sig_len); - AllocatedBuffer sig = AllocatedBuffer::AllocateManaged(env, sig_len); - - EVPKeyCtxPointer pkctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); - if (pkctx && - EVP_PKEY_sign_init(pkctx.get()) > 0 && - ApplyRSAOptions(pkey, pkctx.get(), padding, pss_salt_len) && - EVP_PKEY_CTX_set_signature_md(pkctx.get(), - EVP_MD_CTX_md(mdctx.get())) > 0 && - EVP_PKEY_sign(pkctx.get(), - reinterpret_cast(sig.data()), - &sig_len, - m, - m_len) > 0) { - sig.Resize(sig_len); - return sig; - } - - return AllocatedBuffer(); -} - -static inline bool ValidateDSAParameters(EVP_PKEY* key) { -#ifdef NODE_FIPS_MODE - /* Validate DSA2 parameters from FIPS 186-4 */ - if (FIPS_mode() && EVP_PKEY_DSA == EVP_PKEY_base_id(key)) { - DSA* dsa = EVP_PKEY_get0_DSA(key); - const BIGNUM* p; - DSA_get0_pqg(dsa, &p, nullptr, nullptr); - size_t L = BN_num_bits(p); - const BIGNUM* q; - DSA_get0_pqg(dsa, nullptr, &q, nullptr); - size_t N = BN_num_bits(q); - - return (L == 1024 && N == 160) || - (L == 2048 && N == 224) || - (L == 2048 && N == 256) || - (L == 3072 && N == 256); - } -#endif // NODE_FIPS_MODE - - return true; -} - -Sign::SignResult Sign::SignFinal( - const ManagedEVPPKey& pkey, - int padding, - const Maybe& salt_len, - DSASigEnc dsa_sig_enc) { - if (!mdctx_) - return SignResult(kSignNotInitialised); - - EVPMDPointer mdctx = std::move(mdctx_); - - if (!ValidateDSAParameters(pkey.get())) - return SignResult(kSignPrivateKey); - - AllocatedBuffer buffer = - Node_SignFinal(env(), std::move(mdctx), pkey, padding, salt_len); - Error error = buffer.data() == nullptr ? kSignPrivateKey : kSignOk; - if (error == kSignOk && dsa_sig_enc == kSigEncP1363) { - buffer = ConvertSignatureToP1363(env(), pkey, std::move(buffer)); - CHECK_NOT_NULL(buffer.data()); - } - return SignResult(error, std::move(buffer)); -} - - -void Sign::SignFinal(const FunctionCallbackInfo& args) { - Sign* sign; - ASSIGN_OR_RETURN_UNWRAP(&sign, args.Holder()); - - ClearErrorOnReturn clear_error_on_return; - - unsigned int offset = 0; - ManagedEVPPKey key = GetPrivateKeyFromJs(args, &offset, true); - if (!key) - return; - - int padding = GetDefaultSignPadding(key); - if (!args[offset]->IsUndefined()) { - CHECK(args[offset]->IsInt32()); - padding = args[offset].As()->Value(); - } - - Maybe salt_len = Nothing(); - if (!args[offset + 1]->IsUndefined()) { - CHECK(args[offset + 1]->IsInt32()); - salt_len = Just(args[offset + 1].As()->Value()); - } - - CHECK(args[offset + 2]->IsInt32()); - DSASigEnc dsa_sig_enc = - static_cast(args[offset + 2].As()->Value()); - - SignResult ret = sign->SignFinal( - key, - padding, - salt_len, - dsa_sig_enc); - - if (ret.error != kSignOk) - return sign->CheckThrow(ret.error); - - args.GetReturnValue().Set(ret.signature.ToBuffer().ToLocalChecked()); -} - -void SignOneShot(const FunctionCallbackInfo& args) { - ClearErrorOnReturn clear_error_on_return; - Environment* env = Environment::GetCurrent(args); - - unsigned int offset = 0; - ManagedEVPPKey key = GetPrivateKeyFromJs(args, &offset, true); - if (!key) - return; - - if (!ValidateDSAParameters(key.get())) - return CheckThrow(env, SignBase::Error::kSignPrivateKey); - - ArrayBufferViewContents data(args[offset]); - - const EVP_MD* md; - if (args[offset + 1]->IsNullOrUndefined()) { - md = nullptr; - } else { - const node::Utf8Value sign_type(args.GetIsolate(), args[offset + 1]); - md = EVP_get_digestbyname(*sign_type); - if (md == nullptr) - return CheckThrow(env, SignBase::Error::kSignUnknownDigest); - } - - int rsa_padding = GetDefaultSignPadding(key); - if (!args[offset + 2]->IsUndefined()) { - CHECK(args[offset + 2]->IsInt32()); - rsa_padding = args[offset + 2].As()->Value(); - } - - Maybe rsa_salt_len = Nothing(); - if (!args[offset + 3]->IsUndefined()) { - CHECK(args[offset + 3]->IsInt32()); - rsa_salt_len = Just(args[offset + 3].As()->Value()); - } - - CHECK(args[offset + 4]->IsInt32()); - DSASigEnc dsa_sig_enc = - static_cast(args[offset + 4].As()->Value()); - - EVP_PKEY_CTX* pkctx = nullptr; - EVPMDPointer mdctx(EVP_MD_CTX_new()); - if (!mdctx || - !EVP_DigestSignInit(mdctx.get(), &pkctx, md, nullptr, key.get())) { - return CheckThrow(env, SignBase::Error::kSignInit); - } - - if (!ApplyRSAOptions(key, pkctx, rsa_padding, rsa_salt_len)) - return CheckThrow(env, SignBase::Error::kSignPrivateKey); - - const unsigned char* input = - reinterpret_cast(data.data()); - size_t sig_len; - if (!EVP_DigestSign(mdctx.get(), nullptr, &sig_len, input, data.length())) - return CheckThrow(env, SignBase::Error::kSignPrivateKey); - - AllocatedBuffer signature = AllocatedBuffer::AllocateManaged(env, sig_len); - if (!EVP_DigestSign(mdctx.get(), - reinterpret_cast(signature.data()), - &sig_len, - input, - data.length())) { - return CheckThrow(env, SignBase::Error::kSignPrivateKey); - } - - signature.Resize(sig_len); - - if (dsa_sig_enc == kSigEncP1363) { - signature = ConvertSignatureToP1363(env, key, std::move(signature)); - } - - args.GetReturnValue().Set(signature.ToBuffer().ToLocalChecked()); -} - -Verify::Verify(Environment* env, Local wrap) - : SignBase(env, wrap) { - MakeWeak(); -} - -void Verify::Initialize(Environment* env, Local target) { - Local t = env->NewFunctionTemplate(New); - - t->InstanceTemplate()->SetInternalFieldCount( - SignBase::kInternalFieldCount); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - - env->SetProtoMethod(t, "init", VerifyInit); - env->SetProtoMethod(t, "update", VerifyUpdate); - env->SetProtoMethod(t, "verify", VerifyFinal); - - target->Set(env->context(), - FIXED_ONE_BYTE_STRING(env->isolate(), "Verify"), - t->GetFunction(env->context()).ToLocalChecked()).Check(); -} - - -void Verify::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - new Verify(env, args.This()); -} - - -void Verify::VerifyInit(const FunctionCallbackInfo& args) { - Verify* verify; - ASSIGN_OR_RETURN_UNWRAP(&verify, args.Holder()); - - const node::Utf8Value verify_type(args.GetIsolate(), args[0]); - verify->CheckThrow(verify->Init(*verify_type)); -} - - -void Verify::VerifyUpdate(const FunctionCallbackInfo& args) { - Decode(args, [](Verify* verify, - const FunctionCallbackInfo& args, - const char* data, size_t size) { - Error err = verify->Update(data, size); - verify->CheckThrow(err); - }); -} - - -SignBase::Error Verify::VerifyFinal(const ManagedEVPPKey& pkey, - const ByteSource& sig, - int padding, - const Maybe& saltlen, - bool* verify_result) { - if (!mdctx_) - return kSignNotInitialised; - - unsigned char m[EVP_MAX_MD_SIZE]; - unsigned int m_len; - *verify_result = false; - EVPMDPointer mdctx = std::move(mdctx_); - - if (!EVP_DigestFinal_ex(mdctx.get(), m, &m_len)) - return kSignPublicKey; - - EVPKeyCtxPointer pkctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); - if (pkctx && - EVP_PKEY_verify_init(pkctx.get()) > 0 && - ApplyRSAOptions(pkey, pkctx.get(), padding, saltlen) && - EVP_PKEY_CTX_set_signature_md(pkctx.get(), - EVP_MD_CTX_md(mdctx.get())) > 0) { - const unsigned char* s = reinterpret_cast(sig.get()); - const int r = EVP_PKEY_verify(pkctx.get(), s, sig.size(), m, m_len); - *verify_result = r == 1; - } - - return kSignOk; -} - - -void Verify::VerifyFinal(const FunctionCallbackInfo& args) { - ClearErrorOnReturn clear_error_on_return; - - Verify* verify; - ASSIGN_OR_RETURN_UNWRAP(&verify, args.Holder()); - - unsigned int offset = 0; - ManagedEVPPKey pkey = GetPublicOrPrivateKeyFromJs(args, &offset); - if (!pkey) - return; - - ArrayBufferViewContents hbuf(args[offset]); - - int padding = GetDefaultSignPadding(pkey); - if (!args[offset + 1]->IsUndefined()) { - CHECK(args[offset + 1]->IsInt32()); - padding = args[offset + 1].As()->Value(); - } - - Maybe salt_len = Nothing(); - if (!args[offset + 2]->IsUndefined()) { - CHECK(args[offset + 2]->IsInt32()); - salt_len = Just(args[offset + 2].As()->Value()); - } - - CHECK(args[offset + 3]->IsInt32()); - DSASigEnc dsa_sig_enc = - static_cast(args[offset + 3].As()->Value()); - - ByteSource signature = ByteSource::Foreign(hbuf.data(), hbuf.length()); - if (dsa_sig_enc == kSigEncP1363) { - signature = ConvertSignatureToDER(pkey, hbuf); - if (signature.get() == nullptr) - return verify->CheckThrow(Error::kSignMalformedSignature); - } - - bool verify_result; - Error err = verify->VerifyFinal(pkey, signature, padding, - salt_len, &verify_result); - if (err != kSignOk) - return verify->CheckThrow(err); - args.GetReturnValue().Set(verify_result); -} - -void VerifyOneShot(const FunctionCallbackInfo& args) { - ClearErrorOnReturn clear_error_on_return; - Environment* env = Environment::GetCurrent(args); - - unsigned int offset = 0; - ManagedEVPPKey key = GetPublicOrPrivateKeyFromJs(args, &offset); - if (!key) - return; - - ArrayBufferViewContents sig(args[offset]); - - ArrayBufferViewContents data(args[offset + 1]); - - const EVP_MD* md; - if (args[offset + 2]->IsNullOrUndefined()) { - md = nullptr; - } else { - const node::Utf8Value sign_type(args.GetIsolate(), args[offset + 2]); - md = EVP_get_digestbyname(*sign_type); - if (md == nullptr) - return CheckThrow(env, SignBase::Error::kSignUnknownDigest); - } - - int rsa_padding = GetDefaultSignPadding(key); - if (!args[offset + 3]->IsUndefined()) { - CHECK(args[offset + 3]->IsInt32()); - rsa_padding = args[offset + 3].As()->Value(); - } - - Maybe rsa_salt_len = Nothing(); - if (!args[offset + 4]->IsUndefined()) { - CHECK(args[offset + 4]->IsInt32()); - rsa_salt_len = Just(args[offset + 4].As()->Value()); - } - - CHECK(args[offset + 5]->IsInt32()); - DSASigEnc dsa_sig_enc = - static_cast(args[offset + 5].As()->Value()); - - EVP_PKEY_CTX* pkctx = nullptr; - EVPMDPointer mdctx(EVP_MD_CTX_new()); - if (!mdctx || - !EVP_DigestVerifyInit(mdctx.get(), &pkctx, md, nullptr, key.get())) { - return CheckThrow(env, SignBase::Error::kSignInit); - } - - if (!ApplyRSAOptions(key, pkctx, rsa_padding, rsa_salt_len)) - return CheckThrow(env, SignBase::Error::kSignPublicKey); - - ByteSource sig_bytes = ByteSource::Foreign(sig.data(), sig.length()); - if (dsa_sig_enc == kSigEncP1363) { - sig_bytes = ConvertSignatureToDER(key, sig); - if (!sig_bytes) - return CheckThrow(env, SignBase::Error::kSignMalformedSignature); - } - - bool verify_result; - const int r = EVP_DigestVerify( - mdctx.get(), - reinterpret_cast(sig_bytes.get()), - sig_bytes.size(), - reinterpret_cast(data.data()), - data.length()); - switch (r) { - case 1: - verify_result = true; - break; - case 0: - verify_result = false; - break; - default: - return CheckThrow(env, SignBase::Error::kSignPublicKey); - } - - args.GetReturnValue().Set(verify_result); -} - -template -bool PublicKeyCipher::Cipher(Environment* env, - const ManagedEVPPKey& pkey, - int padding, - const EVP_MD* digest, - const void* oaep_label, - size_t oaep_label_len, - const unsigned char* data, - int len, - AllocatedBuffer* out) { - EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); - if (!ctx) - return false; - if (EVP_PKEY_cipher_init(ctx.get()) <= 0) - return false; - if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), padding) <= 0) - return false; - - if (digest != nullptr) { - if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), digest) <= 0) - return false; - } - - if (oaep_label_len != 0) { - // OpenSSL takes ownership of the label, so we need to create a copy. - void* label = OPENSSL_memdup(oaep_label, oaep_label_len); - CHECK_NOT_NULL(label); - if (0 >= EVP_PKEY_CTX_set0_rsa_oaep_label(ctx.get(), - reinterpret_cast(label), - oaep_label_len)) { - OPENSSL_free(label); - return false; - } - } - - size_t out_len = 0; - if (EVP_PKEY_cipher(ctx.get(), nullptr, &out_len, data, len) <= 0) - return false; - - *out = AllocatedBuffer::AllocateManaged(env, out_len); - - if (EVP_PKEY_cipher(ctx.get(), - reinterpret_cast(out->data()), - &out_len, - data, - len) <= 0) { - return false; - } - - out->Resize(out_len); - return true; -} - - -template -void PublicKeyCipher::Cipher(const FunctionCallbackInfo& args) { - MarkPopErrorOnReturn mark_pop_error_on_return; - Environment* env = Environment::GetCurrent(args); - - unsigned int offset = 0; - ManagedEVPPKey pkey = GetPublicOrPrivateKeyFromJs(args, &offset); - if (!pkey) - return; - - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[offset], "Data"); - ArrayBufferViewContents buf(args[offset]); - - uint32_t padding; - if (!args[offset + 1]->Uint32Value(env->context()).To(&padding)) return; - - const node::Utf8Value oaep_str(env->isolate(), args[offset + 2]); - const char* oaep_hash = args[offset + 2]->IsString() ? *oaep_str : nullptr; - const EVP_MD* digest = nullptr; - if (oaep_hash != nullptr) { - digest = EVP_get_digestbyname(oaep_hash); - if (digest == nullptr) - return THROW_ERR_OSSL_EVP_INVALID_DIGEST(env); - } - - ArrayBufferViewContents oaep_label; - if (!args[offset + 3]->IsUndefined()) { - CHECK(args[offset + 3]->IsArrayBufferView()); - oaep_label.Read(args[offset + 3].As()); - } - - AllocatedBuffer out; - - bool r = Cipher( - env, - pkey, - padding, - digest, - oaep_label.data(), - oaep_label.length(), - buf.data(), - buf.length(), - &out); - - if (!r) - return ThrowCryptoError(env, ERR_get_error()); - - args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); -} - -DiffieHellman::DiffieHellman(Environment* env, Local wrap) - : BaseObject(env, wrap), verifyError_(0) { - MakeWeak(); -} - -void DiffieHellman::Initialize(Environment* env, Local target) { - auto make = [&] (Local name, FunctionCallback callback) { - Local t = env->NewFunctionTemplate(callback); - - const PropertyAttribute attributes = - static_cast(ReadOnly | DontDelete); - - t->InstanceTemplate()->SetInternalFieldCount( - DiffieHellman::kInternalFieldCount); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - - env->SetProtoMethod(t, "generateKeys", GenerateKeys); - env->SetProtoMethod(t, "computeSecret", ComputeSecret); - env->SetProtoMethodNoSideEffect(t, "getPrime", GetPrime); - env->SetProtoMethodNoSideEffect(t, "getGenerator", GetGenerator); - env->SetProtoMethodNoSideEffect(t, "getPublicKey", GetPublicKey); - env->SetProtoMethodNoSideEffect(t, "getPrivateKey", GetPrivateKey); - env->SetProtoMethod(t, "setPublicKey", SetPublicKey); - env->SetProtoMethod(t, "setPrivateKey", SetPrivateKey); - - Local verify_error_getter_templ = - FunctionTemplate::New(env->isolate(), - DiffieHellman::VerifyErrorGetter, - Local(), - Signature::New(env->isolate(), t), - /* length */ 0, - ConstructorBehavior::kThrow, - SideEffectType::kHasNoSideEffect); - - t->InstanceTemplate()->SetAccessorProperty( - env->verify_error_string(), - verify_error_getter_templ, - Local(), - attributes); - - target->Set(env->context(), - name, - t->GetFunction(env->context()).ToLocalChecked()).Check(); - }; - - make(FIXED_ONE_BYTE_STRING(env->isolate(), "DiffieHellman"), New); - make(FIXED_ONE_BYTE_STRING(env->isolate(), "DiffieHellmanGroup"), - DiffieHellmanGroup); -} - - -bool DiffieHellman::Init(int primeLength, int g) { - dh_.reset(DH_new()); - if (!DH_generate_parameters_ex(dh_.get(), primeLength, g, nullptr)) - return false; - return VerifyContext(); -} - - -bool DiffieHellman::Init(const char* p, int p_len, int g) { - dh_.reset(DH_new()); - if (p_len <= 0) { - BNerr(BN_F_BN_GENERATE_PRIME_EX, BN_R_BITS_TOO_SMALL); - return false; - } - if (g <= 1) { - DHerr(DH_F_DH_BUILTIN_GENPARAMS, DH_R_BAD_GENERATOR); - return false; - } - BIGNUM* bn_p = - BN_bin2bn(reinterpret_cast(p), p_len, nullptr); - BIGNUM* bn_g = BN_new(); - if (!BN_set_word(bn_g, g) || - !DH_set0_pqg(dh_.get(), bn_p, nullptr, bn_g)) { - BN_free(bn_p); - BN_free(bn_g); - return false; - } - return VerifyContext(); -} - - -bool DiffieHellman::Init(const char* p, int p_len, const char* g, int g_len) { - dh_.reset(DH_new()); - if (p_len <= 0) { - BNerr(BN_F_BN_GENERATE_PRIME_EX, BN_R_BITS_TOO_SMALL); - return false; - } - if (g_len <= 0) { - DHerr(DH_F_DH_BUILTIN_GENPARAMS, DH_R_BAD_GENERATOR); - return false; - } - BIGNUM* bn_g = - BN_bin2bn(reinterpret_cast(g), g_len, nullptr); - if (BN_is_zero(bn_g) || BN_is_one(bn_g)) { - BN_free(bn_g); - DHerr(DH_F_DH_BUILTIN_GENPARAMS, DH_R_BAD_GENERATOR); - return false; - } - BIGNUM* bn_p = - BN_bin2bn(reinterpret_cast(p), p_len, nullptr); - if (!DH_set0_pqg(dh_.get(), bn_p, nullptr, bn_g)) { - BN_free(bn_p); - BN_free(bn_g); - return false; - } - return VerifyContext(); -} - -inline const modp_group* FindDiffieHellmanGroup(const char* name) { - for (const modp_group& group : modp_groups) { - if (StringEqualNoCase(name, group.name)) - return &group; - } - return nullptr; -} - -void DiffieHellman::DiffieHellmanGroup( - const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - DiffieHellman* diffieHellman = new DiffieHellman(env, args.This()); - - CHECK_EQ(args.Length(), 1); - THROW_AND_RETURN_IF_NOT_STRING(env, args[0], "Group name"); - - bool initialized = false; - - const node::Utf8Value group_name(env->isolate(), args[0]); - const modp_group* group = FindDiffieHellmanGroup(*group_name); - if (group == nullptr) - return THROW_ERR_CRYPTO_UNKNOWN_DH_GROUP(env); - - initialized = diffieHellman->Init(group->prime, - group->prime_size, - group->gen); - if (!initialized) - env->ThrowError("Initialization failed"); -} - - -void DiffieHellman::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - DiffieHellman* diffieHellman = - new DiffieHellman(env, args.This()); - bool initialized = false; - - if (args.Length() == 2) { - if (args[0]->IsInt32()) { - if (args[1]->IsInt32()) { - initialized = diffieHellman->Init(args[0].As()->Value(), - args[1].As()->Value()); - } - } else { - ArrayBufferViewContents arg0(args[0]); - if (args[1]->IsInt32()) { - initialized = diffieHellman->Init(arg0.data(), - arg0.length(), - args[1].As()->Value()); - } else { - ArrayBufferViewContents arg1(args[1]); - initialized = diffieHellman->Init(arg0.data(), arg0.length(), - arg1.data(), arg1.length()); - } - } - } - - if (!initialized) { - return ThrowCryptoError(env, ERR_get_error(), "Initialization failed"); - } -} - - -void DiffieHellman::GenerateKeys(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - DiffieHellman* diffieHellman; - ASSIGN_OR_RETURN_UNWRAP(&diffieHellman, args.Holder()); - - if (!DH_generate_key(diffieHellman->dh_.get())) { - return ThrowCryptoError(env, ERR_get_error(), "Key generation failed"); - } - - const BIGNUM* pub_key; - DH_get0_key(diffieHellman->dh_.get(), &pub_key, nullptr); - const int size = BN_num_bytes(pub_key); - CHECK_GE(size, 0); - AllocatedBuffer data = AllocatedBuffer::AllocateManaged(env, size); - CHECK_EQ(size, - BN_bn2binpad( - pub_key, reinterpret_cast(data.data()), size)); - args.GetReturnValue().Set(data.ToBuffer().ToLocalChecked()); -} - - -void DiffieHellman::GetField(const FunctionCallbackInfo& args, - const BIGNUM* (*get_field)(const DH*), - const char* err_if_null) { - Environment* env = Environment::GetCurrent(args); - - DiffieHellman* dh; - ASSIGN_OR_RETURN_UNWRAP(&dh, args.Holder()); - - const BIGNUM* num = get_field(dh->dh_.get()); - if (num == nullptr) return env->ThrowError(err_if_null); - - const int size = BN_num_bytes(num); - CHECK_GE(size, 0); - AllocatedBuffer data = AllocatedBuffer::AllocateManaged(env, size); - CHECK_EQ( - size, - BN_bn2binpad(num, reinterpret_cast(data.data()), size)); - args.GetReturnValue().Set(data.ToBuffer().ToLocalChecked()); -} - -void DiffieHellman::GetPrime(const FunctionCallbackInfo& args) { - GetField(args, [](const DH* dh) -> const BIGNUM* { - const BIGNUM* p; - DH_get0_pqg(dh, &p, nullptr, nullptr); - return p; - }, "p is null"); -} - - -void DiffieHellman::GetGenerator(const FunctionCallbackInfo& args) { - GetField(args, [](const DH* dh) -> const BIGNUM* { - const BIGNUM* g; - DH_get0_pqg(dh, nullptr, nullptr, &g); - return g; - }, "g is null"); -} - - -void DiffieHellman::GetPublicKey(const FunctionCallbackInfo& args) { - GetField(args, [](const DH* dh) -> const BIGNUM* { - const BIGNUM* pub_key; - DH_get0_key(dh, &pub_key, nullptr); - return pub_key; - }, "No public key - did you forget to generate one?"); -} - - -void DiffieHellman::GetPrivateKey(const FunctionCallbackInfo& args) { - GetField(args, [](const DH* dh) -> const BIGNUM* { - const BIGNUM* priv_key; - DH_get0_key(dh, nullptr, &priv_key); - return priv_key; - }, "No private key - did you forget to generate one?"); -} - -static void ZeroPadDiffieHellmanSecret(size_t remainder_size, - AllocatedBuffer* ret) { - // DH_size returns number of bytes in a prime number. - // DH_compute_key returns number of bytes in a remainder of exponent, which - // may have less bytes than a prime number. Therefore add 0-padding to the - // allocated buffer. - const size_t prime_size = ret->size(); - if (remainder_size != prime_size) { - CHECK_LT(remainder_size, prime_size); - const size_t padding = prime_size - remainder_size; - memmove(ret->data() + padding, ret->data(), remainder_size); - memset(ret->data(), 0, padding); - } -} - -void DiffieHellman::ComputeSecret(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - DiffieHellman* diffieHellman; - ASSIGN_OR_RETURN_UNWRAP(&diffieHellman, args.Holder()); - - ClearErrorOnReturn clear_error_on_return; - - CHECK_EQ(args.Length(), 1); - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "Other party's public key"); - ArrayBufferViewContents key_buf(args[0].As()); - BignumPointer key(BN_bin2bn(key_buf.data(), key_buf.length(), nullptr)); - - AllocatedBuffer ret = - AllocatedBuffer::AllocateManaged(env, DH_size(diffieHellman->dh_.get())); - - int size = DH_compute_key(reinterpret_cast(ret.data()), - key.get(), - diffieHellman->dh_.get()); - - if (size == -1) { - int checkResult; - int checked; - - checked = DH_check_pub_key(diffieHellman->dh_.get(), - key.get(), - &checkResult); - - if (!checked) { - return ThrowCryptoError(env, ERR_get_error(), "Invalid Key"); - } else if (checkResult) { - if (checkResult & DH_CHECK_PUBKEY_TOO_SMALL) { - return env->ThrowError("Supplied key is too small"); - } else if (checkResult & DH_CHECK_PUBKEY_TOO_LARGE) { - return env->ThrowError("Supplied key is too large"); - } else { - return env->ThrowError("Invalid key"); - } - } else { - return env->ThrowError("Invalid key"); - } - - UNREACHABLE(); - } - - CHECK_GE(size, 0); - ZeroPadDiffieHellmanSecret(static_cast(size), &ret); - - args.GetReturnValue().Set(ret.ToBuffer().ToLocalChecked()); -} - -void DiffieHellman::SetKey(const FunctionCallbackInfo& args, - int (*set_field)(DH*, BIGNUM*), const char* what) { - Environment* env = Environment::GetCurrent(args); - - DiffieHellman* dh; - ASSIGN_OR_RETURN_UNWRAP(&dh, args.Holder()); - - char errmsg[64]; - - CHECK_EQ(args.Length(), 1); - if (!Buffer::HasInstance(args[0])) { - snprintf(errmsg, sizeof(errmsg), "%s must be a buffer", what); - return THROW_ERR_INVALID_ARG_TYPE(env, errmsg); - } - - ArrayBufferViewContents buf(args[0].As()); - BIGNUM* num = - BN_bin2bn(buf.data(), buf.length(), nullptr); - CHECK_NOT_NULL(num); - CHECK_EQ(1, set_field(dh->dh_.get(), num)); -} - - -void DiffieHellman::SetPublicKey(const FunctionCallbackInfo& args) { - SetKey(args, - [](DH* dh, BIGNUM* num) { return DH_set0_key(dh, num, nullptr); }, - "Public key"); -} - -void DiffieHellman::SetPrivateKey(const FunctionCallbackInfo& args) { - SetKey(args, - [](DH* dh, BIGNUM* num) { return DH_set0_key(dh, nullptr, num); }, - "Private key"); -} - - -void DiffieHellman::VerifyErrorGetter(const FunctionCallbackInfo& args) { - HandleScope scope(args.GetIsolate()); - - DiffieHellman* diffieHellman; - ASSIGN_OR_RETURN_UNWRAP(&diffieHellman, args.Holder()); - - args.GetReturnValue().Set(diffieHellman->verifyError_); -} - - -bool DiffieHellman::VerifyContext() { - int codes; - if (!DH_check(dh_.get(), &codes)) - return false; - verifyError_ = codes; - return true; -} - - -void ECDH::Initialize(Environment* env, Local target) { - HandleScope scope(env->isolate()); - - Local t = env->NewFunctionTemplate(New); - t->Inherit(BaseObject::GetConstructorTemplate(env)); - - t->InstanceTemplate()->SetInternalFieldCount(ECDH::kInternalFieldCount); - - env->SetProtoMethod(t, "generateKeys", GenerateKeys); - env->SetProtoMethod(t, "computeSecret", ComputeSecret); - env->SetProtoMethodNoSideEffect(t, "getPublicKey", GetPublicKey); - env->SetProtoMethodNoSideEffect(t, "getPrivateKey", GetPrivateKey); - env->SetProtoMethod(t, "setPublicKey", SetPublicKey); - env->SetProtoMethod(t, "setPrivateKey", SetPrivateKey); - - target->Set(env->context(), - FIXED_ONE_BYTE_STRING(env->isolate(), "ECDH"), - t->GetFunction(env->context()).ToLocalChecked()).Check(); -} - -ECDH::ECDH(Environment* env, Local wrap, ECKeyPointer&& key) - : BaseObject(env, wrap), - key_(std::move(key)), - group_(EC_KEY_get0_group(key_.get())) { - MakeWeak(); - CHECK_NOT_NULL(group_); -} - -ECDH::~ECDH() {} - -void ECDH::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - MarkPopErrorOnReturn mark_pop_error_on_return; - - // TODO(indutny): Support raw curves? - CHECK(args[0]->IsString()); - node::Utf8Value curve(env->isolate(), args[0]); - - int nid = OBJ_sn2nid(*curve); - if (nid == NID_undef) - return THROW_ERR_INVALID_ARG_VALUE(env, - "First argument should be a valid curve name"); - - ECKeyPointer key(EC_KEY_new_by_curve_name(nid)); - if (!key) - return env->ThrowError("Failed to create EC_KEY using curve name"); - - new ECDH(env, args.This(), std::move(key)); -} - - -void ECDH::GenerateKeys(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - ECDH* ecdh; - ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); - - if (!EC_KEY_generate_key(ecdh->key_.get())) - return env->ThrowError("Failed to generate EC_KEY"); -} - - -ECPointPointer ECDH::BufferToPoint(Environment* env, - const EC_GROUP* group, - Local buf) { - int r; - - ECPointPointer pub(EC_POINT_new(group)); - if (!pub) { - env->ThrowError("Failed to allocate EC_POINT for a public key"); - return pub; - } - - ArrayBufferViewContents input(buf); - r = EC_POINT_oct2point( - group, - pub.get(), - input.data(), - input.length(), - nullptr); - if (!r) - return ECPointPointer(); - - return pub; -} - - -void ECDH::ComputeSecret(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "Data"); - - ECDH* ecdh; - ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); - - MarkPopErrorOnReturn mark_pop_error_on_return; - - if (!ecdh->IsKeyPairValid()) - return env->ThrowError("Invalid key pair"); - - ECPointPointer pub( - ECDH::BufferToPoint(env, - ecdh->group_, - args[0])); - if (!pub) { - args.GetReturnValue().Set( - FIXED_ONE_BYTE_STRING(env->isolate(), - "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY")); - return; - } - - // NOTE: field_size is in bits - int field_size = EC_GROUP_get_degree(ecdh->group_); - size_t out_len = (field_size + 7) / 8; - AllocatedBuffer out = AllocatedBuffer::AllocateManaged(env, out_len); - - int r = ECDH_compute_key( - out.data(), out_len, pub.get(), ecdh->key_.get(), nullptr); - if (!r) - return env->ThrowError("Failed to compute ECDH key"); - - Local buf = out.ToBuffer().ToLocalChecked(); - args.GetReturnValue().Set(buf); -} - - -void ECDH::GetPublicKey(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - // Conversion form - CHECK_EQ(args.Length(), 1); - - ECDH* ecdh; - ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); - - const EC_GROUP* group = EC_KEY_get0_group(ecdh->key_.get()); - const EC_POINT* pub = EC_KEY_get0_public_key(ecdh->key_.get()); - if (pub == nullptr) - return env->ThrowError("Failed to get ECDH public key"); - - CHECK(args[0]->IsUint32()); - uint32_t val = args[0].As()->Value(); - point_conversion_form_t form = static_cast(val); - - const char* error; - Local buf; - if (!ECPointToBuffer(env, group, pub, form, &error).ToLocal(&buf)) - return env->ThrowError(error); - args.GetReturnValue().Set(buf); -} - - -void ECDH::GetPrivateKey(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - ECDH* ecdh; - ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); - - const BIGNUM* b = EC_KEY_get0_private_key(ecdh->key_.get()); - if (b == nullptr) - return env->ThrowError("Failed to get ECDH private key"); - - const int size = BN_num_bytes(b); - AllocatedBuffer out = AllocatedBuffer::AllocateManaged(env, size); - CHECK_EQ(size, BN_bn2binpad(b, - reinterpret_cast(out.data()), - size)); - - Local buf = out.ToBuffer().ToLocalChecked(); - args.GetReturnValue().Set(buf); -} - - -void ECDH::SetPrivateKey(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - ECDH* ecdh; - ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); - - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "Private key"); - ArrayBufferViewContents priv_buffer(args[0]); - - BignumPointer priv(BN_bin2bn( - priv_buffer.data(), priv_buffer.length(), nullptr)); - if (!priv) - return env->ThrowError("Failed to convert Buffer to BN"); - - if (!ecdh->IsKeyValidForCurve(priv)) { - return env->ThrowError("Private key is not valid for specified curve."); - } - - ECKeyPointer new_key(EC_KEY_dup(ecdh->key_.get())); - CHECK(new_key); - - int result = EC_KEY_set_private_key(new_key.get(), priv.get()); - priv.reset(); - - if (!result) { - return env->ThrowError("Failed to convert BN to a private key"); - } - - MarkPopErrorOnReturn mark_pop_error_on_return; - USE(&mark_pop_error_on_return); - - const BIGNUM* priv_key = EC_KEY_get0_private_key(new_key.get()); - CHECK_NOT_NULL(priv_key); - - ECPointPointer pub(EC_POINT_new(ecdh->group_)); - CHECK(pub); - - if (!EC_POINT_mul(ecdh->group_, pub.get(), priv_key, - nullptr, nullptr, nullptr)) { - return env->ThrowError("Failed to generate ECDH public key"); - } - - if (!EC_KEY_set_public_key(new_key.get(), pub.get())) - return env->ThrowError("Failed to set generated public key"); - - EC_KEY_copy(ecdh->key_.get(), new_key.get()); - ecdh->group_ = EC_KEY_get0_group(ecdh->key_.get()); -} - - -void ECDH::SetPublicKey(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - ECDH* ecdh; - ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.Holder()); - - THROW_AND_RETURN_IF_NOT_BUFFER(env, args[0], "Public key"); - - MarkPopErrorOnReturn mark_pop_error_on_return; - - ECPointPointer pub( - ECDH::BufferToPoint(env, - ecdh->group_, - args[0])); - if (!pub) - return env->ThrowError("Failed to convert Buffer to EC_POINT"); - - int r = EC_KEY_set_public_key(ecdh->key_.get(), pub.get()); - if (!r) - return env->ThrowError("Failed to set EC_POINT as the public key"); -} - - -bool ECDH::IsKeyValidForCurve(const BignumPointer& private_key) { - CHECK(group_); - CHECK(private_key); - // Private keys must be in the range [1, n-1]. - // Ref: Section 3.2.1 - http://www.secg.org/sec1-v2.pdf - if (BN_cmp(private_key.get(), BN_value_one()) < 0) { - return false; - } - BignumPointer order(BN_new()); - CHECK(order); - return EC_GROUP_get_order(group_, order.get(), nullptr) && - BN_cmp(private_key.get(), order.get()) < 0; -} - - -bool ECDH::IsKeyPairValid() { - MarkPopErrorOnReturn mark_pop_error_on_return; - USE(&mark_pop_error_on_return); - return 1 == EC_KEY_check_key(key_.get()); -} - - -// TODO(addaleax): If there is an `AsyncWrap`, it currently has no access to -// this object. This makes proper reporting of memory usage impossible. -struct CryptoJob : public ThreadPoolWork { - std::unique_ptr async_wrap; - inline explicit CryptoJob(Environment* env) : ThreadPoolWork(env) {} - inline void AfterThreadPoolWork(int status) final; - virtual void AfterThreadPoolWork() = 0; - static inline void Run(std::unique_ptr job, Local wrap); -}; - - -void CryptoJob::AfterThreadPoolWork(int status) { - CHECK(status == 0 || status == UV_ECANCELED); - std::unique_ptr job(this); - if (status == UV_ECANCELED) return; - HandleScope handle_scope(env()->isolate()); - Context::Scope context_scope(env()->context()); - CHECK_EQ(false, async_wrap->persistent().IsWeak()); - AfterThreadPoolWork(); -} - - -void CryptoJob::Run(std::unique_ptr job, Local wrap) { - CHECK(wrap->IsObject()); - CHECK_NULL(job->async_wrap); - job->async_wrap.reset(Unwrap(wrap.As())); - CHECK_EQ(false, job->async_wrap->persistent().IsWeak()); - job->ScheduleWork(); - job.release(); // Run free, little job! -} - - -inline void CopyBuffer(Local buf, std::vector* vec) { - CHECK(buf->IsArrayBufferView()); - vec->clear(); - vec->resize(buf.As()->ByteLength()); - buf.As()->CopyContents(vec->data(), vec->size()); -} - - -struct RandomBytesJob : public CryptoJob { - unsigned char* data; - size_t size; - CryptoErrorVector errors; - Maybe rc; - - inline explicit RandomBytesJob(Environment* env) - : CryptoJob(env), rc(Nothing()) {} - - inline void DoThreadPoolWork() override { - CheckEntropy(); // Ensure that OpenSSL's PRNG is properly seeded. - rc = Just(RAND_bytes(data, size)); - if (0 == rc.FromJust()) errors.Capture(); - } - - inline void AfterThreadPoolWork() override { - Local arg = ToResult(); - async_wrap->MakeCallback(env()->ondone_string(), 1, &arg); - } - - inline Local ToResult() const { - if (errors.empty()) return Undefined(env()->isolate()); - return errors.ToException(env()).ToLocalChecked(); - } -}; - - -void RandomBytes(const FunctionCallbackInfo& args) { - CHECK(args[0]->IsArrayBufferView()); // buffer; wrap object retains ref. - CHECK(args[1]->IsUint32()); // offset - CHECK(args[2]->IsUint32()); // size - CHECK(args[3]->IsObject() || args[3]->IsUndefined()); // wrap object - const uint32_t offset = args[1].As()->Value(); - const uint32_t size = args[2].As()->Value(); - CHECK_GE(offset + size, offset); // Overflow check. - CHECK_LE(offset + size, Buffer::Length(args[0])); // Bounds check. - Environment* env = Environment::GetCurrent(args); - std::unique_ptr job(new RandomBytesJob(env)); - job->data = reinterpret_cast(Buffer::Data(args[0])) + offset; - job->size = size; - if (args[3]->IsObject()) return RandomBytesJob::Run(std::move(job), args[3]); - env->PrintSyncTrace(); - job->DoThreadPoolWork(); - args.GetReturnValue().Set(job->ToResult()); -} - - -struct PBKDF2Job : public CryptoJob { - unsigned char* keybuf_data; - size_t keybuf_size; - std::vector pass; - std::vector salt; - uint32_t iteration_count; - const EVP_MD* digest; - Maybe success; - - inline explicit PBKDF2Job(Environment* env) - : CryptoJob(env), success(Nothing()) {} - - inline ~PBKDF2Job() override { - Cleanse(); - } - - inline void DoThreadPoolWork() override { - auto salt_data = reinterpret_cast(salt.data()); - const bool ok = - PKCS5_PBKDF2_HMAC(pass.data(), pass.size(), salt_data, salt.size(), - iteration_count, digest, keybuf_size, keybuf_data); - success = Just(ok); - Cleanse(); - } - - inline void AfterThreadPoolWork() override { - Local arg = ToResult(); - async_wrap->MakeCallback(env()->ondone_string(), 1, &arg); - } - - inline Local ToResult() const { - return Boolean::New(env()->isolate(), success.FromJust()); - } - - inline void Cleanse() { - OPENSSL_cleanse(pass.data(), pass.size()); - OPENSSL_cleanse(salt.data(), salt.size()); - pass.clear(); - salt.clear(); - } -}; - - -inline void PBKDF2(const FunctionCallbackInfo& args) { - auto rv = args.GetReturnValue(); - Environment* env = Environment::GetCurrent(args); - CHECK(args[0]->IsArrayBufferView()); // keybuf; wrap object retains ref. - CHECK(args[1]->IsArrayBufferView()); // pass - CHECK(args[2]->IsArrayBufferView()); // salt - CHECK(args[3]->IsUint32()); // iteration_count - CHECK(args[4]->IsString()); // digest_name - CHECK(args[5]->IsObject() || args[5]->IsUndefined()); // wrap object - std::unique_ptr job(new PBKDF2Job(env)); - job->keybuf_data = reinterpret_cast(Buffer::Data(args[0])); - job->keybuf_size = Buffer::Length(args[0]); - CopyBuffer(args[1], &job->pass); - CopyBuffer(args[2], &job->salt); - job->iteration_count = args[3].As()->Value(); - Utf8Value digest_name(args.GetIsolate(), args[4]); - job->digest = EVP_get_digestbyname(*digest_name); - if (job->digest == nullptr) return rv.Set(-1); - if (args[5]->IsObject()) return PBKDF2Job::Run(std::move(job), args[5]); - env->PrintSyncTrace(); - job->DoThreadPoolWork(); - rv.Set(job->ToResult()); -} - - -#ifndef OPENSSL_NO_SCRYPT -struct ScryptJob : public CryptoJob { - unsigned char* keybuf_data; - size_t keybuf_size; - std::vector pass; - std::vector salt; - uint32_t N; - uint32_t r; - uint32_t p; - uint64_t maxmem; - CryptoErrorVector errors; - - inline explicit ScryptJob(Environment* env) : CryptoJob(env) {} - - inline ~ScryptJob() override { - Cleanse(); - } - - inline bool Validate() { - if (1 == EVP_PBE_scrypt(nullptr, 0, nullptr, 0, N, r, p, maxmem, - nullptr, 0)) { - return true; - } else { - // Note: EVP_PBE_scrypt() does not always put errors on the error stack. - errors.Capture(); - return false; - } - } - - inline void DoThreadPoolWork() override { - auto salt_data = reinterpret_cast(salt.data()); - if (1 != EVP_PBE_scrypt(pass.data(), pass.size(), salt_data, salt.size(), - N, r, p, maxmem, keybuf_data, keybuf_size)) { - errors.Capture(); - } - } - - inline void AfterThreadPoolWork() override { - Local arg = ToResult(); - async_wrap->MakeCallback(env()->ondone_string(), 1, &arg); - } - - inline Local ToResult() const { - if (errors.empty()) return Undefined(env()->isolate()); - return errors.ToException(env()).ToLocalChecked(); - } - - inline void Cleanse() { - OPENSSL_cleanse(pass.data(), pass.size()); - OPENSSL_cleanse(salt.data(), salt.size()); - pass.clear(); - salt.clear(); - } -}; - - -void Scrypt(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - CHECK(args[0]->IsArrayBufferView()); // keybuf; wrap object retains ref. - CHECK(args[1]->IsArrayBufferView()); // pass - CHECK(args[2]->IsArrayBufferView()); // salt - CHECK(args[3]->IsUint32()); // N - CHECK(args[4]->IsUint32()); // r - CHECK(args[5]->IsUint32()); // p - CHECK(args[6]->IsNumber()); // maxmem - CHECK(args[7]->IsObject() || args[7]->IsUndefined()); // wrap object - std::unique_ptr job(new ScryptJob(env)); - job->keybuf_data = reinterpret_cast(Buffer::Data(args[0])); - job->keybuf_size = Buffer::Length(args[0]); - CopyBuffer(args[1], &job->pass); - CopyBuffer(args[2], &job->salt); - job->N = args[3].As()->Value(); - job->r = args[4].As()->Value(); - job->p = args[5].As()->Value(); - Local ctx = env->isolate()->GetCurrentContext(); - job->maxmem = static_cast(args[6]->IntegerValue(ctx).ToChecked()); - if (!job->Validate()) { - // EVP_PBE_scrypt() does not always put errors on the error stack - // and therefore ToResult() may or may not return an exception - // object. Return a sentinel value to inform JS land it should - // throw an ERR_CRYPTO_SCRYPT_INVALID_PARAMETER on our behalf. - auto result = job->ToResult(); - if (result->IsUndefined()) result = Null(args.GetIsolate()); - return args.GetReturnValue().Set(result); - } - if (args[7]->IsObject()) return ScryptJob::Run(std::move(job), args[7]); - env->PrintSyncTrace(); - job->DoThreadPoolWork(); - args.GetReturnValue().Set(job->ToResult()); -} -#endif // OPENSSL_NO_SCRYPT - - -class KeyPairGenerationConfig { - public: - virtual EVPKeyCtxPointer Setup() = 0; - virtual bool Configure(const EVPKeyCtxPointer& ctx) { - return true; - } - virtual ~KeyPairGenerationConfig() = default; -}; - -class RSAKeyPairGenerationConfig : public KeyPairGenerationConfig { - public: - RSAKeyPairGenerationConfig(unsigned int modulus_bits, unsigned int exponent) - : modulus_bits_(modulus_bits), exponent_(exponent) {} - - EVPKeyCtxPointer Setup() override { - return EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)); - } - - bool Configure(const EVPKeyCtxPointer& ctx) override { - if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), modulus_bits_) <= 0) - return false; - - // 0x10001 is the default RSA exponent. - if (exponent_ != 0x10001) { - BignumPointer bn(BN_new()); - CHECK_NOT_NULL(bn.get()); - CHECK(BN_set_word(bn.get(), exponent_)); - // EVP_CTX acceps ownership of bn on success. - if (EVP_PKEY_CTX_set_rsa_keygen_pubexp(ctx.get(), bn.get()) <= 0) - return false; - bn.release(); - } - - return true; - } - - private: - const unsigned int modulus_bits_; - const unsigned int exponent_; -}; - -class RSAPSSKeyPairGenerationConfig : public RSAKeyPairGenerationConfig { - public: - RSAPSSKeyPairGenerationConfig(unsigned int modulus_bits, - unsigned int exponent, - const EVP_MD* md, - const EVP_MD* mgf1_md, - int saltlen) - : RSAKeyPairGenerationConfig(modulus_bits, exponent), - md_(md), mgf1_md_(mgf1_md), saltlen_(saltlen) {} - - EVPKeyCtxPointer Setup() override { - return EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA_PSS, nullptr)); - } - - bool Configure(const EVPKeyCtxPointer& ctx) override { - if (!RSAKeyPairGenerationConfig::Configure(ctx)) - return false; - - if (md_ != nullptr) { - if (EVP_PKEY_CTX_set_rsa_pss_keygen_md(ctx.get(), md_) <= 0) - return false; - } - - if (mgf1_md_ != nullptr) { - if (EVP_PKEY_CTX_set_rsa_pss_keygen_mgf1_md(ctx.get(), mgf1_md_) <= 0) - return false; - } - - if (saltlen_ >= 0) { - if (EVP_PKEY_CTX_set_rsa_pss_keygen_saltlen(ctx.get(), saltlen_) <= 0) - return false; - } - - return true; - } - - private: - const EVP_MD* md_; - const EVP_MD* mgf1_md_; - const int saltlen_; -}; - -class DSAKeyPairGenerationConfig : public KeyPairGenerationConfig { - public: - DSAKeyPairGenerationConfig(unsigned int modulus_bits, int divisor_bits) - : modulus_bits_(modulus_bits), divisor_bits_(divisor_bits) {} - - EVPKeyCtxPointer Setup() override { - EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_DSA, nullptr)); - if (!param_ctx) - return nullptr; - - if (EVP_PKEY_paramgen_init(param_ctx.get()) <= 0) - return nullptr; - - if (EVP_PKEY_CTX_set_dsa_paramgen_bits(param_ctx.get(), modulus_bits_) <= 0) - return nullptr; - - if (divisor_bits_ != -1) { - if (EVP_PKEY_CTX_ctrl(param_ctx.get(), EVP_PKEY_DSA, EVP_PKEY_OP_PARAMGEN, - EVP_PKEY_CTRL_DSA_PARAMGEN_Q_BITS, divisor_bits_, - nullptr) <= 0) { - return nullptr; - } - } - - EVP_PKEY* raw_params = nullptr; - if (EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) - return nullptr; - EVPKeyPointer params(raw_params); - param_ctx.reset(); - - EVPKeyCtxPointer key_ctx(EVP_PKEY_CTX_new(params.get(), nullptr)); - return key_ctx; - } - - private: - const unsigned int modulus_bits_; - const int divisor_bits_; -}; - -class ECKeyPairGenerationConfig : public KeyPairGenerationConfig { - public: - ECKeyPairGenerationConfig(int curve_nid, int param_encoding) - : curve_nid_(curve_nid), param_encoding_(param_encoding) {} - - EVPKeyCtxPointer Setup() override { - EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr)); - if (!param_ctx) - return nullptr; - - if (EVP_PKEY_paramgen_init(param_ctx.get()) <= 0) - return nullptr; - - if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(param_ctx.get(), - curve_nid_) <= 0) - return nullptr; - - if (EVP_PKEY_CTX_set_ec_param_enc(param_ctx.get(), param_encoding_) <= 0) - return nullptr; - - EVP_PKEY* raw_params = nullptr; - if (EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) - return nullptr; - EVPKeyPointer params(raw_params); - param_ctx.reset(); - - EVPKeyCtxPointer key_ctx(EVP_PKEY_CTX_new(params.get(), nullptr)); - return key_ctx; - } - - private: - const int curve_nid_; - const int param_encoding_; -}; - -class NidKeyPairGenerationConfig : public KeyPairGenerationConfig { - public: - explicit NidKeyPairGenerationConfig(int id) : id_(id) {} - - EVPKeyCtxPointer Setup() override { - return EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(id_, nullptr)); - } - - private: - const int id_; -}; - -// TODO(tniessen): Use std::variant instead. -// Diffie-Hellman can either generate keys using a fixed prime, or by first -// generating a random prime of a given size (in bits). Only one of both options -// may be specified. -struct PrimeInfo { - BignumPointer fixed_value_; - unsigned int prime_size_; -}; - -class DHKeyPairGenerationConfig : public KeyPairGenerationConfig { - public: - explicit DHKeyPairGenerationConfig(PrimeInfo&& prime_info, - unsigned int generator) - : prime_info_(std::move(prime_info)), - generator_(generator) {} - - EVPKeyCtxPointer Setup() override { - EVPKeyPointer params; - if (prime_info_.fixed_value_) { - DHPointer dh(DH_new()); - if (!dh) - return nullptr; - - BIGNUM* prime = prime_info_.fixed_value_.get(); - BignumPointer bn_g(BN_new()); - if (!BN_set_word(bn_g.get(), generator_) || - !DH_set0_pqg(dh.get(), prime, nullptr, bn_g.get())) - return nullptr; - - prime_info_.fixed_value_.release(); - bn_g.release(); - - params = EVPKeyPointer(EVP_PKEY_new()); - CHECK(params); - EVP_PKEY_assign_DH(params.get(), dh.release()); - } else { - EVPKeyCtxPointer param_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_DH, nullptr)); - if (!param_ctx) - return nullptr; - - if (EVP_PKEY_paramgen_init(param_ctx.get()) <= 0) - return nullptr; - - if (EVP_PKEY_CTX_set_dh_paramgen_prime_len(param_ctx.get(), - prime_info_.prime_size_) <= 0) - return nullptr; - - if (EVP_PKEY_CTX_set_dh_paramgen_generator(param_ctx.get(), - generator_) <= 0) - return nullptr; - - EVP_PKEY* raw_params = nullptr; - if (EVP_PKEY_paramgen(param_ctx.get(), &raw_params) <= 0) - return nullptr; - params = EVPKeyPointer(raw_params); - } - - return EVPKeyCtxPointer(EVP_PKEY_CTX_new(params.get(), nullptr)); - } - - private: - PrimeInfo prime_info_; - unsigned int generator_; -}; - -class GenerateKeyPairJob : public CryptoJob { - public: - GenerateKeyPairJob(Environment* env, - std::unique_ptr config, - PublicKeyEncodingConfig public_key_encoding, - PrivateKeyEncodingConfig&& private_key_encoding) - : CryptoJob(env), - config_(std::move(config)), - public_key_encoding_(public_key_encoding), - private_key_encoding_(std::forward( - private_key_encoding)), - pkey_(nullptr) {} - - inline void DoThreadPoolWork() override { - if (!GenerateKey()) - errors_.Capture(); - } - - inline bool GenerateKey() { - // Make sure that the CSPRNG is properly seeded so the results are secure. - CheckEntropy(); - - // Create the key generation context. - EVPKeyCtxPointer ctx = config_->Setup(); - if (!ctx) - return false; - - // Initialize key generation. - if (EVP_PKEY_keygen_init(ctx.get()) <= 0) - return false; - - // Configure key generation. - if (!config_->Configure(ctx)) - return false; - - // Generate the key. - EVP_PKEY* pkey = nullptr; - if (EVP_PKEY_keygen(ctx.get(), &pkey) != 1) - return false; - pkey_ = ManagedEVPPKey(EVPKeyPointer(pkey)); - return true; - } - - inline void AfterThreadPoolWork() override { - Local args[3]; - ToResult(&args[0], &args[1], &args[2]); - async_wrap->MakeCallback(env()->ondone_string(), 3, args); - } - - inline void ToResult(Local* err, - Local* pubkey, - Local* privkey) { - if (pkey_ && EncodeKeys(pubkey, privkey)) { - CHECK(errors_.empty()); - *err = Undefined(env()->isolate()); - } else { - if (errors_.empty()) - errors_.Capture(); - CHECK(!errors_.empty()); - *err = errors_.ToException(env()).ToLocalChecked(); - *pubkey = Undefined(env()->isolate()); - *privkey = Undefined(env()->isolate()); - } - } - - inline bool EncodeKeys(Local* pubkey, Local* privkey) { - // Encode the public key. - if (public_key_encoding_.output_key_object_) { - // Note that this has the downside of containing sensitive data of the - // private key. - std::shared_ptr data = - KeyObjectData::CreateAsymmetric(kKeyTypePublic, pkey_); - if (!KeyObjectHandle::Create(env(), data).ToLocal(pubkey)) - return false; - } else { - if (!WritePublicKey(env(), pkey_.get(), public_key_encoding_) - .ToLocal(pubkey)) - return false; - } - - // Now do the same for the private key. - if (private_key_encoding_.output_key_object_) { - std::shared_ptr data = - KeyObjectData::CreateAsymmetric(kKeyTypePrivate, pkey_); - if (!KeyObjectHandle::Create(env(), data).ToLocal(privkey)) - return false; - } else { - if (!WritePrivateKey(env(), pkey_.get(), private_key_encoding_) - .ToLocal(privkey)) - return false; - } - - return true; - } - - private: - CryptoErrorVector errors_; - std::unique_ptr config_; - PublicKeyEncodingConfig public_key_encoding_; - PrivateKeyEncodingConfig private_key_encoding_; - ManagedEVPPKey pkey_; -}; - -void GenerateKeyPair(const FunctionCallbackInfo& args, - unsigned int offset, - std::unique_ptr config) { - Environment* env = Environment::GetCurrent(args); - - PublicKeyEncodingConfig public_key_encoding = - GetPublicKeyEncodingFromJs(args, &offset, kKeyContextGenerate); - NonCopyableMaybe private_key_encoding = - GetPrivateKeyEncodingFromJs(args, &offset, kKeyContextGenerate); - - if (private_key_encoding.IsEmpty()) - return; - - std::unique_ptr job( - new GenerateKeyPairJob(env, std::move(config), public_key_encoding, - private_key_encoding.Release())); - if (args[offset]->IsObject()) - return GenerateKeyPairJob::Run(std::move(job), args[offset]); - env->PrintSyncTrace(); - job->DoThreadPoolWork(); - Local err, pubkey, privkey; - job->ToResult(&err, &pubkey, &privkey); - - Local ret[] = { err, pubkey, privkey }; - args.GetReturnValue().Set(Array::New(env->isolate(), ret, arraysize(ret))); -} - -void GenerateKeyPairRSA(const FunctionCallbackInfo& args) { - CHECK(args[0]->IsUint32()); - const uint32_t modulus_bits = args[0].As()->Value(); - CHECK(args[1]->IsUint32()); - const uint32_t exponent = args[1].As()->Value(); - std::unique_ptr config( - new RSAKeyPairGenerationConfig(modulus_bits, exponent)); - GenerateKeyPair(args, 2, std::move(config)); -} - -void GenerateKeyPairRSAPSS(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - CHECK(args[0]->IsUint32()); - const uint32_t modulus_bits = args[0].As()->Value(); - CHECK(args[1]->IsUint32()); - const uint32_t exponent = args[1].As()->Value(); - - const EVP_MD* md = nullptr; - if (!args[2]->IsUndefined()) { - CHECK(args[2]->IsString()); - String::Utf8Value md_name(env->isolate(), args[2].As()); - md = EVP_get_digestbyname(*md_name); - if (md == nullptr) - return env->ThrowTypeError("Digest method not supported"); - } - - const EVP_MD* mgf1_md = nullptr; - if (!args[3]->IsUndefined()) { - CHECK(args[3]->IsString()); - String::Utf8Value mgf1_md_name(env->isolate(), args[3].As()); - mgf1_md = EVP_get_digestbyname(*mgf1_md_name); - if (mgf1_md == nullptr) - return env->ThrowTypeError("Digest method not supported"); - } - - int saltlen = -1; - if (!args[4]->IsUndefined()) { - CHECK(args[4]->IsInt32()); - saltlen = args[4].As()->Value(); - } - - std::unique_ptr config( - new RSAPSSKeyPairGenerationConfig(modulus_bits, exponent, - md, mgf1_md, saltlen)); - GenerateKeyPair(args, 5, std::move(config)); -} - -void GenerateKeyPairDSA(const FunctionCallbackInfo& args) { - CHECK(args[0]->IsUint32()); - const uint32_t modulus_bits = args[0].As()->Value(); - CHECK(args[1]->IsInt32()); - const int32_t divisor_bits = args[1].As()->Value(); - std::unique_ptr config( - new DSAKeyPairGenerationConfig(modulus_bits, divisor_bits)); - GenerateKeyPair(args, 2, std::move(config)); -} - -void GenerateKeyPairEC(const FunctionCallbackInfo& args) { - CHECK(args[0]->IsString()); - String::Utf8Value curve_name(args.GetIsolate(), args[0].As()); - int curve_nid = EC_curve_nist2nid(*curve_name); - if (curve_nid == NID_undef) - curve_nid = OBJ_sn2nid(*curve_name); - // TODO(tniessen): Should we also support OBJ_ln2nid? (Other APIs don't.) - if (curve_nid == NID_undef) { - Environment* env = Environment::GetCurrent(args); - return env->ThrowTypeError("Invalid ECDH curve name"); - } - CHECK(args[1]->IsUint32()); - const uint32_t param_encoding = args[1].As()->Value(); - CHECK(param_encoding == OPENSSL_EC_NAMED_CURVE || - param_encoding == OPENSSL_EC_EXPLICIT_CURVE); - std::unique_ptr config( - new ECKeyPairGenerationConfig(curve_nid, param_encoding)); - GenerateKeyPair(args, 2, std::move(config)); -} - -void GenerateKeyPairNid(const FunctionCallbackInfo& args) { - CHECK(args[0]->IsInt32()); - const int id = args[0].As()->Value(); - std::unique_ptr config( - new NidKeyPairGenerationConfig(id)); - GenerateKeyPair(args, 1, std::move(config)); -} - -void GenerateKeyPairDH(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - PrimeInfo prime_info = {}; - unsigned int generator; - if (args[0]->IsString()) { - String::Utf8Value group_name(args.GetIsolate(), args[0].As()); - const modp_group* group = FindDiffieHellmanGroup(*group_name); - if (group == nullptr) - return THROW_ERR_CRYPTO_UNKNOWN_DH_GROUP(env); - - prime_info.fixed_value_ = BignumPointer( - BN_bin2bn(reinterpret_cast(group->prime), - group->prime_size, nullptr)); - generator = group->gen; - } else { - if (args[0]->IsInt32()) { - prime_info.prime_size_ = args[0].As()->Value(); - } else { - ArrayBufferViewContents input(args[0]); - prime_info.fixed_value_ = BignumPointer( - BN_bin2bn(input.data(), input.length(), nullptr)); - } - - CHECK(args[1]->IsInt32()); - generator = args[1].As()->Value(); - } - - std::unique_ptr config( - new DHKeyPairGenerationConfig(std::move(prime_info), generator)); - GenerateKeyPair(args, 2, std::move(config)); -} - - -void GetSSLCiphers(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - SSLCtxPointer ctx(SSL_CTX_new(TLS_method())); - CHECK(ctx); - - SSLPointer ssl(SSL_new(ctx.get())); - CHECK(ssl); - - STACK_OF(SSL_CIPHER)* ciphers = SSL_get_ciphers(ssl.get()); - // TLSv1.3 ciphers aren't listed by EVP. There are only 5, we could just - // document them, but since there are only 5, easier to just add them manually - // and not have to explain their absence in the API docs. They are lower-cased - // because the docs say they will be. - static const char* TLS13_CIPHERS[] = { - "tls_aes_256_gcm_sha384", - "tls_chacha20_poly1305_sha256", - "tls_aes_128_gcm_sha256", - "tls_aes_128_ccm_8_sha256", - "tls_aes_128_ccm_sha256" - }; - - const int n = sk_SSL_CIPHER_num(ciphers); - std::vector> arr(n + arraysize(TLS13_CIPHERS)); - - for (int i = 0; i < n; ++i) { - const SSL_CIPHER* cipher = sk_SSL_CIPHER_value(ciphers, i); - arr[i] = OneByteString(env->isolate(), SSL_CIPHER_get_name(cipher)); - } - - for (unsigned i = 0; i < arraysize(TLS13_CIPHERS); ++i) { - const char* name = TLS13_CIPHERS[i]; - arr[n + i] = OneByteString(env->isolate(), name); - } - - args.GetReturnValue().Set(Array::New(env->isolate(), arr.data(), arr.size())); -} - - -class CipherPushContext { - public: - explicit CipherPushContext(Environment* env) - : arr(Array::New(env->isolate())), - env_(env) { - } - - inline Environment* env() const { return env_; } - - Local arr; - - private: - Environment* env_; -}; - - -template -static void array_push_back(const TypeName* md, - const char* from, - const char* to, - void* arg) { - CipherPushContext* ctx = static_cast(arg); - ctx->arr->Set(ctx->env()->context(), - ctx->arr->Length(), - OneByteString(ctx->env()->isolate(), from)).Check(); -} - - -void GetCiphers(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - CipherPushContext ctx(env); - EVP_CIPHER_do_all_sorted(array_push_back, &ctx); - args.GetReturnValue().Set(ctx.arr); -} - - -void GetHashes(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - CipherPushContext ctx(env); - EVP_MD_do_all_sorted(array_push_back, &ctx); - args.GetReturnValue().Set(ctx.arr); -} - - -void GetCurves(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - const size_t num_curves = EC_get_builtin_curves(nullptr, 0); - - if (num_curves) { - std::vector curves(num_curves); - - if (EC_get_builtin_curves(curves.data(), num_curves)) { - std::vector> arr(num_curves); - - for (size_t i = 0; i < num_curves; i++) - arr[i] = OneByteString(env->isolate(), OBJ_nid2sn(curves[i].nid)); - - args.GetReturnValue().Set( - Array::New(env->isolate(), arr.data(), arr.size())); - return; - } - } - - args.GetReturnValue().Set(Array::New(env->isolate())); -} - - -bool VerifySpkac(const char* data, unsigned int len) { - NetscapeSPKIPointer spki(NETSCAPE_SPKI_b64_decode(data, len)); - if (!spki) - return false; - - EVPKeyPointer pkey(X509_PUBKEY_get(spki->spkac->pubkey)); - if (!pkey) - return false; - - return NETSCAPE_SPKI_verify(spki.get(), pkey.get()) > 0; -} - - -void VerifySpkac(const FunctionCallbackInfo& args) { - bool verify_result = false; - - ArrayBufferViewContents input(args[0]); - if (input.length() == 0) - return args.GetReturnValue().SetEmptyString(); - - CHECK_NOT_NULL(input.data()); - - verify_result = VerifySpkac(input.data(), input.length()); - - args.GetReturnValue().Set(verify_result); -} - -AllocatedBuffer ExportPublicKey(Environment* env, - const char* data, - int len, - size_t* size) { - BIOPointer bio(BIO_new(BIO_s_mem())); - if (!bio) return AllocatedBuffer(); - - NetscapeSPKIPointer spki(NETSCAPE_SPKI_b64_decode(data, len)); - if (!spki) return AllocatedBuffer(); - - EVPKeyPointer pkey(NETSCAPE_SPKI_get_pubkey(spki.get())); - if (!pkey) return AllocatedBuffer(); - - if (PEM_write_bio_PUBKEY(bio.get(), pkey.get()) <= 0) - return AllocatedBuffer(); - - BUF_MEM* ptr; - BIO_get_mem_ptr(bio.get(), &ptr); - - *size = ptr->length; - AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, *size); - memcpy(buf.data(), ptr->data, *size); - - return buf; -} - - -void ExportPublicKey(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - ArrayBufferViewContents input(args[0]); - if (input.length() == 0) - return args.GetReturnValue().SetEmptyString(); - - CHECK_NOT_NULL(input.data()); - - size_t pkey_size; - AllocatedBuffer pkey = - ExportPublicKey(env, input.data(), input.length(), &pkey_size); - if (pkey.data() == nullptr) - return args.GetReturnValue().SetEmptyString(); - - args.GetReturnValue().Set(pkey.ToBuffer().ToLocalChecked()); -} - - -OpenSSLBuffer ExportChallenge(const char* data, int len) { - NetscapeSPKIPointer sp(NETSCAPE_SPKI_b64_decode(data, len)); - if (!sp) - return nullptr; - - unsigned char* buf = nullptr; - ASN1_STRING_to_UTF8(&buf, sp->spkac->challenge); - - return OpenSSLBuffer(reinterpret_cast(buf)); -} - - -void ExportChallenge(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - ArrayBufferViewContents input(args[0]); - if (input.length() == 0) - return args.GetReturnValue().SetEmptyString(); - - OpenSSLBuffer cert = ExportChallenge(input.data(), input.length()); - if (!cert) - return args.GetReturnValue().SetEmptyString(); - - Local outString = - Encode(env->isolate(), cert.get(), strlen(cert.get()), BUFFER); - - args.GetReturnValue().Set(outString); -} - - -// Convert the input public key to compressed, uncompressed, or hybrid formats. -void ConvertKey(const FunctionCallbackInfo& args) { - MarkPopErrorOnReturn mark_pop_error_on_return; - Environment* env = Environment::GetCurrent(args); - - CHECK_EQ(args.Length(), 3); - CHECK(args[0]->IsArrayBufferView()); - - size_t len = args[0].As()->ByteLength(); - if (len == 0) - return args.GetReturnValue().SetEmptyString(); - - node::Utf8Value curve(env->isolate(), args[1]); - - int nid = OBJ_sn2nid(*curve); - if (nid == NID_undef) - return env->ThrowTypeError("Invalid ECDH curve name"); - - ECGroupPointer group( - EC_GROUP_new_by_curve_name(nid)); - if (group == nullptr) - return env->ThrowError("Failed to get EC_GROUP"); - - ECPointPointer pub( - ECDH::BufferToPoint(env, - group.get(), - args[0])); - - if (pub == nullptr) - return env->ThrowError("Failed to convert Buffer to EC_POINT"); - - CHECK(args[2]->IsUint32()); - uint32_t val = args[2].As()->Value(); - point_conversion_form_t form = static_cast(val); - - const char* error; - Local buf; - if (!ECPointToBuffer(env, group.get(), pub.get(), form, &error).ToLocal(&buf)) - return env->ThrowError(error); - args.GetReturnValue().Set(buf); -} - -AllocatedBuffer StatelessDiffieHellman(Environment* env, ManagedEVPPKey our_key, - ManagedEVPPKey their_key) { - size_t out_size; - - EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(our_key.get(), nullptr)); - if (!ctx || - EVP_PKEY_derive_init(ctx.get()) <= 0 || - EVP_PKEY_derive_set_peer(ctx.get(), their_key.get()) <= 0 || - EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0) - return AllocatedBuffer(); - - AllocatedBuffer result = AllocatedBuffer::AllocateManaged(env, out_size); - CHECK_NOT_NULL(result.data()); - - unsigned char* data = reinterpret_cast(result.data()); - if (EVP_PKEY_derive(ctx.get(), data, &out_size) <= 0) - return AllocatedBuffer(); - - ZeroPadDiffieHellmanSecret(out_size, &result); - return result; -} - -void StatelessDiffieHellman(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - - CHECK(args[0]->IsObject() && args[1]->IsObject()); - KeyObjectHandle* our_key_object; - ASSIGN_OR_RETURN_UNWRAP(&our_key_object, args[0].As()); - CHECK_EQ(our_key_object->Data()->GetKeyType(), kKeyTypePrivate); - KeyObjectHandle* their_key_object; - ASSIGN_OR_RETURN_UNWRAP(&their_key_object, args[1].As()); - CHECK_NE(their_key_object->Data()->GetKeyType(), kKeyTypeSecret); - - ManagedEVPPKey our_key = our_key_object->Data()->GetAsymmetricKey(); - ManagedEVPPKey their_key = their_key_object->Data()->GetAsymmetricKey(); - - AllocatedBuffer out = StatelessDiffieHellman(env, our_key, their_key); - if (out.size() == 0) - return ThrowCryptoError(env, ERR_get_error(), "diffieHellman failed"); - - args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked()); -} - - -void TimingSafeEqual(const FunctionCallbackInfo& args) { - // Moving the type checking into JS leads to test failures, most likely due - // to V8 inlining certain parts of the wrapper. Therefore, keep them in C++. - // Refs: https://github.com/nodejs/node/issues/34073. - Environment* env = Environment::GetCurrent(args); - if (!args[0]->IsArrayBufferView()) { - THROW_ERR_INVALID_ARG_TYPE( - env, "The \"buf1\" argument must be an instance of " - "Buffer, TypedArray, or DataView."); - return; - } - if (!args[1]->IsArrayBufferView()) { - THROW_ERR_INVALID_ARG_TYPE( - env, "The \"buf2\" argument must be an instance of " - "Buffer, TypedArray, or DataView."); - return; - } - - ArrayBufferViewContents buf1(args[0].As()); - ArrayBufferViewContents buf2(args[1].As()); - - if (buf1.length() != buf2.length()) { - THROW_ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH(env); - return; - } - - return args.GetReturnValue().Set( - CRYPTO_memcmp(buf1.data(), buf2.data(), buf1.length()) == 0); -} - -void InitCryptoOnce() { -#ifndef OPENSSL_IS_BORINGSSL - OPENSSL_INIT_SETTINGS* settings = OPENSSL_INIT_new(); - - // --openssl-config=... - if (!per_process::cli_options->openssl_config.empty()) { - const char* conf = per_process::cli_options->openssl_config.c_str(); - OPENSSL_INIT_set_config_filename(settings, conf); - } - - OPENSSL_init_ssl(0, settings); - OPENSSL_INIT_free(settings); - settings = nullptr; -#endif - -#ifdef NODE_FIPS_MODE - /* Override FIPS settings in cnf file, if needed. */ - unsigned long err = 0; // NOLINT(runtime/int) - if (per_process::cli_options->enable_fips_crypto || - per_process::cli_options->force_fips_crypto) { - if (0 == FIPS_mode() && !FIPS_mode_set(1)) { - err = ERR_get_error(); - } - } - if (0 != err) { - fprintf(stderr, - "openssl fips failed: %s\n", - ERR_error_string(err, nullptr)); - UNREACHABLE(); - } -#endif // NODE_FIPS_MODE - - - // Turn off compression. Saves memory and protects against CRIME attacks. - // No-op with OPENSSL_NO_COMP builds of OpenSSL. - sk_SSL_COMP_zero(SSL_COMP_get_compression_methods()); - -#ifndef OPENSSL_NO_ENGINE - ERR_load_ENGINE_strings(); - ENGINE_load_builtin_engines(); -#endif // !OPENSSL_NO_ENGINE - - NodeBIO::GetMethod(); -} - - -#ifndef OPENSSL_NO_ENGINE -void SetEngine(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - CHECK(args.Length() >= 2 && args[0]->IsString()); - uint32_t flags; - if (!args[1]->Uint32Value(env->context()).To(&flags)) return; - - ClearErrorOnReturn clear_error_on_return; - - // Load engine. - const node::Utf8Value engine_id(env->isolate(), args[0]); - char errmsg[1024]; - ENGINE* engine = LoadEngineById(*engine_id, &errmsg); - - if (engine == nullptr) { - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - if (err == 0) - return args.GetReturnValue().Set(false); - return ThrowCryptoError(env, err); - } - - int r = ENGINE_set_default(engine, flags); - ENGINE_free(engine); - if (r == 0) - return ThrowCryptoError(env, ERR_get_error()); - - args.GetReturnValue().Set(true); -} -#endif // !OPENSSL_NO_ENGINE - -#ifdef NODE_FIPS_MODE -void GetFipsCrypto(const FunctionCallbackInfo& args) { - args.GetReturnValue().Set(FIPS_mode() ? 1 : 0); -} +#include "async_wrap-inl.h" +#include "debug_utils-inl.h" +#include "threadpoolwork-inl.h" +#include "memory_tracker-inl.h" +#include "v8.h" -void SetFipsCrypto(const FunctionCallbackInfo& args) { - CHECK(!per_process::cli_options->force_fips_crypto); - Environment* env = Environment::GetCurrent(args); - const bool enabled = FIPS_mode(); - bool enable = args[0]->BooleanValue(env->isolate()); +namespace node { - if (enable == enabled) - return; // No action needed. - if (!FIPS_mode_set(enable)) { - unsigned long err = ERR_get_error(); // NOLINT(runtime/int) - return ThrowCryptoError(env, err); - } -} -#endif /* NODE_FIPS_MODE */ +using v8::Context; +using v8::Local; +using v8::Object; +using v8::Value; +namespace crypto { void Initialize(Local target, Local unused, @@ -6953,91 +43,28 @@ void Initialize(Local target, uv_once(&init_once, InitCryptoOnce); Environment* env = Environment::GetCurrent(context); - SecureContext::Initialize(env, target); - target->Set(env->context(), - FIXED_ONE_BYTE_STRING(env->isolate(), "KeyObjectHandle"), - KeyObjectHandle::Initialize(env)).Check(); - env->SetMethod(target, "createNativeKeyObjectClass", - CreateNativeKeyObjectClass); + + AES::Initialize(env, target); CipherBase::Initialize(env, target); DiffieHellman::Initialize(env, target); + DSAAlg::Initialize(env, target); ECDH::Initialize(env, target); - Hmac::Initialize(env, target); Hash::Initialize(env, target); + HKDFJob::Initialize(env, target); + Hmac::Initialize(env, target); + Keygen::Initialize(env, target); + Keys::Initialize(env, target); + NativeKeyObject::Initialize(env, target); + PBKDF2Job::Initialize(env, target); + Random::Initialize(env, target); + RSAAlg::Initialize(env, target); + ScryptJob::Initialize(env, target); + SecureContext::Initialize(env, target); Sign::Initialize(env, target); + SPKAC::Initialize(env, target); + Timing::Initialize(env, target); + Util::Initialize(env, target); Verify::Initialize(env, target); - - env->SetMethodNoSideEffect(target, "certVerifySpkac", VerifySpkac); - env->SetMethodNoSideEffect(target, "certExportPublicKey", ExportPublicKey); - env->SetMethodNoSideEffect(target, "certExportChallenge", ExportChallenge); - env->SetMethodNoSideEffect(target, "getRootCertificates", - GetRootCertificates); - // Exposed for testing purposes only. - env->SetMethodNoSideEffect(target, "isExtraRootCertsFileLoaded", - IsExtraRootCertsFileLoaded); - - env->SetMethodNoSideEffect(target, "ECDHConvertKey", ConvertKey); -#ifndef OPENSSL_NO_ENGINE - env->SetMethod(target, "setEngine", SetEngine); -#endif // !OPENSSL_NO_ENGINE - -#ifdef NODE_FIPS_MODE - env->SetMethodNoSideEffect(target, "getFipsCrypto", GetFipsCrypto); - env->SetMethod(target, "setFipsCrypto", SetFipsCrypto); -#endif - - env->SetMethod(target, "pbkdf2", PBKDF2); - env->SetMethod(target, "generateKeyPairRSA", GenerateKeyPairRSA); - env->SetMethod(target, "generateKeyPairRSAPSS", GenerateKeyPairRSAPSS); - env->SetMethod(target, "generateKeyPairDSA", GenerateKeyPairDSA); - env->SetMethod(target, "generateKeyPairEC", GenerateKeyPairEC); - env->SetMethod(target, "generateKeyPairNid", GenerateKeyPairNid); - env->SetMethod(target, "generateKeyPairDH", GenerateKeyPairDH); - NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED25519); - NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED448); - NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519); - NODE_DEFINE_CONSTANT(target, EVP_PKEY_X448); - NODE_DEFINE_CONSTANT(target, OPENSSL_EC_NAMED_CURVE); - NODE_DEFINE_CONSTANT(target, OPENSSL_EC_EXPLICIT_CURVE); - NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1); - NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS8); - NODE_DEFINE_CONSTANT(target, kKeyEncodingSPKI); - NODE_DEFINE_CONSTANT(target, kKeyEncodingSEC1); - NODE_DEFINE_CONSTANT(target, kKeyFormatDER); - NODE_DEFINE_CONSTANT(target, kKeyFormatPEM); - NODE_DEFINE_CONSTANT(target, kKeyTypeSecret); - NODE_DEFINE_CONSTANT(target, kKeyTypePublic); - NODE_DEFINE_CONSTANT(target, kKeyTypePrivate); - NODE_DEFINE_CONSTANT(target, kSigEncDER); - NODE_DEFINE_CONSTANT(target, kSigEncP1363); - env->SetMethodNoSideEffect(target, "statelessDH", StatelessDiffieHellman); - env->SetMethod(target, "randomBytes", RandomBytes); - env->SetMethod(target, "signOneShot", SignOneShot); - env->SetMethod(target, "verifyOneShot", VerifyOneShot); - env->SetMethodNoSideEffect(target, "timingSafeEqual", TimingSafeEqual); - env->SetMethodNoSideEffect(target, "getSSLCiphers", GetSSLCiphers); - env->SetMethodNoSideEffect(target, "getCiphers", GetCiphers); - env->SetMethodNoSideEffect(target, "getHashes", GetHashes); - env->SetMethodNoSideEffect(target, "getCurves", GetCurves); - env->SetMethod(target, "publicEncrypt", - PublicKeyCipher::Cipher); - env->SetMethod(target, "privateDecrypt", - PublicKeyCipher::Cipher); - env->SetMethod(target, "privateEncrypt", - PublicKeyCipher::Cipher); - env->SetMethod(target, "publicDecrypt", - PublicKeyCipher::Cipher); -#ifndef OPENSSL_NO_SCRYPT - env->SetMethod(target, "scrypt", Scrypt); -#endif // OPENSSL_NO_SCRYPT } } // namespace crypto diff --git a/src/node_crypto.h b/src/node_crypto.h index d818a850310386..a12f353af90b89 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -24,842 +24,32 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -// ClientHelloParser -#include "crypto/crypto_clienthello.h" - -#include "allocated_buffer.h" -#include "env.h" -#include "base_object.h" -#include "util.h" -#include "node_messaging.h" - -#include "v8.h" - -#include -#include -#include -#include -#include -#include - -namespace node { -namespace crypto { - -// Forcibly clear OpenSSL's error stack on return. This stops stale errors -// from popping up later in the lifecycle of crypto operations where they -// would cause spurious failures. It's a rather blunt method, though. -// ERR_clear_error() isn't necessarily cheap either. -struct ClearErrorOnReturn { - ~ClearErrorOnReturn() { ERR_clear_error(); } -}; - -// Pop errors from OpenSSL's error stack that were added -// between when this was constructed and destructed. -struct MarkPopErrorOnReturn { - MarkPopErrorOnReturn() { ERR_set_mark(); } - ~MarkPopErrorOnReturn() { ERR_pop_to_mark(); } -}; - -// Define smart pointers for the most commonly used OpenSSL types: -using X509Pointer = DeleteFnPtr; -using BIOPointer = DeleteFnPtr; -using SSLCtxPointer = DeleteFnPtr; -using SSLSessionPointer = DeleteFnPtr; -using SSLPointer = DeleteFnPtr; -using PKCS8Pointer = DeleteFnPtr; -using EVPKeyPointer = DeleteFnPtr; -using EVPKeyCtxPointer = DeleteFnPtr; -using EVPMDPointer = DeleteFnPtr; -using RSAPointer = DeleteFnPtr; -using ECPointer = DeleteFnPtr; -using BignumPointer = DeleteFnPtr; -using NetscapeSPKIPointer = DeleteFnPtr; -using ECGroupPointer = DeleteFnPtr; -using ECPointPointer = DeleteFnPtr; -using ECKeyPointer = DeleteFnPtr; -using DHPointer = DeleteFnPtr; -using ECDSASigPointer = DeleteFnPtr; - -extern int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx); - -extern void UseExtraCaCerts(const std::string& file); - -void InitCryptoOnce(); - -class SecureContext final : public BaseObject { - public: - ~SecureContext() override; - - static void Initialize(Environment* env, v8::Local target); - - SSL_CTX* operator*() const { return ctx_.get(); } - - // TODO(joyeecheung): track the memory used by OpenSSL types - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(SecureContext) - SET_SELF_SIZE(SecureContext) - - SSLCtxPointer ctx_; - X509Pointer cert_; - X509Pointer issuer_; -#ifndef OPENSSL_NO_ENGINE - bool client_cert_engine_provided_ = false; - std::unique_ptr> private_key_engine_; -#endif // !OPENSSL_NO_ENGINE - - static const int kMaxSessionSize = 10 * 1024; - - // See TicketKeyCallback - static const int kTicketKeyReturnIndex = 0; - static const int kTicketKeyHMACIndex = 1; - static const int kTicketKeyAESIndex = 2; - static const int kTicketKeyNameIndex = 3; - static const int kTicketKeyIVIndex = 4; - - unsigned char ticket_key_name_[16]; - unsigned char ticket_key_aes_[16]; - unsigned char ticket_key_hmac_[16]; - - protected: - // OpenSSL structures are opaque. This is sizeof(SSL_CTX) for OpenSSL 1.1.1b: - static const int64_t kExternalSize = 1024; - - static void New(const v8::FunctionCallbackInfo& args); - static void Init(const v8::FunctionCallbackInfo& args); - static void SetKey(const v8::FunctionCallbackInfo& args); -#ifndef OPENSSL_NO_ENGINE - static void SetEngineKey(const v8::FunctionCallbackInfo& args); -#endif // !OPENSSL_NO_ENGINE - static void SetCert(const v8::FunctionCallbackInfo& args); - static void AddCACert(const v8::FunctionCallbackInfo& args); - static void AddCRL(const v8::FunctionCallbackInfo& args); - static void AddRootCerts(const v8::FunctionCallbackInfo& args); - static void SetCipherSuites(const v8::FunctionCallbackInfo& args); - static void SetCiphers(const v8::FunctionCallbackInfo& args); - static void SetSigalgs(const v8::FunctionCallbackInfo& args); - static void SetECDHCurve(const v8::FunctionCallbackInfo& args); - static void SetDHParam(const v8::FunctionCallbackInfo& args); - static void SetOptions(const v8::FunctionCallbackInfo& args); - static void SetSessionIdContext( - const v8::FunctionCallbackInfo& args); - static void SetSessionTimeout( - const v8::FunctionCallbackInfo& args); - static void SetMinProto(const v8::FunctionCallbackInfo& args); - static void SetMaxProto(const v8::FunctionCallbackInfo& args); - static void GetMinProto(const v8::FunctionCallbackInfo& args); - static void GetMaxProto(const v8::FunctionCallbackInfo& args); - static void Close(const v8::FunctionCallbackInfo& args); - static void LoadPKCS12(const v8::FunctionCallbackInfo& args); -#ifndef OPENSSL_NO_ENGINE - static void SetClientCertEngine( - const v8::FunctionCallbackInfo& args); -#endif // !OPENSSL_NO_ENGINE - static void GetTicketKeys(const v8::FunctionCallbackInfo& args); - static void SetTicketKeys(const v8::FunctionCallbackInfo& args); - static void SetFreeListLength( - const v8::FunctionCallbackInfo& args); - static void EnableTicketKeyCallback( - const v8::FunctionCallbackInfo& args); - static void CtxGetter(const v8::FunctionCallbackInfo& info); - - template - static void GetCertificate(const v8::FunctionCallbackInfo& args); - - static int TicketKeyCallback(SSL* ssl, - unsigned char* name, - unsigned char* iv, - EVP_CIPHER_CTX* ectx, - HMAC_CTX* hctx, - int enc); - - static int TicketCompatibilityCallback(SSL* ssl, - unsigned char* name, - unsigned char* iv, - EVP_CIPHER_CTX* ectx, - HMAC_CTX* hctx, - int enc); - - SecureContext(Environment* env, v8::Local wrap); - void Reset(); -}; - -// SSLWrap implicitly depends on the inheriting class' handle having an -// internal pointer to the Base class. -template -class SSLWrap { - public: - enum Kind { - kClient, - kServer - }; - - SSLWrap(Environment* env, SecureContext* sc, Kind kind) - : env_(env), - kind_(kind), - next_sess_(nullptr), - session_callbacks_(false), - awaiting_new_session_(false), - cert_cb_(nullptr), - cert_cb_arg_(nullptr), - cert_cb_running_(false) { - ssl_.reset(SSL_new(sc->ctx_.get())); - CHECK(ssl_); - env_->isolate()->AdjustAmountOfExternalAllocatedMemory(kExternalSize); - } - - virtual ~SSLWrap() { - DestroySSL(); - } - - inline void enable_session_callbacks() { session_callbacks_ = true; } - inline bool is_server() const { return kind_ == kServer; } - inline bool is_client() const { return kind_ == kClient; } - inline bool is_awaiting_new_session() const { return awaiting_new_session_; } - inline bool is_waiting_cert_cb() const { return cert_cb_ != nullptr; } - - void MemoryInfo(MemoryTracker* tracker) const; - - protected: - typedef void (*CertCb)(void* arg); - - // OpenSSL structures are opaque. Estimate SSL memory size for OpenSSL 1.1.1b: - // SSL: 6224 - // SSL->SSL3_STATE: 1040 - // ...some buffers: 42 * 1024 - // NOTE: Actually it is much more than this - static const int64_t kExternalSize = 6224 + 1040 + 42 * 1024; - - static void ConfigureSecureContext(SecureContext* sc); - static void AddMethods(Environment* env, v8::Local t); - - static SSL_SESSION* GetSessionCallback(SSL* s, - const unsigned char* key, - int len, - int* copy); - static int NewSessionCallback(SSL* s, SSL_SESSION* sess); - static void KeylogCallback(const SSL* s, const char* line); - static void OnClientHello(void* arg, - const ClientHelloParser::ClientHello& hello); - - static void GetPeerCertificate( - const v8::FunctionCallbackInfo& args); - static void GetCertificate(const v8::FunctionCallbackInfo& args); - static void GetFinished(const v8::FunctionCallbackInfo& args); - static void GetPeerFinished(const v8::FunctionCallbackInfo& args); - static void GetSession(const v8::FunctionCallbackInfo& args); - static void SetSession(const v8::FunctionCallbackInfo& args); - static void LoadSession(const v8::FunctionCallbackInfo& args); - static void IsSessionReused(const v8::FunctionCallbackInfo& args); - static void VerifyError(const v8::FunctionCallbackInfo& args); - static void GetCipher(const v8::FunctionCallbackInfo& args); - static void GetSharedSigalgs(const v8::FunctionCallbackInfo& args); - static void ExportKeyingMaterial( - const v8::FunctionCallbackInfo& args); - static void EndParser(const v8::FunctionCallbackInfo& args); - static void CertCbDone(const v8::FunctionCallbackInfo& args); - static void Renegotiate(const v8::FunctionCallbackInfo& args); - static void GetTLSTicket(const v8::FunctionCallbackInfo& args); - static void NewSessionDone(const v8::FunctionCallbackInfo& args); - static void SetOCSPResponse(const v8::FunctionCallbackInfo& args); - static void RequestOCSP(const v8::FunctionCallbackInfo& args); - static void GetEphemeralKeyInfo( - const v8::FunctionCallbackInfo& args); - static void GetProtocol(const v8::FunctionCallbackInfo& args); - -#ifdef SSL_set_max_send_fragment - static void SetMaxSendFragment( - const v8::FunctionCallbackInfo& args); -#endif // SSL_set_max_send_fragment - - static void GetALPNNegotiatedProto( - const v8::FunctionCallbackInfo& args); - static void SetALPNProtocols(const v8::FunctionCallbackInfo& args); - static int SelectALPNCallback(SSL* s, - const unsigned char** out, - unsigned char* outlen, - const unsigned char* in, - unsigned int inlen, - void* arg); - static int TLSExtStatusCallback(SSL* s, void* arg); - static int SSLCertCallback(SSL* s, void* arg); - - void DestroySSL(); - void WaitForCertCb(CertCb cb, void* arg); - int SetCACerts(SecureContext* sc); - - inline Environment* ssl_env() const { - return env_; - } - - Environment* const env_; - Kind kind_; - SSLSessionPointer next_sess_; - SSLPointer ssl_; - bool session_callbacks_; - bool awaiting_new_session_; - - // SSL_set_cert_cb - CertCb cert_cb_; - void* cert_cb_arg_; - bool cert_cb_running_; - - ClientHelloParser hello_parser_; - - v8::Global ocsp_response_; - BaseObjectPtr sni_context_; - - friend class SecureContext; -}; - -// A helper class representing a read-only byte array. When deallocated, its -// contents are zeroed. -class ByteSource { - public: - ByteSource() = default; - ByteSource(ByteSource&& other); - ~ByteSource(); - - ByteSource& operator=(ByteSource&& other); - - const char* get() const; - size_t size() const; - - inline operator bool() const { - return data_ != nullptr; - } - - static ByteSource Allocated(char* data, size_t size); - static ByteSource Foreign(const char* data, size_t size); - - static ByteSource FromStringOrBuffer(Environment* env, - v8::Local value); - - static ByteSource FromString(Environment* env, - v8::Local str, - bool ntc = false); - - static ByteSource FromBuffer(v8::Local buffer, - bool ntc = false); - - static ByteSource NullTerminatedCopy(Environment* env, - v8::Local value); - - static ByteSource FromSymmetricKeyObjectHandle(v8::Local handle); - - ByteSource(const ByteSource&) = delete; - ByteSource& operator=(const ByteSource&) = delete; - - private: - const char* data_ = nullptr; - char* allocated_data_ = nullptr; - size_t size_ = 0; - - ByteSource(const char* data, char* allocated_data, size_t size); -}; - -enum PKEncodingType { - // RSAPublicKey / RSAPrivateKey according to PKCS#1. - kKeyEncodingPKCS1, - // PrivateKeyInfo or EncryptedPrivateKeyInfo according to PKCS#8. - kKeyEncodingPKCS8, - // SubjectPublicKeyInfo according to X.509. - kKeyEncodingSPKI, - // ECPrivateKey according to SEC1. - kKeyEncodingSEC1 -}; - -enum PKFormatType { - kKeyFormatDER, - kKeyFormatPEM -}; - -struct AsymmetricKeyEncodingConfig { - bool output_key_object_; - PKFormatType format_; - v8::Maybe type_ = v8::Nothing(); -}; - -typedef AsymmetricKeyEncodingConfig PublicKeyEncodingConfig; - -struct PrivateKeyEncodingConfig : public AsymmetricKeyEncodingConfig { - const EVP_CIPHER* cipher_; - ByteSource passphrase_; -}; - -enum KeyType { - kKeyTypeSecret, - kKeyTypePublic, - kKeyTypePrivate -}; - -// This uses the built-in reference counter of OpenSSL to manage an EVP_PKEY -// which is slightly more efficient than using a shared pointer and easier to -// use. -class ManagedEVPPKey { - public: - ManagedEVPPKey() = default; - explicit ManagedEVPPKey(EVPKeyPointer&& pkey); - ManagedEVPPKey(const ManagedEVPPKey& that); - ManagedEVPPKey& operator=(const ManagedEVPPKey& that); - - operator bool() const; - EVP_PKEY* get() const; - - private: - EVPKeyPointer pkey_; -}; - -// Objects of this class can safely be shared among threads. -class KeyObjectData { - public: - static std::shared_ptr CreateSecret( - v8::Local abv); - static std::shared_ptr CreateAsymmetric( - KeyType type, const ManagedEVPPKey& pkey); - - KeyType GetKeyType() const; - - // These functions allow unprotected access to the raw key material and should - // only be used to implement cryptographic operations requiring the key. - ManagedEVPPKey GetAsymmetricKey() const; - const char* GetSymmetricKey() const; - size_t GetSymmetricKeySize() const; - - private: - KeyObjectData(std::unique_ptr> symmetric_key, - unsigned int symmetric_key_len) - : key_type_(KeyType::kKeyTypeSecret), - symmetric_key_(std::move(symmetric_key)), - symmetric_key_len_(symmetric_key_len), - asymmetric_key_() {} - - KeyObjectData(KeyType type, const ManagedEVPPKey& pkey) - : key_type_(type), - symmetric_key_(), - symmetric_key_len_(0), - asymmetric_key_{pkey} {} - - const KeyType key_type_; - const std::unique_ptr> symmetric_key_; - const unsigned int symmetric_key_len_; - const ManagedEVPPKey asymmetric_key_; -}; - -class KeyObjectHandle : public BaseObject { - public: - static v8::Local Initialize(Environment* env); - - static v8::MaybeLocal Create(Environment* env, - std::shared_ptr data); - - // TODO(tniessen): track the memory used by OpenSSL types - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(KeyObjectHandle) - SET_SELF_SIZE(KeyObjectHandle) - - const std::shared_ptr& Data(); - - protected: - static void New(const v8::FunctionCallbackInfo& args); - - static void Init(const v8::FunctionCallbackInfo& args); - - static void GetAsymmetricKeyType( - const v8::FunctionCallbackInfo& args); - v8::Local GetAsymmetricKeyType() const; - - static void GetSymmetricKeySize( - const v8::FunctionCallbackInfo& args); - - static void Export(const v8::FunctionCallbackInfo& args); - v8::Local ExportSecretKey() const; - v8::MaybeLocal ExportPublicKey( - const PublicKeyEncodingConfig& config) const; - v8::MaybeLocal ExportPrivateKey( - const PrivateKeyEncodingConfig& config) const; - - KeyObjectHandle(Environment* env, - v8::Local wrap); - - private: - std::shared_ptr data_; -}; - -class NativeKeyObject : public BaseObject { - public: - static void New(const v8::FunctionCallbackInfo& args); - - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(NativeKeyObject) - SET_SELF_SIZE(NativeKeyObject) - - class KeyObjectTransferData : public worker::TransferData { - public: - explicit KeyObjectTransferData(const std::shared_ptr& data) - : data_(data) {} - - BaseObjectPtr Deserialize( - Environment* env, - v8::Local context, - std::unique_ptr self) override; - - SET_MEMORY_INFO_NAME(KeyObjectTransferData) - SET_SELF_SIZE(KeyObjectTransferData) - SET_NO_MEMORY_INFO() - - private: - std::shared_ptr data_; - }; - - BaseObject::TransferMode GetTransferMode() const override; - std::unique_ptr CloneForMessaging() const override; - - private: - NativeKeyObject(Environment* env, - v8::Local wrap, - const std::shared_ptr& handle_data) - : BaseObject(env, wrap), - handle_data_(handle_data) { - MakeWeak(); - } - - std::shared_ptr handle_data_; -}; - -class CipherBase : public BaseObject { - public: - static void Initialize(Environment* env, v8::Local target); - - // TODO(joyeecheung): track the memory used by OpenSSL types - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(CipherBase) - SET_SELF_SIZE(CipherBase) - - protected: - enum CipherKind { - kCipher, - kDecipher - }; - enum UpdateResult { - kSuccess, - kErrorMessageSize, - kErrorState - }; - enum AuthTagState { - kAuthTagUnknown, - kAuthTagKnown, - kAuthTagPassedToOpenSSL - }; - static const unsigned kNoAuthTagLength = static_cast(-1); - - void CommonInit(const char* cipher_type, - const EVP_CIPHER* cipher, - const unsigned char* key, - int key_len, - const unsigned char* iv, - int iv_len, - unsigned int auth_tag_len); - void Init(const char* cipher_type, - const char* key_buf, - int key_buf_len, - unsigned int auth_tag_len); - void InitIv(const char* cipher_type, - const unsigned char* key, - int key_len, - const unsigned char* iv, - int iv_len, - unsigned int auth_tag_len); - bool InitAuthenticated(const char* cipher_type, int iv_len, - unsigned int auth_tag_len); - bool CheckCCMMessageLength(int message_len); - UpdateResult Update(const char* data, int len, AllocatedBuffer* out); - bool Final(AllocatedBuffer* out); - bool SetAutoPadding(bool auto_padding); - - bool IsAuthenticatedMode() const; - bool SetAAD(const char* data, unsigned int len, int plaintext_len); - bool MaybePassAuthTagToOpenSSL(); - - static void New(const v8::FunctionCallbackInfo& args); - static void Init(const v8::FunctionCallbackInfo& args); - static void InitIv(const v8::FunctionCallbackInfo& args); - static void Update(const v8::FunctionCallbackInfo& args); - static void Final(const v8::FunctionCallbackInfo& args); - static void SetAutoPadding(const v8::FunctionCallbackInfo& args); - - static void GetAuthTag(const v8::FunctionCallbackInfo& args); - static void SetAuthTag(const v8::FunctionCallbackInfo& args); - static void SetAAD(const v8::FunctionCallbackInfo& args); - - CipherBase(Environment* env, v8::Local wrap, CipherKind kind); - - private: - DeleteFnPtr ctx_; - const CipherKind kind_; - AuthTagState auth_tag_state_; - unsigned int auth_tag_len_; - char auth_tag_[EVP_GCM_TLS_TAG_LEN]; - bool pending_auth_failed_; - int max_message_size_; -}; - -class Hmac : public BaseObject { - public: - static void Initialize(Environment* env, v8::Local target); - - // TODO(joyeecheung): track the memory used by OpenSSL types - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(Hmac) - SET_SELF_SIZE(Hmac) - - protected: - void HmacInit(const char* hash_type, const char* key, int key_len); - bool HmacUpdate(const char* data, int len); - - static void New(const v8::FunctionCallbackInfo& args); - static void HmacInit(const v8::FunctionCallbackInfo& args); - static void HmacUpdate(const v8::FunctionCallbackInfo& args); - static void HmacDigest(const v8::FunctionCallbackInfo& args); - - Hmac(Environment* env, v8::Local wrap); - - private: - DeleteFnPtr ctx_; -}; - -class Hash final : public BaseObject { - public: - ~Hash() override; - - static void Initialize(Environment* env, v8::Local target); - - // TODO(joyeecheung): track the memory used by OpenSSL types - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(Hash) - SET_SELF_SIZE(Hash) - - bool HashInit(const EVP_MD* md, v8::Maybe xof_md_len); - bool HashUpdate(const char* data, int len); - - protected: - static void New(const v8::FunctionCallbackInfo& args); - static void HashUpdate(const v8::FunctionCallbackInfo& args); - static void HashDigest(const v8::FunctionCallbackInfo& args); - - Hash(Environment* env, v8::Local wrap); - - private: - EVPMDPointer mdctx_; - bool has_md_; - unsigned int md_len_; - unsigned char* md_value_; -}; - -class SignBase : public BaseObject { - public: - typedef enum { - kSignOk, - kSignUnknownDigest, - kSignInit, - kSignNotInitialised, - kSignUpdate, - kSignPrivateKey, - kSignPublicKey, - kSignMalformedSignature - } Error; - - SignBase(Environment* env, v8::Local wrap); - - Error Init(const char* sign_type); - Error Update(const char* data, int len); - - // TODO(joyeecheung): track the memory used by OpenSSL types - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(SignBase) - SET_SELF_SIZE(SignBase) - - protected: - void CheckThrow(Error error); - - EVPMDPointer mdctx_; -}; - -enum DSASigEnc { - kSigEncDER, kSigEncP1363 -}; - -class Sign : public SignBase { - public: - static void Initialize(Environment* env, v8::Local target); - - struct SignResult { - Error error; - AllocatedBuffer signature; - - explicit SignResult( - Error err, - AllocatedBuffer&& sig = AllocatedBuffer()) - : error(err), signature(std::move(sig)) {} - }; - - SignResult SignFinal( - const ManagedEVPPKey& pkey, - int padding, - const v8::Maybe& saltlen, - DSASigEnc dsa_sig_enc); - - protected: - static void New(const v8::FunctionCallbackInfo& args); - static void SignInit(const v8::FunctionCallbackInfo& args); - static void SignUpdate(const v8::FunctionCallbackInfo& args); - static void SignFinal(const v8::FunctionCallbackInfo& args); - - Sign(Environment* env, v8::Local wrap); -}; - -class Verify : public SignBase { - public: - static void Initialize(Environment* env, v8::Local target); - - Error VerifyFinal(const ManagedEVPPKey& key, - const ByteSource& sig, - int padding, - const v8::Maybe& saltlen, - bool* verify_result); - - protected: - static void New(const v8::FunctionCallbackInfo& args); - static void VerifyInit(const v8::FunctionCallbackInfo& args); - static void VerifyUpdate(const v8::FunctionCallbackInfo& args); - static void VerifyFinal(const v8::FunctionCallbackInfo& args); - - Verify(Environment* env, v8::Local wrap); -}; - -class PublicKeyCipher { - public: - typedef int (*EVP_PKEY_cipher_init_t)(EVP_PKEY_CTX* ctx); - typedef int (*EVP_PKEY_cipher_t)(EVP_PKEY_CTX* ctx, - unsigned char* out, size_t* outlen, - const unsigned char* in, size_t inlen); - - enum Operation { - kPublic, - kPrivate - }; - - template - static bool Cipher(Environment* env, - const ManagedEVPPKey& pkey, - int padding, - const EVP_MD* digest, - const void* oaep_label, - size_t oaep_label_size, - const unsigned char* data, - int len, - AllocatedBuffer* out); - - template - static void Cipher(const v8::FunctionCallbackInfo& args); -}; - -class DiffieHellman : public BaseObject { - public: - static void Initialize(Environment* env, v8::Local target); - - bool Init(int primeLength, int g); - bool Init(const char* p, int p_len, int g); - bool Init(const char* p, int p_len, const char* g, int g_len); - - protected: - static void DiffieHellmanGroup( - const v8::FunctionCallbackInfo& args); - static void New(const v8::FunctionCallbackInfo& args); - static void GenerateKeys(const v8::FunctionCallbackInfo& args); - static void GetPrime(const v8::FunctionCallbackInfo& args); - static void GetGenerator(const v8::FunctionCallbackInfo& args); - static void GetPublicKey(const v8::FunctionCallbackInfo& args); - static void GetPrivateKey(const v8::FunctionCallbackInfo& args); - static void ComputeSecret(const v8::FunctionCallbackInfo& args); - static void SetPublicKey(const v8::FunctionCallbackInfo& args); - static void SetPrivateKey(const v8::FunctionCallbackInfo& args); - static void VerifyErrorGetter( - const v8::FunctionCallbackInfo& args); - - DiffieHellman(Environment* env, v8::Local wrap); - - // TODO(joyeecheung): track the memory used by OpenSSL types - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(DiffieHellman) - SET_SELF_SIZE(DiffieHellman) - - private: - static void GetField(const v8::FunctionCallbackInfo& args, - const BIGNUM* (*get_field)(const DH*), - const char* err_if_null); - static void SetKey(const v8::FunctionCallbackInfo& args, - int (*set_field)(DH*, BIGNUM*), const char* what); - bool VerifyContext(); - - int verifyError_; - DHPointer dh_; -}; - -class ECDH final : public BaseObject { - public: - ~ECDH() override; - - static void Initialize(Environment* env, v8::Local target); - static ECPointPointer BufferToPoint(Environment* env, - const EC_GROUP* group, - v8::Local buf); - - // TODO(joyeecheung): track the memory used by OpenSSL types - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(ECDH) - SET_SELF_SIZE(ECDH) - - protected: - ECDH(Environment* env, v8::Local wrap, ECKeyPointer&& key); - - static void New(const v8::FunctionCallbackInfo& args); - static void GenerateKeys(const v8::FunctionCallbackInfo& args); - static void ComputeSecret(const v8::FunctionCallbackInfo& args); - static void GetPrivateKey(const v8::FunctionCallbackInfo& args); - static void SetPrivateKey(const v8::FunctionCallbackInfo& args); - static void GetPublicKey(const v8::FunctionCallbackInfo& args); - static void SetPublicKey(const v8::FunctionCallbackInfo& args); - - bool IsKeyPairValid(); - bool IsKeyValidForCurve(const BignumPointer& private_key); - - ECKeyPointer key_; - const EC_GROUP* group_; -}; - -bool EntropySource(unsigned char* buffer, size_t length); -#ifndef OPENSSL_NO_ENGINE -void SetEngine(const v8::FunctionCallbackInfo& args); -#endif // !OPENSSL_NO_ENGINE -void InitCrypto(v8::Local target); - -void ThrowCryptoError(Environment* env, - unsigned long err, // NOLINT(runtime/int) - const char* message = nullptr); - -template -inline T* MallocOpenSSL(size_t count) { - void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); - CHECK_IMPLIES(mem == nullptr, count == 0); - return static_cast(mem); -} - -} // namespace crypto -} // namespace node +// All of the crypto definitions previously contained in this header +// have been split across multiple headers in src/crypto. This header +// remains for convenience for any code that still imports it. New +// code should include the relevant src/crypto headers directly. +#include "crypto/crypto_aes.h" +#include "crypto/crypto_bio.h" +#include "crypto/crypto_cipher.h" +#include "crypto/crypto_context.h" +#include "crypto/crypto_dh.h" +#include "crypto/crypto_dsa.h" +#include "crypto/crypto_ecdh.h" +#include "crypto/crypto_groups.h" +#include "crypto/crypto_hash.h" +#include "crypto/crypto_hkdf.h" +#include "crypto/crypto_hmac.h" +#include "crypto/crypto_keygen.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_pbkdf2.h" +#include "crypto/crypto_random.h" +#include "crypto/crypto_rsa.h" +#include "crypto/crypto_scrypt.h" +#include "crypto/crypto_sig.h" +#include "crypto/crypto_spkac.h" +#include "crypto/crypto_ssl.h" +#include "crypto/crypto_timing.h" +#include "crypto/crypto_util.h" #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/node_errors.h b/src/node_errors.h index 921fb09887f9e7..873478c361f34e 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -33,9 +33,26 @@ void OnFatalError(const char* location, const char* message); V(ERR_BUFFER_TOO_LARGE, Error) \ V(ERR_CONSTRUCT_CALL_REQUIRED, TypeError) \ V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \ + V(ERR_CRYPTO_INITIALIZATION_FAILED, Error) \ + V(ERR_CRYPTO_INVALID_AUTH_TAG, TypeError) \ + V(ERR_CRYPTO_INVALID_COUNTER, TypeError) \ + V(ERR_CRYPTO_INVALID_CURVE, TypeError) \ + V(ERR_CRYPTO_INVALID_DIGEST, TypeError) \ + V(ERR_CRYPTO_INVALID_IV, TypeError) \ + V(ERR_CRYPTO_INVALID_JWK, TypeError) \ + V(ERR_CRYPTO_INVALID_KEYLEN, RangeError) \ + V(ERR_CRYPTO_INVALID_KEYPAIR, RangeError) \ + V(ERR_CRYPTO_INVALID_KEYTYPE, RangeError) \ + V(ERR_CRYPTO_INVALID_MESSAGELEN, RangeError) \ + V(ERR_CRYPTO_INVALID_SCRYPT_PARAMS, RangeError) \ + V(ERR_CRYPTO_INVALID_STATE, Error) \ + V(ERR_CRYPTO_INVALID_TAG_LENGTH, RangeError) \ + V(ERR_CRYPTO_OPERATION_FAILED, Error) \ V(ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH, RangeError) \ V(ERR_CRYPTO_UNKNOWN_CIPHER, Error) \ V(ERR_CRYPTO_UNKNOWN_DH_GROUP, Error) \ + V(ERR_CRYPTO_UNSUPPORTED_OPERATION, Error) \ + V(ERR_CRYPTO_JOB_INIT_FAILED, Error) \ V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \ V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \ @@ -89,10 +106,27 @@ void OnFatalError(const char* location, const char* message); "Buffer is not available for the current Context") \ V(ERR_CONSTRUCT_CALL_INVALID, "Constructor cannot be called") \ V(ERR_CONSTRUCT_CALL_REQUIRED, "Cannot call constructor without `new`") \ + V(ERR_CRYPTO_INITIALIZATION_FAILED, "Initialization failed") \ + V(ERR_CRYPTO_INVALID_AUTH_TAG, "Invalid authentication tag") \ + V(ERR_CRYPTO_INVALID_COUNTER, "Invalid counter") \ + V(ERR_CRYPTO_INVALID_CURVE, "Invalid EC curve name") \ + V(ERR_CRYPTO_INVALID_DIGEST, "Invalid digest") \ + V(ERR_CRYPTO_INVALID_IV, "Invalid initialization vector") \ + V(ERR_CRYPTO_INVALID_JWK, "Invalid JWK format") \ + V(ERR_CRYPTO_INVALID_KEYLEN, "Invalid key length") \ + V(ERR_CRYPTO_INVALID_KEYPAIR, "Invalid key pair") \ + V(ERR_CRYPTO_INVALID_KEYTYPE, "Invalid key type") \ + V(ERR_CRYPTO_INVALID_MESSAGELEN, "Invalid message length") \ + V(ERR_CRYPTO_INVALID_SCRYPT_PARAMS, "Invalid scrypt params") \ + V(ERR_CRYPTO_INVALID_STATE, "Invalid state") \ + V(ERR_CRYPTO_INVALID_TAG_LENGTH, "Invalid taglength") \ + V(ERR_CRYPTO_OPERATION_FAILED, "Operation failed") \ V(ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH, \ "Input buffers must have the same byte length") \ V(ERR_CRYPTO_UNKNOWN_CIPHER, "Unknown cipher") \ V(ERR_CRYPTO_UNKNOWN_DH_GROUP, "Unknown DH group") \ + V(ERR_CRYPTO_UNSUPPORTED_OPERATION, "Unsupported crypto operation") \ + V(ERR_CRYPTO_JOB_INIT_FAILED, "Failed to initialize crypto job config") \ V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, \ "Context not associated with Node.js environment") \ V(ERR_INVALID_TRANSFER_OBJECT, "Found invalid object in transferList") \ diff --git a/src/node_native_module.cc b/src/node_native_module.cc index e3474c4ff9026c..f7d73544d2dbf1 100644 --- a/src/node_native_module.cc +++ b/src/node_native_module.cc @@ -93,6 +93,7 @@ void NativeModuleLoader::InitializeModuleCategories() { #if !HAVE_OPENSSL "crypto", + "crypto/promises", "https", "http2", "tls", diff --git a/src/node_task_queue.cc b/src/node_task_queue.cc index 9a22cabdedf4a2..0f29fe0a59f0fc 100644 --- a/src/node_task_queue.cc +++ b/src/node_task_queue.cc @@ -60,7 +60,7 @@ void PromiseRejectCallback(PromiseRejectMessage message) { Environment* env = Environment::GetCurrent(isolate); - if (env == nullptr) return; + if (env == nullptr || !env->can_call_into_js()) return; Local callback = env->promise_reject_callback(); // The promise is rejected before JS land calls SetPromiseRejectCallback diff --git a/src/quic/node_quic.cc b/src/quic/node_quic.cc index 2228ef3f520e7b..f34c8669f94809 100644 --- a/src/quic/node_quic.cc +++ b/src/quic/node_quic.cc @@ -1,7 +1,7 @@ #include "debug_utils-inl.h" #include "node.h" #include "env-inl.h" -#include "node_crypto.h" // SecureContext +#include "crypto/crypto_context.h" #include "crypto/crypto_common.h" #include "node_errors.h" #include "node_process.h" diff --git a/src/quic/node_quic_crypto.cc b/src/quic/node_quic_crypto.cc index 3bbf04b9be3cf8..74bb4f18783842 100644 --- a/src/quic/node_quic_crypto.cc +++ b/src/quic/node_quic_crypto.cc @@ -1,6 +1,8 @@ #include "node_quic_crypto.h" #include "env-inl.h" #include "node_crypto.h" +#include "crypto/crypto_util.h" +#include "crypto/crypto_context.h" #include "crypto/crypto_common.h" #include "node_process.h" #include "node_quic_session-inl.h" diff --git a/src/string_bytes.cc b/src/string_bytes.cc index b03268c49afcbe..556dba97c093f3 100644 --- a/src/string_bytes.cc +++ b/src/string_bytes.cc @@ -358,6 +358,8 @@ size_t StringBytes::Write(Isolate* isolate, break; } + case BASE64URL: + // Fall through case BASE64: if (str->IsExternalOneByte()) { auto ext = str->GetExternalOneByteStringResource(); @@ -425,6 +427,8 @@ Maybe StringBytes::StorageSize(Isolate* isolate, data_size = str->Length() * sizeof(uint16_t); break; + case BASE64URL: + // Fall through case BASE64: data_size = base64_decoded_size_fast(str->Length()); break; @@ -466,6 +470,8 @@ Maybe StringBytes::Size(Isolate* isolate, case UCS2: return Just(str->Length() * sizeof(uint16_t)); + case BASE64URL: + // Fall through case BASE64: { String::Value value(isolate, str); return Just(base64_decoded_size(*value, value.length())); @@ -691,6 +697,20 @@ MaybeLocal StringBytes::Encode(Isolate* isolate, return ExternOneByteString::New(isolate, dst, dlen, error); } + case BASE64URL: { + size_t dlen = base64_encoded_size(buflen, Base64Mode::URL); + char* dst = node::UncheckedMalloc(dlen); + if (dst == nullptr) { + *error = node::ERR_MEMORY_ALLOCATION_FAILED(isolate); + return MaybeLocal(); + } + + size_t written = base64_encode(buf, buflen, dst, dlen, Base64Mode::URL); + CHECK_EQ(written, dlen); + + return ExternOneByteString::New(isolate, dst, dlen, error); + } + case HEX: { size_t dlen = buflen * 2; char* dst = node::UncheckedMalloc(dlen); diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc index 1a1c5c41fec1d7..908e3899db35be 100644 --- a/src/tls_wrap.cc +++ b/src/tls_wrap.cc @@ -25,10 +25,10 @@ #include "debug_utils-inl.h" #include "memory_tracker-inl.h" #include "node_buffer.h" // Buffer -#include "node_crypto.h" // SecureContext +#include "crypto/crypto_context.h" +#include "crypto/crypto_util.h" #include "crypto/crypto_bio.h" // NodeBIO -// ClientHelloParser -#include "crypto/crypto_clienthello-inl.h" +#include "crypto/crypto_clienthello-inl.h" // ClientHelloParser #include "node_errors.h" #include "stream_base-inl.h" #include "util-inl.h" diff --git a/src/tls_wrap.h b/src/tls_wrap.h index 579f53cf9a391a..3b9a6c85987d8f 100644 --- a/src/tls_wrap.h +++ b/src/tls_wrap.h @@ -25,6 +25,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include "node_crypto.h" // SSLWrap +#include "crypto/crypto_ssl.h" #include "allocated_buffer.h" #include "async_wrap.h" diff --git a/test/cctest/test_base64.cc b/test/cctest/test_base64.cc index e92498da61b871..167e5e27bb05fe 100644 --- a/test/cctest/test_base64.cc +++ b/test/cctest/test_base64.cc @@ -44,6 +44,20 @@ TEST(Base64Test, Encode) { "IGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLg=="); } +TEST(Base64Test, EncodeURL) { + auto test = [](const char* string, const char* base64_string) { + const size_t len = strlen(base64_string); + char* const buffer = new char[len + 1]; + buffer[len] = 0; + base64_encode(string, strlen(string), buffer, len, node::Base64Mode::URL); + EXPECT_STREQ(base64_string, buffer); + delete[] buffer; + }; + + test("\x68\xd9\x16\x25\x5c\x1e\x40\x92\x2d\xfb", "aNkWJVweQJIt-w"); + test("\xac\xc7\x93\xaa\x83\x6f\xc3\xe3\x3f\x75", "rMeTqoNvw-M_dQ"); +} + TEST(Base64Test, Decode) { auto test = [](const char* base64_string, const char* string) { const size_t len = strlen(string); @@ -75,6 +89,7 @@ TEST(Base64Test, Decode) { test("YWJj ZGVm", "abcdef"); test("Y W J j Z G V m", "abcdef"); test("Y W\n JjZ \nG Vm", "abcdef"); + test("rMeTqoNvw-M_dQ", "\xac\xc7\x93\xaa\x83\x6f\xc3\xe3\x3f\x75"); const char* text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " diff --git a/test/fixtures/crypto/aes_cbc.js b/test/fixtures/crypto/aes_cbc.js new file mode 100644 index 00000000000000..2b6dbd9fd33e92 --- /dev/null +++ b/test/fixtures/crypto/aes_cbc.js @@ -0,0 +1,144 @@ +'use strict'; + +module.exports = function() { + const kPlaintext = + Buffer.from('546869732073706563696669636174696f6e206465736372696265' + + '732061204a6176615363726970742041504920666f722070657266' + + '6f726d696e672062617369632063727970746f6772617068696320' + + '6f7065726174696f6e7320696e20776562206170706c6963617469' + + '6f6e732c20737563682061732068617368696e672c207369676e61' + + '747572652067656e65726174696f6e20616e642076657269666963' + + '6174696f6e2c20616e6420656e6372797074696f6e20616e642064' + + '656372797074696f6e2e204164646974696f6e616c6c792c206974' + + '2064657363726962657320616e2041504920666f72206170706c69' + + '636174696f6e7320746f2067656e657261746520616e642f6f7220' + + '6d616e61676520746865206b6579696e67206d6174657269616c20' + + '6e656365737361727920746f20706572666f726d20746865736520' + + '6f7065726174696f6e732e205573657320666f7220746869732041' + + '50492072616e67652066726f6d2075736572206f72207365727669' + + '63652061757468656e7469636174696f6e2c20646f63756d656e74' + + '206f7220636f6465207369676e696e672c20616e64207468652063' + + '6f6e666964656e7469616c69747920616e6420696e746567726974' + + '79206f6620636f6d6d756e69636174696f6e732e', 'hex'); + + const kKeyBytes = { + '128': Buffer.from('dec0d4fcbf3c4741c892dabd1cd4c04e', 'hex'), + '256': Buffer.from('67693823fb1d58073f91ece9cc3af910e5532616a4d27b1' + + '3eb7b74d8000bbf30', 'hex') + } + + const iv = Buffer.from('55aaf89ba89413d54ea727a76c27a284', 'hex'); + + const kCipherText = { + '128': Buffer.from( + '237f03fee70872e78faec148ddbd01bd77cb96e3381ef4ece2afea17a7afd37' + + 'ccbe461df9c4d58aea6bbbae1b05cfab1e129877cd756c6867c319a3ce05da5' + + '0cbef5f1a4f7dce345f269d06cdec1df00e2d927a04e93bf2699e8ceddfe19b' + + '9f907b5d76862a3c2a167a1eda70af2255002ffad60146aaa6e5026887f1055' + + 'f44eac386a0373823aba81ecfffbb270189f52fc01b2845c287d12877440b21' + + 'fae577272da4e6f00effc4f3f773a764e37f92482e1cd0d4c61d6faaee84367' + + 'd3b2ce2081bcf364473f9a9fc87d228a2749824b61cbcc6ff44bbab52bcfaf9' + + '262cf1b175a90a113ebc75d62ee48869ddccf42a7ec5e390003cafa371aa314' + + '85bf43143f96cb57d82c39bcec40506f441a0c0aa35203bf1347bac4b154f40' + + '74e29accb1be1e76cce8dddfdccdc8614823671517fc51b65799fdfc173be0c' + + '99aee7c45c8e9c3dbd031299cebe3aff9a7342176b5edc9cdce4f14206b82ce' + + 'ef933f06d8ed0bd0b7546aad9aad842e712af79dd101d8b37675bef6f1d6c5e' + + 'b38a8649821d45b6c0f996a54f2f5bcbe23f57343cacbfbeb3ab9bcd58ac6f3' + + 'b28c6fad194b173c8282ba5a74374409ff051fdeb898431dfd6ac35072fb8df' + + '783b33217c93dd1b3c10fe187373d64b496188d6d1b16a47fed35e3968aaa82' + + '3255dcbc7261c54', 'hex'), + '256': Buffer.from( + '29d5798cb5e3c86164853ae36a73193f4d331a39ee8c633f47d38054731aec3' + + '46751910e65a1b53a87c138a7d6dc053455deb71b6586569b40947cd4dbfb41' + + '2a202c80023280dd16ee38bd531c7a799dd7879780e9c141be5694bf8cc4780' + + '8ac64a6fe29f54b3806a6f4b26fea17046b061684bbe61147ac71ee4904b45a' + + '674d2533767081eec707de7aad1ee8b2e9ea90620eea704d443e3e9fe665622' + + 'b02cc459c566880228007ad5a7821683b2dfb5d33f0e83c5ebd865a14b87a1d' + + 'e155d526749f50456aa8ecc9458c62f02da085e16a2df5d4a0b0801b7299b69' + + '091d648c48ab7573df59638529ee032727d7aaca181ea463ff5881e880980dc' + + 'e59ddec395bd46084728c35d1b07eaa4af66c99573f8b37d427ac21a3ddac6b' + + '5988cc730941f0ef1c5034680ef20560fd756f5be5f8d296f00e81c984357c5' + + 'ff760dfb475416e786bcaf738a25c705eec70263cb4b3ee71596ef5ec9b9db3' + + 'ad2e497834c94683c4a5206a831fbb603e8add2c91365a6075e0bc2d392e54b' + + 'f10f32bb24af4ee362e0035fd15d7e70b21d126cf1e84fd22902eed0beab869' + + '3bcbfe57a20d1a67681df82d6c359435eda9bb90090ff84d5193b53f2394594' + + '6d853da31ed6fe36a903d94d427bc1ccc76d7b31badfe508e6a4abc491e10a6' + + 'ff86fa4d836e1fd', 'hex') + }; + + const kBadPadding = { + '128': { + zeroPadChar: Buffer.from('ee1bf8a9da8aa456cf6624df06a64d0e', 'hex'), + bigPadChar: Buffer.from('5b437768fceeaf90114b0ca3d4342e33', 'hex'), + inconsistentPadChars: + Buffer.from('876570d0036ae21419db4f5e3ad4f2c0', 'hex') + }, + '256': { + zeroPadChar: Buffer.from('01fd8dd61ec1fe448cc89d6ec859b181', 'hex'), + bigPadChar: Buffer.from('58076edd4a22616d6319bdde5e5a1b3c', 'hex'), + inconsistentPadChars: + Buffer.from('98363c943b88c1154d8caa43784a6a3e', 'hex') + } + }; + + const kKeyLengths = [128, 256]; + + const passing = []; + kKeyLengths.forEach((keyLength) => { + passing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { name: 'AES-CBC', iv }, + plaintext: kPlaintext, + result: kCipherText[keyLength] + }); + }); + + const failing = []; + kKeyLengths.forEach((keyLength) => { + failing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { + name: 'AES-CBC', + iv: iv.slice(0, 8) + }, + plaintext: kPlaintext, + result: kCipherText[keyLength] + }); + + const longIv = new Uint8Array(24); + longIv.set(iv, 0); + longIv.set(iv.slice(0, 8), 16); + failing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { name: 'AES-CBC', iv: longIv }, + plaintext: kPlaintext, + result: kCipherText[keyLength] + }); + }); + + // Scenarios that should fail decryption because of bad padding + const decryptionFailing = []; + kKeyLengths.forEach(function(keyLength) { + [ + 'zeroPadChar', + 'bigPadChar', + 'inconsistentPadChars' + ].forEach((paddingProblem) => { + const badCiphertext = + new Uint8Array(kCipherText[keyLength].byteLength); + badCiphertext.set( + kCipherText[keyLength] + .slice(0, kCipherText[keyLength].byteLength - 16)); + badCiphertext.set(kBadPadding[keyLength][paddingProblem]); + + decryptionFailing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { name: 'AES-CBC', iv }, + plaintext: kPlaintext, + result: badCiphertext + }); + }); + }); + + return { passing, failing, decryptionFailing }; +}; diff --git a/test/fixtures/crypto/aes_ctr.js b/test/fixtures/crypto/aes_ctr.js new file mode 100644 index 00000000000000..7852045c663304 --- /dev/null +++ b/test/fixtures/crypto/aes_ctr.js @@ -0,0 +1,99 @@ +'use strict'; + +module.exports = function() { + const kPlaintext = + Buffer.from('546869732073706563696669636174696f6e206465736372696265' + + '732061204a6176615363726970742041504920666f722070657266' + + '6f726d696e672062617369632063727970746f6772617068696320' + + '6f7065726174696f6e7320696e20776562206170706c6963617469' + + '6f6e732c20737563682061732068617368696e672c207369676e61' + + '747572652067656e65726174696f6e20616e642076657269666963' + + '6174696f6e2c20616e6420656e6372797074696f6e20616e642064' + + '656372797074696f6e2e204164646974696f6e616c6c792c206974' + + '2064657363726962657320616e2041504920666f72206170706c69' + + '636174696f6e7320746f2067656e657261746520616e642f6f7220' + + '6d616e61676520746865206b6579696e67206d6174657269616c20' + + '6e656365737361727920746f20706572666f726d20746865736520' + + '6f7065726174696f6e732e205573657320666f7220746869732041' + + '50492072616e67652066726f6d2075736572206f72207365727669' + + '63652061757468656e7469636174696f6e2c20646f63756d656e74' + + '206f7220636f6465207369676e696e672c20616e64207468652063' + + '6f6e666964656e7469616c69747920616e6420696e746567726974' + + '79206f6620636f6d6d756e69636174696f6e732e', 'hex'); + + const kKeyBytes = { + '128': Buffer.from('dec0d4fcbf3c4741c892dabd1cd4c04e', 'hex'), + '256': Buffer.from('67693823fb1d58073f91ece9cc3af910e5532616a4d27b1' + + '3eb7b74d8000bbf30', 'hex') + } + + const counter = Buffer.from('55aaf89ba89413d54ea727a76c27a284', 'hex'); + + const kCiphertext = { + '128': Buffer.from( + 'e91175fda4f5ea57c52b0d000bbe98af68c0a59058aeed8ab5b7063503a1ce4' + + '70d79dad174f90aaafaa5449d848dc8b2c557d1e7fa4b9a41a2fb1e9fea1414' + + 'b593dab40c04f14b4f81400fe43c93990181b096a15561169aea177f100416e' + + '20b6810b00ee1b04fef67f3bede28baf4d41d397daf1511e9020d7766e9e604' + + '10de38e1432dbffa0f992dc1f0d4756544e8c765af7df706f90e009db9384c3' + + '3e44dea543c2a77bbd52022de41e7d71a498de7feb9760eb47e503366c88dcc' + + '2d1a387788de2d8f78e72c2bdd8815bc8a54e8d0eee275683ca5041290f031a' + + 'd5a4454efa17cc4907718f3ef4b75fedbd13583254f441a15a8a3323b12f40b' + + '8fbebc816cf9b468d8d7a5a0fb548498c39a6ed84615f894929838aef8e3016' + + '60f76b632493f23709fedfd5e107f78267f331b60a38c146f9710484a4acdef' + + 'f110b3b7745ff83aa8cb5de9e15b11e20a785572041f2852a1981156edcf07e' + + '46eb64144449cce74b9cc94163a6fda8ae19219721d60b757b5b5ec718dabd5' + + '0954b6e6a393f656f6346f40229d0c50e01c15701f2a4fe5d25a174edf9b90e' + + 'e0c0ebf9e06b5fe00558638a1ea3781403b0c9206d9e814d6a79fb7a56060e1' + + 'c7176af36c6a1ad635981a9bfd8007d8cf6d9f93f0e8e22b93a9a2ccd7090ab' + + '1df63cea3f040', 'hex'), + '256': Buffer.from( + '37529a432f50ba4e53385f8266ec3deccceceade7ae29395e9291076c95bb9a' + + '24f4792fcdd6ea5894b815edb5d5e4022fabe055a06b1a7e01979555b579838' + + '64bf23019cb1b37ffdadb057f728cfb2af0a33d146344cfba0accb4dbf613a7' + + 'bee523ca6d6860e474a9c0f4d068d4c0acd94cc55cbf21e4285ca15116c9702' + + '0f2c33b4585008f8fe97c9e29c0627c5d47c48d94be88b9b16c7f2df740a8d2' + + 'a07556305b82b919f7a87ca2ed19db27262c277c213f2a7eca25e5a6adbea43' + + '0ba2e1061198171054285aff9e0869c638dcd524cbf1f255da675acad6d7867' + + '9a9958b7a8f9bb21dd9c580ad196f9a0e4c6a6500d7bb21df74cd5934ce3c4d' + + '8d1f39d34a2adb58d224c48097887cde9d3be146a3ea3bade4c6864cf9e445b' + + '5c4c2b3ef4e2b8f5eea0ab1c0b9abe7a4fe5b2c0b1d94df6b12953d3273260e' + + '80bd094dec97a3177a9cec0b5042be1804040c9439403b8f72f7426fa756ad6' + + '266cf2c8659e740329dd0d24f9f85497662cad739f71d6174011c77f8f31fb4' + + '4226288dfb86817ef17116321c71bb9ed97db6e990f62058580f006683431f2' + + '29662f1d5e3cdaffe0335467ca72635688c939ec8b32d6465f651a635f73c0a' + + '4e7f0aadb0e81f5bcbfaec2671ac97fdc2fd32f24c941775c37a6810d4b171b' + + 'c8aba90a86603', 'hex') + }; + + const kKeyLengths = [128, 256]; + + const passing = []; + kKeyLengths.forEach((keyLength) => { + passing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: {name: 'AES-CTR', counter, length: 64}, + plaintext: kPlaintext, + result: kCiphertext[keyLength] + }); + }); + + const failing = []; + kKeyLengths.forEach((keyLength) => { + failing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: {name: 'AES-CTR', counter, length: 0}, + plaintext: kPlaintext, + result: kCiphertext[keyLength] + }); + + failing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: {name: 'AES-CTR', counter, length: 129}, + plaintext: kPlaintext, + result: kCiphertext[keyLength] + }); + }); + + return { passing, failing, decryptionFailing: [] }; +}; diff --git a/test/fixtures/crypto/aes_gcm.js b/test/fixtures/crypto/aes_gcm.js new file mode 100644 index 00000000000000..07332d44d9e16d --- /dev/null +++ b/test/fixtures/crypto/aes_gcm.js @@ -0,0 +1,134 @@ +'use strict'; + +module.exports = function() { + const kPlaintext = + Buffer.from('546869732073706563696669636174696f6e206465736372696265' + + '732061204a6176615363726970742041504920666f722070657266' + + '6f726d696e672062617369632063727970746f6772617068696320' + + '6f7065726174696f6e7320696e20776562206170706c6963617469' + + '6f6e732c20737563682061732068617368696e672c207369676e61' + + '747572652067656e65726174696f6e20616e642076657269666963' + + '6174696f6e2c20616e6420656e6372797074696f6e20616e642064' + + '656372797074696f6e2e204164646974696f6e616c6c792c206974' + + '2064657363726962657320616e2041504920666f72206170706c69' + + '636174696f6e7320746f2067656e657261746520616e642f6f7220' + + '6d616e61676520746865206b6579696e67206d6174657269616c20' + + '6e656365737361727920746f20706572666f726d20746865736520' + + '6f7065726174696f6e732e205573657320666f7220746869732041' + + '50492072616e67652066726f6d2075736572206f72207365727669' + + '63652061757468656e7469636174696f6e2c20646f63756d656e74' + + '206f7220636f6465207369676e696e672c20616e64207468652063' + + '6f6e666964656e7469616c69747920616e6420696e746567726974' + + '79206f6620636f6d6d756e69636174696f6e732e', 'hex'); + + const kKeyBytes = { + '128': Buffer.from('dec0d4fcbf3c4741c892dabd1cd4c04e', 'hex'), + '256': Buffer.from('67693823fb1d58073f91ece9cc3af910e5532616a4d27b1' + + '3eb7b74d8000bbf30', 'hex') + } + + const iv = Buffer.from('3a92732aa6ea39bf3986e0c73fa920002' + + '02175385ef8adeac2c87335eb928dd4', 'hex'); + + const additionalData = Buffer.from( + '5468657265206172652037206675727468657220656469746f72696' + + '16c206e6f74657320696e2074686520646f63756d656e742e', 'hex'); + + const tag = { + '128': Buffer.from('c2e2c6fdef1cc5f07bd8b097efc8b8b7', 'hex'), + '256': Buffer.from('bceff1309f15d500f12a554cc21c313c', 'hex') + }; + + const tag_with_empty_ad = { + '128': Buffer.from('de330b1724defaf81b621e519623dcc6', 'hex'), + '256': Buffer.from('f4ba56cb9a25bff8f6398b82e02fd9ee', 'hex') + }; + + // AES-GCM produces ciphertext and a tag. + const kCiphertext = { + '128': Buffer.from( + 'b4f128b7693493eee0afafeca8f4f17909cae1ed38d8fdfeba666fcfe4be82b19ff6' + + '0635f971e4fe517efdbf642bfb936b5ba6e7c9f1b4d6702f7ba4ba86364116b5c952' + + 'ec3b348bac2729597b3e66a75296fa5d60a98759f5ffa4c0a99f19108b914c049083' + + '94c5cc2e176ec1e47f78f21836f0b5a262f4f944867a7e97266c7444966d26c2159f' + + '8ccdb7236197ba789116eb16d2dfbb8fa2b75dc468336035eafab84ced9d25cbe257' + + 'de4bf05fdade4051a54bc9d8be0d74d945422fa144f74afd9db5a27935205b7ce669' + + 'e011bb323d4d674f4739a374ea951b69181f9f0380822a5e7dc88efb94c91195e854' + + '321112cbbae2a4e3ca4c4110a3e084341f658148ab9f2ab1fd6256c95f753e0ccd4e' + + '247ec47959b925a142b575ba477c846e781bf6a3120d5ac87f52d1f1aa49f78960f4' + + 'fefb77479c1b6b35212d16009030200b74157df6d9ab9ee08eea8df2a8599a42e3a1' + + 'b66001584e0c07ef1ece1f596f6b2a25f194e80108fb7592b70930275e3b46e61aa5' + + '619c8c8d1f3e0ace3730cf00c5cac56c85af5004109adfff04c4bcb2f01d0d7805e1' + + 'ca0323e19e5c9849cd6b9de0f563c2ab9cf5f7b7a5283ec86e1d97ce64af5824f25a' + + '045249fa8cf5d9099923f2ce4ec579730f508065bff05b97f93e3ef412031187ded2' + + '5d957b', 'hex'), + '256': Buffer.from( + '0861eb7146208783d2d17ca0ffb6091d7dc11bf0812e0289a98e3d079136aacf9f6f' + + '275f573fa21b0612dbd774225a3972f4669143063398f7a5f27464dbb148b1116e43' + + '5ddb64d914cf599a2d25695343a28ceb8128b1caae3694379cc1e8f986a3c3337274' + + '4126496360f9e0451177babcb52b4e9c4c8ae23f05f8095e1a0102eb27ae4a2fb716' + + '282f2f0d64770c43b2b838a7ee8f0d2cd0b9976c0611347ab6d2cf2adb254a5e7e24' + + 'f9252004da2cee4538db1f4dad2ebb672470d5fc2857a4f0a39f20817db26c2f1c1f' + + '242a73240e91c39cbf2ea3f9b51f5a491e4839df3f3c4f8c0e751f91de9c79ed2091' + + '8f600cfe2315153ba8ab9ad9003bcaaf67d6c0af1a122b36b0de4b16077afde0913d' + + '2ad049ed548dd1d5e42ef43b0944062358bd0a3e09551c2c521399a0b2f038a0f4c9' + + 'ad4d3d14e31eb4a71069b9c15fcf2917864ec6b65d1859f7e74be9c289f272c2be82' + + '8aee5e89c1c27389becfa9539b0ed2a081c3a1eaddff7243620c5d2941b7f467f765' + + '52f67d577d4e15ba66cd142820c9ae0f34f0d9b4a26c06d3291287e8b812bca99dbe' + + '4ca64bb07f27fb16cb995031f17c89977bcc2b9fbeb1c41275a92e98fb2d19a41b91' + + 'd6e4370f0283d850ffccaf643b910f6728212dffc8feac8a143a57b6c094db2958e6' + + 'e546f9', 'hex') + }; + + const kKeyLengths = [128, 256]; + const kTagLengths = [32, 64, 96, 104, 112, 120, 128]; + + const passing = []; + kKeyLengths.forEach((keyLength) => { + kTagLengths.forEach((tagLength) => { + const byteCount = tagLength / 8; + const result = + new Uint8Array(kCiphertext[keyLength].byteLength + byteCount); + result.set(kCiphertext[keyLength], 0); + result.set(tag[keyLength].slice(0, byteCount), + kCiphertext[keyLength].byteLength); + passing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { name: 'AES-GCM', iv, additionalData, tagLength }, + plaintext: kPlaintext, + result + }); + + const noadresult = + new Uint8Array(kCiphertext[keyLength].byteLength + byteCount); + noadresult.set(kCiphertext[keyLength], 0); + noadresult.set(tag_with_empty_ad[keyLength].slice(0, byteCount), + kCiphertext[keyLength].byteLength); + passing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { name: 'AES-GCM', iv, tagLength }, + plaintext: kPlaintext, + result: noadresult + }); + }); + }); + + const failing = []; + kKeyLengths.forEach((keyLength) => { + [24, 48, 72, 95, 129, 256].forEach((badTagLength) => { + failing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { + name: 'AES-GCM', + iv, + additionalData, + tagLength: badTagLength + }, + plaintext: kPlaintext, + result: kCiphertext[keyLength] + }); + }); + }); + + return { passing, failing, decryptionFailing: [] }; +}; diff --git a/test/fixtures/crypto/dsa.js b/test/fixtures/crypto/dsa.js new file mode 100644 index 00000000000000..c88720220c5e03 --- /dev/null +++ b/test/fixtures/crypto/dsa.js @@ -0,0 +1,93 @@ +'use strict'; + +module.exports = function() { + const pkcs8 = Buffer.from( + '3082015b0201003082013406072a8648ce3804013082012702818100d5f35aa5730e2' + + '6166fd3ea81f8f0eeb05bd1250e164b7c76b180b6dae95096d13dee6956e15a9aea7c' + + 'f18a0df7c5dc326ccef1cbf97636d22f870b76f2607f9a867db2756aecf65505aa48f' + + 'dea5f5ee54f508a05d9dae76bf262b4ca3662cc176b7c628c7bee2076df07f9a64e04' + + '02630dfee63eaf0ed64d48b469fe1c9ac4a1021d00b14213226cfcfb59e3a0379e559' + + 'c74ff8a7383eb4c41cecb6f3732b702818100a0865b7f8954e7ae587c8e6a89e391e8' + + '2657c58f05ccd94de61748e89e217efab3d9b5fa842ebc62525966916ad2b7af422a9' + + 'b2407817a5b382b6581434fd1a169c75ad4d0e3862a3f484e9f9f2a816f943a8e6060' + + 'f26fe27c533587b765e57948439084e76fd6a4fd004f5c78d972cf7f100ec9494a902' + + '645baca4b4c6f3993041e021c600daa0a9c4cc674c98bb07956374c84ac1c33af8816' + + '3ea7e2587876', 'hex'); + + const spki = Buffer.from( + '308201c03082013406072a8648ce3804013082012702818100d5f35aa5730e26166fd' + + '3ea81f8f0eeb05bd1250e164b7c76b180b6dae95096d13dee6956e15a9aea7cf18a0d' + + 'f7c5dc326ccef1cbf97636d22f870b76f2607f9a867db2756aecf65505aa48fdea5f5' + + 'ee54f508a05d9dae76bf262b4ca3662cc176b7c628c7bee2076df07f9a64e0402630d' + + 'fee63eaf0ed64d48b469fe1c9ac4a1021d00b14213226cfcfb59e3a0379e559c74ff8' + + 'a7383eb4c41cecb6f3732b702818100a0865b7f8954e7ae587c8e6a89e391e82657c5' + + '8f05ccd94de61748e89e217efab3d9b5fa842ebc62525966916ad2b7af422a9b24078' + + '17a5b382b6581434fd1a169c75ad4d0e3862a3f484e9f9f2a816f943a8e6060f26fe2' + + '7c533587b765e57948439084e76fd6a4fd004f5c78d972cf7f100ec9494a902645bac' + + 'a4b4c6f399303818500028181009a8df69f2fe321869e2094e387bc1dc2b5f3bff2a2' + + 'e23cfba51d3c119fba6b4c15a49485fa811b6955d91d28c9e2e0445a79ddc5426b2fe' + + '44e00a6c9254c776f13fd10dbc934262077b1df72c16bc848817c61fb6a607abe60c7' + + 'd11528ab9bdf55de45495733a047bd75a48b8166f1aa3deab681a2574a4f35106f0d7' + + '8b641d7', 'hex'); + + const plaintext = Buffer.from( + '5f4dba4f320c0ce876725afce5fbd25bf83e5a7125a08cafe73c3ebac421779df9d55' + + 'd180c3ae9942645e1d82fee8c9d294b3cb1a08a9931201b3c0e81fc47cacf8315a2af' + + '66324113c3b66230c34608c4f4593634ce02b267362277f0a840ca74bc3d1a6236952' + + 'c5ed7aaf8a8fecbddfa7584e6978cea5d2a5b9fb7f1b48c8b0be58a305202754d8376' + + '107374793cf026aaee5300727d836cd71e71b345ddb2e44446ffc5b901635413890d9' + + '10ea380984a90191031323f16dbcc9d6be168b84885384ca03e12600ac1c248028af3' + + '726cc93463882ea8c02aab', 'hex'); + + const signatures = { + 'sha-1': Buffer.from( + '303d021c685192dc48fbd8c6dd80536eaf92050ca07aa85af5a25a41a93bc8ea021' + + 'd009a032a19182ceca7b202d7e0ce327fe5830a9937a37714d4c6801ae9', 'hex'), + 'sha-256': Buffer.from( + '303d021c7b660d1b2b40237d0f615877efbf2a3f51577bc42a3cb4e280c0a6a9021' + + 'd008393a01a9bcabce267dfcc6bc867d348911f6eda3db96d3078a380c1', 'hex'), + 'sha-384': Buffer.from( + '303e021d00ab84d3c306aea1c5e69e126d21dafc03ee9c7aa8fa38884de5015d510' + + '21d0099030adacdc6787dcb1dc3b05fa122ad72d4ddc281cc619565dba947', 'hex'), + 'sha-512': Buffer.from( + '303c021c069c40654607388a56d3cb827c11121d07850b3b62ed203c1d92ce7f021' + + 'c0e69f41933d0ee3b1c1b184d742f3813ee3b32d053f13fbb93ab8229', 'hex'), + } + + const vectors = [ + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'NODE-DSA' }, + hash: 'SHA-1', + plaintext, + signature: signatures['sha-1'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'NODE-DSA' }, + hash: 'SHA-256', + plaintext, + signature: signatures['sha-256'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'NODE-DSA'}, + hash: 'SHA-384', + plaintext, + signature: signatures['sha-384'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'NODE-DSA' }, + hash: 'SHA-512', + plaintext, + signature: signatures['sha-512'] + } + ]; + + return vectors; +}; diff --git a/test/fixtures/crypto/ecdsa.js b/test/fixtures/crypto/ecdsa.js new file mode 100644 index 00000000000000..4b3539edb1fc89 --- /dev/null +++ b/test/fixtures/crypto/ecdsa.js @@ -0,0 +1,113 @@ +'use strict'; + +module.exports = function() { + const pkcs8 = { + 'P-384': Buffer.from( + '3081b6020100301006072a8648ce3d020106052b8104002204819e30819b02010104' + + '3002a9a0d899efa87e7564110907e9d82c21bd6265a37abd9a6fdb0f80ec844dd3a1' + + '425320d67ddc30f5db74efb9a2e661a164036200041d319d692dca5f5754ba7b32c1' + + '1642c6d8d2b4fb8249c3f214d71e90b5252966d97f7beb1faab1e4f3e2605549c2ee' + + 'db520329b3bea6b5e55624a15150a16966635f1916ef04dd758e69409d0633cb4b25' + + '994179b22a769c743436910e799951', 'hex'), + 'P-521': Buffer.from( + '3081ee020100301006072a8648ce3d020106052b810400230481d63081d302010104' + + '4201533e618f98ead1b513ec8878c8820d377a36d8f03f2ba046c931825a3d358730' + + 'c0b26033dbb7f7e4a3d4434a035e24b707f9124766176e1af0b85df22eaaba9c25a1' + + '8189038186000400a6deecfb489117f1e41cc4a064073d8673086e51db25086e8db7' + + '64d4eff60aad6358fdcf967ac68459275e2a804f8eeeb7e4c4284b1451c0a5ea76fe' + + '7007ac054701c5eddaf9a89e7c4fdcc924c737d8f585da9703a954c23be7c14aafa6' + + '6654b256770a938e7f26e700c603931c7bd0bdb5d0632c7d1eab466f09d976c24a32' + + '3e1b7c', 'hex') + } + + const spki = { + 'P-384': Buffer.from( + '3076301006072a8648ce3d020106052b81040022036200041d319d692dca5f5754ba' + + '7b32c11642c6d8d2b4fb8249c3f214d71e90b5252966d97f7beb1faab1e4f3e26055' + + '49c2eedb520329b3bea6b5e55624a15150a16966635f1916ef04dd758e69409d0633' + + 'cb4b25994179b22a769c743436910e799951', 'hex'), + 'P-521': Buffer.from( + '30819b301006072a8648ce3d020106052b81040023038186000400a6deecfb489117' + + 'f1e41cc4a064073d8673086e51db25086e8db764d4eff60aad6358fdcf967ac68459' + + '275e2a804f8eeeb7e4c4284b1451c0a5ea76fe7007ac054701c5eddaf9a89e7c4fdc' + + 'c924c737d8f585da9703a954c23be7c14aafa66654b256770a938e7f26e700c60393' + + '1c7bd0bdb5d0632c7d1eab466f09d976c24a323e1b7c', 'hex') + } + + const plaintext = Buffer.from( + '5f4dba4f320c0ce876725afce5fbd25bf83e5a7125a08cafe73c3ebac421779df9d55d' + + '180c3ae9942645e1d82fee8c9d294b3cb1a08a9931201b3c0e81fc47cacf8315a2af66' + + '324113c3b66230c34608c4f4593634ce02b267362277f0a840ca74bc3d1a6236952c5e' + + 'd7aaf8a8fecbddfa7584e6978cea5d2a5b9fb7f1b48c8b0be58a305202754d83761073' + + '74793cf026aaee5300727d836cd71e71b345ddb2e44446ffc5b901635413890d910ea3' + + '80984a90191031323f16dbcc9d6be168b84885384ca03e12600ac1c248028af3726cc9' + + '3463882ea8c02aab', 'hex'); + + // For verification tests. + const signatures = { + 'P-384': { + 'SHA-1': Buffer.from( + '65fe070ec3eac35250d00b9ee6db4d2dadd5f3bbb9c495c8671d2a0d2b99149fb2' + + '4f88af074e0b903268b3d0ed5f0e14685796b28fe34b2d8edcdf10845b24cf79b3' + + '3627d8bd2c81621cb51e030c21a43abb0a8740fac26f8522e683c367ac96', 'hex'), + 'SHA-256': Buffer.from( + '4bc2dfea3bcda4fbb4fd927b030f9b80b1f5d2ad9bb7aa06293869577120b2b1d0' + + 'ef11ccd9fed0714aab36bef63928f784f53c7e09df93e9b3e5b0c883cf720951b4' + + 'fe2382c7842edcfcd45d956a72d29a4030a038a900e6f7dd857a5650d3e8', 'hex'), + 'SHA-384': Buffer.from( + '0dd9c2c7f0b6f4d9328254a902e87374b3c092195e6be21aa1a6dcd8eba60f7b0b' + + '38c4006dfa2146d4e9fd23dc336179974017493a1f4f74eecfe455be3da9ed9964' + + '1d81610dfeb468b607da941d5714e7b51aee2c45aa0e9c4da021b2370090', 'hex'), + 'SHA-512': Buffer.from( + '72fbdb369fd34c1c54264d07f4facd69b02e4206f8a8bb259b882a305c56fde2d3' + + '5107e493c53cd6b4af0b31306f4d03fd43cfc762a1030e17a3d775453a1212b142' + + '9f7b3d93066a5f42a10b138cd177dc09616e827d598822d78d4627b754e6', 'hex') + }, + 'P-521': { + 'SHA-1': Buffer.from( + '01781a17a60e43126960fd396e1210916c2115ca4428d968389c4b46c1553674ce' + + '937b8e21700ce60932ae0f575ca187dd597720db839eb1f20c7e3394787559dcd5' + + '00207e570df5c7e4ad9fc0a5f72065e9ce1c9e3d12ca5e6dd9f44fe128561b75f4' + + '226c4fadf23d83536cc669ea4098e373b6cb919c8b5cfc0505a67d96b276a46a3d', + 'hex'), + 'SHA-256': Buffer.from( + '0174dba77b14d73f66f5716786a3e5a8d7c931445e6d320a9229d961d8a1b3efd1' + + '1a5ea33c79495ac599bbb68a641a849d58d83ef854cc265fa6c917dff6ee435a67' + + '01b3d5527dac20fb0a7033c3fe79744eacef7b3ffc27b64dc863f86f42982cb222' + + '9245fe9de48aa59eb653d4497086d911a5bd270e95c51e7e98f7a5863fc7fb065c', + 'hex'), + 'SHA-384': Buffer.from( + '01f77db1e51378e117c5b8bec8a03f9657d244c54e837908bf7101255f4151525d' + + '9e89cf7f54631b3368919d3824ff9f7f78fe81239a1a9fde2b7a83e95ca6a0ca11' + + '01b98b1da4ed00ec769367e9958b8047d47f92ab8bff96f1330bf948c92209011b' + + '8cdbb496d464dbb916720eb702bdad928c99b980b76504e0ad1c12b4a85731c70c', + 'hex'), + 'SHA-512': Buffer.from( + '00b2caaf6798519a9d36dbfafe786b2fba1cc2acb99593c177b36e3a1ceeb70227' + + '5ae23cfcca0aad78f6b6dee6b4718b95d0d1a715aa3378470e50b516c18e0f3305' + + '01f0071e6a32867fa70f695cd39c4e87e142b9e4134d38740bd6fee354a575167e' + + '13524e94832637910fe11e53a85fb21b91adb81bb1779c4e2b8bc87c717dc35084', + 'hex') + } + } + + const curves = ['P-384', 'P-521']; + const hashes = ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']; + + const vectors = []; + curves.forEach((namedCurve) => { + hashes.forEach((hash) => { + vectors.push({ + publicKeyBuffer: spki[namedCurve], + privateKeyBuffer: pkcs8[namedCurve], + name: 'ECDSA', + namedCurve, + hash, + plaintext, + signature: signatures[namedCurve][hash] + }); + }) + }); + + return vectors; +} diff --git a/test/fixtures/crypto/hmac.js b/test/fixtures/crypto/hmac.js new file mode 100644 index 00000000000000..c4942976adf49b --- /dev/null +++ b/test/fixtures/crypto/hmac.js @@ -0,0 +1,52 @@ +'use strict'; + +module.exports = function () { + const plaintext = Buffer.from( + '5f4dba4f320c0ce876725afce5fbd25bf83e5a7125a08cafe73c3ebac421779df9d55d' + + '180c3ae9942645e1d82fee8c9d294b3cb1a08a9931201b3c0e81fc47cacf8315a2af66' + + '324113c3b66230c34608c4f4593634ce02b267362277f0a840ca74bc3d1a6236952c5e' + + 'd7aaf8a8fecbddfa7584e6978cea5d2a5b9fb7f1b48c8b0be58a305202754d83761073' + + '74793cf026aaee5300727d836cd71e71b345ddb2e44446ffc5b901635413890d910ea3' + + '80984a90191031323f16dbcc9d6be168b84885384ca03e12600ac1c248028af3726cc9' + + '3463882ea8c02aab', 'hex'); + + const raw = { + 'SHA-1': Buffer.from('47a20746d17179db65e0a79dedffc7fdf181081b', 'hex'), + 'SHA-256': Buffer.from( + 'e588ec0811463d767241df1074b47ae4071b51f2ce36537ba69ccdc3fdc2b7a8', + 'hex'), + 'SHA-384': Buffer.from( + '6b1da28eab1f582ad9718effe05e23d5fd2c9877a2d9443f90bec093bece2ea7' + + 'd2354cd0bdc5e147d2e9009373494488', 'hex'), + 'SHA-512': Buffer.from( + '5dcc359443aaf652fa1375d6b3e61fdcf29bb4a28bd5d3dcfa40f82f906bb280' + + '0455db03b5d31fb972a15a6d0103a24e56d156a119c0e5a1e92a44c3c5657cf9', + 'hex') + } + + const signatures = { + 'SHA-1': Buffer.from('0533902a99f8524ee50af01d38dedce133d98ca0', 'hex'), + 'SHA-256': Buffer.from( + '85a40cea2e078c2827a3953ffb66c27b291a472b0d70a0000b45d823803eeb54', + 'hex'), + 'SHA-384': Buffer.from( + '217c3d50f0ba9a6d6eae1efdd7a518fe2e3880b582a40d061e9099c1e026ef58' + + '82548b5d5cecdd5598d99b6b6f3057ff', 'hex'), + 'SHA-512': Buffer.from( + '61fb278c3ffb0cce2bf1cf723ddfd8ef1f931c0c618c25907324605939e3f9a2' + + 'c6f4af690bda3407dc2f5770f6a0a44b954d64a332e3ee0821abf82b7f3e99c1', + 'hex') + } + + const vectors = []; + Object.keys(raw).forEach((hash) => { + vectors.push({ + hash, + keyBuffer: raw[hash], + plaintext, + signature: signatures[hash] + }); + }); + + return vectors; +}; diff --git a/test/fixtures/crypto/rsa.js b/test/fixtures/crypto/rsa.js new file mode 100644 index 00000000000000..4bbeccbeeb3839 --- /dev/null +++ b/test/fixtures/crypto/rsa.js @@ -0,0 +1,330 @@ +'use strict'; + +module.exports = function() { + const pkcs8 = Buffer.from( + '308204bf020100300d06092a864886f70d0101010500048204a930820' + + '4a50201000282010100d3576092e62957364544e7e4233b7bdb293db2' + + '085122c479328546f9f0f712f657c4b17868c930908cc594f7ed00c01' + + '442c1af04c2f678a48ba2c80fd1713e30b5ac50787ac3516589f17196' + + '7f6386ada34900a6bb04eecea42bf043ced9a0f94d0cc09e919b9d716' + + '6c08ab6ce204640aea4c4920db6d86eb916d0dcc0f4341a10380429e7' + + 'e1032144ea949de8f6c0ccbf95fa8e928d70d8a38ce168db45f6f1346' + + '63d6f656f5ceabc725da8c02aabeaaa13ac36a75cc0bae135df3114b6' + + '6589c7ed3cb61559ae5a384f162bfa80dbe4617f86c3f1d010c94fe2c' + + '9bf019a6e63b3efc028d43cee611c85ec263c906c463772c6911b19ee' + + 'c096ca76ec5e31e1e3020301000102820101008b375ccb87c825c5ff3' + + 'd53d009916e9641057e18527227a07ab226be1088813a3b38bb7b48f3' + + '77055165fa2a9339d24dc667d5c5ba3427e6a481176eac15ffd490683' + + '11e1c283b9f3a8e0cb809b4630c50aa8f3e45a60b359e19bf8cbb5eca' + + 'd64e761f1095743ff36aaf5cf0ecb97fedaddda60b5bf35d811a75b82' + + '2230cfaa0192fad40547e275448aa3316bf8e2b4ce0854fc7708b537b' + + 'a22d13210b09aec37a2759efc082a1531b23a91730037dde4ef26b5f9' + + '6efdcc39fd34c345ad51cbbe44fe58b8a3b4ec997866c086dff1b8831' + + 'ef0a1fea263cf7dacd03c04cbcc2b279e57fa5b953996bfb1dd68817a' + + 'f7fb42cdef7a5294a57fac2b8ad739f1b029902818100fbf833c2c631' + + 'c970240c8e7485f06a3ea2a84822511a8627dd464ef8afaf7148d1a42' + + '5b6b8657ddd5246832b8e533020c5bbb568855a6aec3e4221d793f1dc' + + '5b2f2584e2415e48e9a2bd292b134031f99c8eb42fc0bcd0449bf22ce' + + '6dec97014efe5ac93ebe835877656252cbbb16c415b67b184d2284568' + + 'a277d59335585cfd02818100d6b8ce27c7295d5d16fc3570ed64c8da9' + + '303fad29488c1a65e9ad711f90370187dbbfd81316d69648bc88cc5c8' + + '3551afff45debacfb61105f709e4c30809b90031ebd686244496c6f69' + + 'e692ebdc814f64239f4ad15756ecb78c5a5b09931db183077c546a38c' + + '4c743889ad3d3ed079b5622ed0120fa0e1f93b593db7d852e05f02818' + + '038874b9d83f78178ce2d9efc175c83897fd67f306bbfa69f64ee3423' + + '68ced47c80c3f1ce177a758d64bafb0c9786a44285fa01cdec3507cde' + + 'e7dc9b7e2b21d3cbbcc100eee9967843b057329fdcca62998ed0f11b3' + + '8ce8b0abc7de39017c71cfd0ae57546c559144cdd0afd0645f7ea8ff0' + + '7b974d1ed44fd1f8e00f560bf6d45028181008529ef9073cf8f7b5ff9' + + 'e21abadf3a4173d3900670dfaf59426abcdf0493c13d2f1d1b46b824a' + + '6ac1894b3d925250c181e3472c16078056eb19a8d28f71f3080927534' + + '81d49444fdf78c9ea6c24407dc018e77d3afef385b2ff7439e9623794' + + '1332dd446cebeffdb4404fe4f71595161d016402c334d0f57c61abe4f' + + 'f9f4cbf90281810087d87708d46763e4ccbeb2d1e9712e5bf0216d70d' + + 'e9420a5b2069b7459b99f5d9f7f2fad7cd79aaee67a7f9a34437e3c79' + + 'a84af0cd8de9dff268eb0c4793f501f988d540f6d3475c2079b8227a2' + + '3d968dec4e3c66503187193459630472bfdb6ba1de786c797fa6f4ea6' + + '5a2a8419262f29678856cb73c9bd4bc89b5e041b2277', 'hex'); + + const spki = Buffer.from( + '30820122300d06092a864886f70d01010105000382010f003082010a0' + + '282010100d3576092e62957364544e7e4233b7bdb293db2085122c479' + + '328546f9f0f712f657c4b17868c930908cc594f7ed00c01442c1af04c' + + '2f678a48ba2c80fd1713e30b5ac50787ac3516589f171967f6386ada3' + + '4900a6bb04eecea42bf043ced9a0f94d0cc09e919b9d7166c08ab6ce2' + + '04640aea4c4920db6d86eb916d0dcc0f4341a10380429e7e1032144ea' + + '949de8f6c0ccbf95fa8e928d70d8a38ce168db45f6f134663d6f656f5' + + 'ceabc725da8c02aabeaaa13ac36a75cc0bae135df3114b66589c7ed3c' + + 'b61559ae5a384f162bfa80dbe4617f86c3f1d010c94fe2c9bf019a6e6' + + '3b3efc028d43cee611c85ec263c906c463772c6911b19eec096ca76ec' + + '5e31e1e30203010001', 'hex'); + + const label = Buffer.from( + '5468657265206172652037206675727468657220656469746f7269616' + + 'c206e6f74657320696e2074686520646f63756d656e742e', 'hex'); + + // overlong plaintext for RSA-OAEP + const plaintext = Buffer.from( + '5f4dba4f320c0ce876725afce5fbd25bf83e5a7125a08cafe73c3ebac4' + + '21779df9d55d180c3ae9942645e1d82fee8c9d294b3cb1a08a9931201b' + + '3c0e81fc47cacf8315a2af66324113c3b66230c34608c4f4593634ce02' + + 'b267362277f0a840ca74bc3d1a6236952c5ed7aaf8a8fecbddfa7584e6' + + '978cea5d2a5b9fb7f1b48c8b0be58a305202754d8376107374793cf026' + + 'aaee5300727d836cd71e71b345ddb2e44446ffc5b901635413890d910e' + + 'a380984a90191031323f16dbcc9d6be168b84885384ca03e12600ac1c2' + + '48028af3726cc93463882ea8c02aab', 'hex'); + + const ciphertext = { + 'sha-1, no label': Buffer.from( + '901ef09cbbfe9a4939b9a9c43ccf22339e1575f7c74324494075c192' + + 'c40b68996eb0e04d7b151774fff7c00b66174a249fc3d1a6123c70a6' + + '6fd61b67cb54f683fe01049e4a44a5937e35cae1b73dce12aea09cd0' + + '1c4c48900fafdd753ac401566076b9b863807cf141a63044865e0cbc' + + '42b995fb639fe9994e94c0e381404ae1b554cdb27515f09798dc5a07' + + 'a60b162bf12f8cef7ce166314d942d80fe43d0df1fb0d7e9514ef729' + + 'dc6c845583f74ab2c36d9684d43b71962a18ff0e2b13ce74f537fb3a' + + '0b00ede329e77c11900a070e20f86dc07cacb56f7821d0249234106c' + + '6e0b4dda82e0febdb202ef0c7b10d560f0bafdc78f0624185783b522' + + '83ca14a1', 'hex'), + 'sha-256, no label': Buffer.from( + '0531eaeb4b8cef8eb77dd736d096b6dce840d164e9751f86a8d5ced0' + + '9999506ffaf000eb6153f7456f311ae99e47f1207150eb97f2982db7' + + '73be9e990e8a12985a5f115af906ef0c870eef3c9fca1c5b4d5e9904' + + '99b67d0094adaf8debdf9a5d72335b54b3d1ca26ea0957d63e064ce9' + + '69d52e53bd605c8fa9320d9eabe239eef47099555c194d1bb8dfeffe' + + 'ad6b4fd7f8f28990355cc8ee22a36c4867f0acead7f4a5025f1517f7' + + '52a7e8c093533d0cd659ad60a7dc05044220019887016437dcc94c6f' + + '9e8202b03bc955eb2c790d3fb7c7e77e2612ffa521daf467f640a749' + + 'e9e1191574be76e2d55c3cfe7a93551a7c28ddb2ba6b26c33a30c237' + + '1cd8974d', 'hex'), + 'sha-384, no label': Buffer.from( + '0c2392e30f92f1f4e4acd1b4a6fd99f983c61dcaf39bddde47b29ead' + + '3add104a7a86df1f7099f3683c65affe329d2bcab95035ec96c13dab' + + '9a0cb4b95960fee08aa5835566a1b57ddf545ac950509134ced00273' + + '9ea6ff3e3de4841f9fa9061660ecf10533e9f50ca5164f324944cc7b' + + '8e3aec6998a366f90cfaee7977651a0d1e8d194bcd1c2a008729aa01' + + '1a9d0d8ca271f98e015bf466bcd99cd97686b592f66fb1369f54a358' + + '937abcf917df6f39badc6f5ff630c7ac73b92fadbadd0c29fce04ca7' + + 'd62aab52b264e5a282bcbf02721c119e28e9426cd996b3791973d8a2' + + 'acf43a2c2f67ff884c1a77b83fc3268c640cab414336c31f7a6977e4' + + '951031d4', 'hex'), + 'sha-512, no label': Buffer.from( + '0626d346d5253113ddc0f8ced1918d88ebf00869f880af89c5e67bb3' + + '794bb58a9bf619e5a55909418f6c7e14a858a0c530427502db7afe60' + + '2493aa6f7ba83997198b0ae97ddb8d3a7dae5926dc190f0587452105' + + '0a311cfd94fbd535df08b840b95ef9d3403583882002a33d4c09a2bd' + + '50476ded93090b933dfe01b9d0e42cdb11a3efb8d4c5e5d223ec0475' + + '25bba91af85fa0a5fc1566fd4973917c588f7980ec469065b536b340' + + '39e899489e60f14fff5a43106e2bea9914394b5317b8d819d73409f1' + + '7215dfb8b1f742620d0fafcc5251cd160f5c1c63baeaf12125d20f08' + + 'c51ed061072a33add5ab3b9a47354e58f4329d216f8fb93d5b76edf5' + + '5d5b9c24', 'hex'), + 'sha-1, with label': Buffer.from( + '450c932bdb5f223d1d40dabe85457d230849499a57c25bc9826ff356' + + '5a7cfe82bb859e209fea96625bf678a639e96207a03a7d71354f02ca' + + 'd0687bc31b54fa6208a953cf5e1f611f5100f799d06340b9f4d5c23e' + + 'c8ab4ef50a3e90b0ce5e68ac2d3972c4f3a6224389294d0620e109ea' + + 'b72026281a5de6bf1bc0b743e09c40bd241bd8393aa430a44adaa7c4' + + 'd0dd4f6676177b1ae335b9c40ee99a068ce9cc996da3a4e2aacf4f7b' + + '1abc817c6252ff5f8e471a05d7c681b36e82fbde8cd2e225c87564ac' + + '1a8a610b57a168d244752e574afa9856c22a757afeebca96f93f6e6d' + + '17c520515927d99ca34eedfd19bce31f23aebe159da0253c27c10b5c' + + '0ffb7d97', 'hex'), + 'sha-256, with label': Buffer.from( + 'b4d46d087682180560797166729d951fea055f8ab80a10f9314e55de' + + 'e12fac6c2124ce2fd39fcaf134acdd1c6301b0335292be89a3cad44a' + + '96cc3a1a2b36875bf6c93b6ab5090a76bf6a7a66af7202b692a9377c' + + '54bbfeede1fc20c54f61dbfa3651347992c20dc458d40792ede81f71' + + 'd7c82a9eefb43398d6916a0b73a01547abbe2d19e51382d2343a0e37' + + '52fad7c16eb28f65a33f95d8b0a39142ef3eccc3660e9d029b72e6e1' + + '3779a7acb6bb4b9531d56890cae66f7d77ecd59b837a2b37f5b73c8c' + + '67582d549df743f3a966b0e1c0b59afc5a5f11a17060c4696837065c' + + 'd4127f55be0c697bdb6e826fb320b751f6f0873b05d2ad0f66d7575f' + + '8888ee0c', 'hex'), + 'sha-384, with label': Buffer.from( + 'cc3cbc830f7256f6be9bce3e44f9623fb29001f42af82f09fd088bba' + + 'd7b4bf5cf71392f241c369d38854e1ec4bcaf394c54473338741b43e' + + '7b5b1b43850eb7413101065e72f94224fabd49da9bd5ccf067b9cebd' + + '503cf9017fbe1cc4a7437bdecb7ccd99fbf24d97d5d7a2bd1f1cf401' + + 'a73a03a95d8f1b28a756e1553b5db052d1e0901523fcb66173c84675' + + '6df0af66d0647ce6b4e89f4db04b8b3a39fe0db7191bf6b633abc5e2' + + '1a0769e1ee933d446661f79527084446d7dccd4ac3b770985b467aa3' + + '1e423334beccd1df6f432c120e6c9c3ea5dd06f694e99432d9f82c63' + + 'e976ebf84e2dca3dd3dcc14a06e5cbd47274f2d655a5c7727d350557' + + 'eed091b8', 'hex'), + 'sha-512, with label': Buffer.from( + '8697b55eef5b0d5311398a15f2ce1856b8efee39e774718b0c806864' + + '9339e4b7a7e129b4f7858d0079c1eba8b8f86b2222e961d7f7f014f5' + + '0e00a711ebcc5161345109885b3b7ac8f94a3f440a12a2f30abe763c' + + '184ae75c62b3dd9605424e5dc8d41d4c32f6be5407f5b09461051016' + + 'deada5c8a95d3a087be57cdc427b22453121196b20fa623d29de6c70' + + '252ab2a3519d1ca00379580af9191916420024bbb0c79a7a8a2b48d9' + + '5a2b7732d2a6ca0279ac18ac334aa12d6b96bb590959b7e9a9954e91' + + 'e49c8ad7e8db2121e0b2ae100648b9d622cc9fa19a0b978e27f44a4b' + + 'f3bf7be7203676eb0c13c8a5fca1572e6333f892b47a2cd267eda932' + + '1cd27988', 'hex') + }; + + const passing = [ + { + name: 'RSA-OAEP with SHA-1 and no label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP' }, + hash: 'SHA-1', + plaintext: plaintext.slice(0, 214), + ciphertext: ciphertext['sha-1, no label'] + }, + { + name: 'RSA-OAEP with SHA-256 and no label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP' }, + hash: 'SHA-256', + plaintext: plaintext.slice(0, 190), + ciphertext: ciphertext['sha-256, no label'] + }, + { + name: 'RSA-OAEP with SHA-384 and no label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP' }, + hash: 'SHA-384', + plaintext: plaintext.slice(0, 158), + ciphertext: ciphertext['sha-384, no label'] + }, + { + name: 'RSA-OAEP with SHA-512 and no label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP' }, + hash: 'SHA-512', + plaintext: plaintext.slice(0, 126), + ciphertext: ciphertext['sha-512, no label'] + }, + { + name: 'RSA-OAEP with SHA-1 and empty label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP', label: new Uint8Array([]) }, + hash: 'SHA-1', + plaintext: plaintext.slice(0, 214), + ciphertext: ciphertext['sha-1, no label'] + }, + { + name: 'RSA-OAEP with SHA-256 and empty label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP', label: new Uint8Array([]) }, + hash: 'SHA-256', + plaintext: plaintext.slice(0, 190), + ciphertext: ciphertext['sha-256, no label'] + }, + { + name: 'RSA-OAEP with SHA-384 and empty label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP', label: new Uint8Array([]) }, + hash: 'SHA-384', + plaintext: plaintext.slice(0, 158), + ciphertext: ciphertext['sha-384, no label'] + }, + { + name: 'RSA-OAEP with SHA-512 and empty label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP', label: new Uint8Array([]) }, + hash: 'SHA-512', + plaintext: plaintext.slice(0, 126), + ciphertext: ciphertext['sha-512, no label'] + }, + { + name: 'RSA-OAEP with SHA-1 and a label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP', label }, + hash: 'SHA-1', + plaintext: plaintext.slice(0, 214), + ciphertext: ciphertext['sha-1, with label'] + }, + { + name: 'RSA-OAEP with SHA-256 and a label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP', label }, + hash: 'SHA-256', + plaintext: plaintext.slice(0, 190), + ciphertext: ciphertext['sha-256, with label'] + }, + { + name: 'RSA-OAEP with SHA-384 and a label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP', label }, + hash: 'SHA-384', + plaintext: plaintext.slice(0, 158), + ciphertext: ciphertext['sha-384, with label'] + }, + { + name: 'RSA-OAEP with SHA-512 and a label', + publicKeyBuffer: spki, + publicKeyFormat: 'spki', + privateKey: null, + privateKeyBuffer: pkcs8, + privateKeyFormat: 'pkcs8', + publicKey: null, + algorithm: { name: 'RSA-OAEP', label }, + hash: 'SHA-512', + plaintext: plaintext.slice(0, 126), + ciphertext: ciphertext['sha-512, with label'] + } + ]; + + const failing = []; + + return { passing, failing }; +}; diff --git a/test/fixtures/crypto/rsa_pkcs.js b/test/fixtures/crypto/rsa_pkcs.js new file mode 100644 index 00000000000000..49e202c512d9e2 --- /dev/null +++ b/test/fixtures/crypto/rsa_pkcs.js @@ -0,0 +1,138 @@ +'use strict'; + +module.exports = function () { + const pkcs8 = Buffer.from( + '308204bf020100300d06092a864886f70d0101010500048204a9308204a50201000282' + + '010100d3576092e62957364544e7e4233b7bdb293db2085122c479328546f9f0f712f6' + + '57c4b17868c930908cc594f7ed00c01442c1af04c2f678a48ba2c80fd1713e30b5ac50' + + '787ac3516589f171967f6386ada34900a6bb04eecea42bf043ced9a0f94d0cc09e919b' + + '9d7166c08ab6ce204640aea4c4920db6d86eb916d0dcc0f4341a10380429e7e1032144' + + 'ea949de8f6c0ccbf95fa8e928d70d8a38ce168db45f6f134663d6f656f5ceabc725da8' + + 'c02aabeaaa13ac36a75cc0bae135df3114b66589c7ed3cb61559ae5a384f162bfa80db' + + 'e4617f86c3f1d010c94fe2c9bf019a6e63b3efc028d43cee611c85ec263c906c463772' + + 'c6911b19eec096ca76ec5e31e1e3020301000102820101008b375ccb87c825c5ff3d53' + + 'd009916e9641057e18527227a07ab226be1088813a3b38bb7b48f377055165fa2a9339' + + 'd24dc667d5c5ba3427e6a481176eac15ffd49068311e1c283b9f3a8e0cb809b4630c50' + + 'aa8f3e45a60b359e19bf8cbb5ecad64e761f1095743ff36aaf5cf0ecb97fedaddda60b' + + '5bf35d811a75b822230cfaa0192fad40547e275448aa3316bf8e2b4ce0854fc7708b53' + + '7ba22d13210b09aec37a2759efc082a1531b23a91730037dde4ef26b5f96efdcc39fd3' + + '4c345ad51cbbe44fe58b8a3b4ec997866c086dff1b8831ef0a1fea263cf7dacd03c04c' + + 'bcc2b279e57fa5b953996bfb1dd68817af7fb42cdef7a5294a57fac2b8ad739f1b0299' + + '02818100fbf833c2c631c970240c8e7485f06a3ea2a84822511a8627dd464ef8afaf71' + + '48d1a425b6b8657ddd5246832b8e533020c5bbb568855a6aec3e4221d793f1dc5b2f25' + + '84e2415e48e9a2bd292b134031f99c8eb42fc0bcd0449bf22ce6dec97014efe5ac93eb' + + 'e835877656252cbbb16c415b67b184d2284568a277d59335585cfd02818100d6b8ce27' + + 'c7295d5d16fc3570ed64c8da9303fad29488c1a65e9ad711f90370187dbbfd81316d69' + + '648bc88cc5c83551afff45debacfb61105f709e4c30809b90031ebd686244496c6f69e' + + '692ebdc814f64239f4ad15756ecb78c5a5b09931db183077c546a38c4c743889ad3d3e' + + 'd079b5622ed0120fa0e1f93b593db7d852e05f02818038874b9d83f78178ce2d9efc17' + + '5c83897fd67f306bbfa69f64ee342368ced47c80c3f1ce177a758d64bafb0c9786a442' + + '85fa01cdec3507cdee7dc9b7e2b21d3cbbcc100eee9967843b057329fdcca62998ed0f' + + '11b38ce8b0abc7de39017c71cfd0ae57546c559144cdd0afd0645f7ea8ff07b974d1ed' + + '44fd1f8e00f560bf6d45028181008529ef9073cf8f7b5ff9e21abadf3a4173d3900670' + + 'dfaf59426abcdf0493c13d2f1d1b46b824a6ac1894b3d925250c181e3472c16078056e' + + 'b19a8d28f71f308092753481d49444fdf78c9ea6c24407dc018e77d3afef385b2ff743' + + '9e96237941332dd446cebeffdb4404fe4f71595161d016402c334d0f57c61abe4ff9f4' + + 'cbf90281810087d87708d46763e4ccbeb2d1e9712e5bf0216d70de9420a5b2069b7459' + + 'b99f5d9f7f2fad7cd79aaee67a7f9a34437e3c79a84af0cd8de9dff268eb0c4793f501' + + 'f988d540f6d3475c2079b8227a23d968dec4e3c66503187193459630472bfdb6ba1de7' + + '86c797fa6f4ea65a2a8419262f29678856cb73c9bd4bc89b5e041b2277', 'hex'); + + const spki = Buffer.from( + '30820122300d06092a864886f70d01010105000382010f003082010a0282010100d357' + + '6092e62957364544e7e4233b7bdb293db2085122c479328546f9f0f712f657c4b17868' + + 'c930908cc594f7ed00c01442c1af04c2f678a48ba2c80fd1713e30b5ac50787ac35165' + + '89f171967f6386ada34900a6bb04eecea42bf043ced9a0f94d0cc09e919b9d7166c08a' + + 'b6ce204640aea4c4920db6d86eb916d0dcc0f4341a10380429e7e1032144ea949de8f6' + + 'c0ccbf95fa8e928d70d8a38ce168db45f6f134663d6f656f5ceabc725da8c02aabeaaa' + + '13ac36a75cc0bae135df3114b66589c7ed3cb61559ae5a384f162bfa80dbe4617f86c3' + + 'f1d010c94fe2c9bf019a6e63b3efc028d43cee611c85ec263c906c463772c6911b19ee' + + 'c096ca76ec5e31e1e30203010001', 'hex'); + + // plaintext + const plaintext = Buffer.from( + '5f4dba4f320c0ce876725afce5fbd25bf83e5a7125a08cafe73c3ebac421779df9d55d' + + '180c3ae9942645e1d82fee8c9d294b3cb1a08a9931201b3c0e81fc47cacf8315a2af66' + + '324113c3b66230c34608c4f4593634ce02b267362277f0a840ca74bc3d1a6236952c5e' + + 'd7aaf8a8fecbddfa7584e6978cea5d2a5b9fb7f1b48c8b0be58a305202754d83761073' + + '74793cf026aaee5300727d836cd71e71b345ddb2e44446ffc5b901635413890d910ea3' + + '80984a90191031323f16dbcc9d6be168b84885384ca03e12600ac1c248028af3726cc9' + + '3463882ea8c02aab', 'hex'); + + // For verification tests. + const signatures = { + 'sha-1': Buffer.from( + '532e2f1b69cc2ee8472ef28f7f36a81a24cde4ee83858a7d17054ac3602c98dd432' + + 'e3b3690440935072bb7c031e680701d19b97cb5510d86c9bedbe7d1c06839ceed8a' + + '3b6ac9564131c4512bbbabde237b4daa29fa0d3c97487b48a8fee9d63b50569dc6b' + + '7d10850c8320559343b8537b61214a7e4543a714d65e21c4e478294eb4246cea668' + + 'e351fce0b4e118c758be4fdc46c7b3226bbf40b5b3950d62b8bdaa4f6bb76a30222' + + 'ba32734ed5df4ac8d4fffa7557105087a6acfba5b48516163bb916864e82cb861eb' + + '910dcf6f1adbad5399afd497fb7afb7f75da83c80592ea1ade3e3803b4bb6831b93' + + '3297c0fccc36937e46018797fca85947d29c6a27a81', 'hex'), + 'sha-256': Buffer.from( + '13306aba251dee526459c28352a429cdd85554c7e4a6790a2c6e44b45ebbc4a00a0' + + 'a55244d62a60ddf0ed7d42d7714f12f783ac6709d0e2702db2692aeaacc5a5cdbbe' + + 'c173198caab0d14fe885d0a8da1f166a965c9e25d48470b4884d5c92a4d84417440' + + '5568f4ac0340df6c410fc44cf7ee6d59ba634f9c6240c96b59a06fceeff4dd29622' + + 'e7f983aebf00ecf241f1c912cfd5dc6eeeb94f9d916113e843a98555c24257f8c3e' + + 'dab1f27839f8cc9a963e8b85465a56ec1d876225ae001fbd4244701e2e47d81b557' + + '847e3827e33b36f3f5e8fedfa49a0d34d01dbdaf84fa5c752f0202a8cab2c4cc2cb' + + '5076f6537d9c22d353738e9b39c97026b059ce95d89', 'hex'), + 'sha-384': Buffer.from( + '354fcd1c62e2362d4e8bcedf5150f7b27bec33ab32a27975345a4c8cfeb234669bc' + + '4abaf81e719ddf4c1afae45432cb7aeb9133cbd878de766e872628178a33ac20a02' + + '8a7d8c2b641a5c523b16bbe65eb20bab331c983a961bae6de64e6b9077aa89c846b' + + '8d69dcf7101478d10751a3b87b2a5de2ca6ce71a0bfed7f587a216e053a1e53c4a2' + + 'ace93c261b440fecd4330dd7cbd792b8398502b2a20845a4c2918d872aacf50b302' + + '71357019a58ae18819e75c48e9ef80810860fa04964776ca04b200329674dca5320' + + 'b400f517864e71e087b68b81df613ee24bac34dcf2e64595e12c799070f3f719d93' + + 'd7843b69592346cfc20c6bbf93107f279d6207ec657', 'hex'), + 'sha-512': Buffer.from( + '6229b70897f8620b635487cd4aa996269831ff2931d28714f01e58b165f1082c319' + + '8b8f451788cfd3ed59a78f834d11ce285d1051c42a5cea0227fdefe293444c2518e' + + 'be5cb0055bea4b5806f0eba1b665022a63be44c088fe9ad26325d79f7c41ed97f90' + + '9cd4ca28328e4c4a9de8da67c35dc1c85b71ed6ffaaf99d74b2b88e9f5f05a732f6' + + '688c993b58a0ed35e8f0a106d4e8b1b360e334415c742e94675823db0fd25e22cff' + + '7a6335c70e193235dcda48add6858626bd96311e60f7e5ea4491b6c1e6248afe12b' + + 'bbd54f8869b043a5b0444562813f0a98b300356f306e6b783a29f3bec97ca40ea20' + + '062cab8926ec5d96aa387cc84821a6d72b8ea126e7d', 'hex') + } + + const vectors = [ + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSASSA-PKCS1-v1_5' }, + hash: 'SHA-1', + plaintext, + signature: signatures['sha-1'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSASSA-PKCS1-v1_5' }, + hash: 'SHA-256', + plaintext, + signature: signatures['sha-256'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSASSA-PKCS1-v1_5' }, + hash: 'SHA-384', + plaintext, + signature: signatures['sha-384'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSASSA-PKCS1-v1_5' }, + hash: 'SHA-512', + plaintext, + signature: signatures['sha-512'] + } + ]; + + return vectors; +} diff --git a/test/fixtures/crypto/rsa_pss.js b/test/fixtures/crypto/rsa_pss.js new file mode 100644 index 00000000000000..effb3605a73f1a --- /dev/null +++ b/test/fixtures/crypto/rsa_pss.js @@ -0,0 +1,149 @@ +'use strict'; + +module.exports = function() { + const pkcs8 = Buffer.from( + '308204bf020100300d06092a864886f70d0101010500048204a9308204a5020100028' + + '2010100d3576092e62957364544e7e4233b7bdb293db2085122c479328546f9f0f712' + + 'f657c4b17868c930908cc594f7ed00c01442c1af04c2f678a48ba2c80fd1713e30b5a' + + 'c50787ac3516589f171967f6386ada34900a6bb04eecea42bf043ced9a0f94d0cc09e' + + '919b9d7166c08ab6ce204640aea4c4920db6d86eb916d0dcc0f4341a10380429e7e10' + + '32144ea949de8f6c0ccbf95fa8e928d70d8a38ce168db45f6f134663d6f656f5ceabc' + + '725da8c02aabeaaa13ac36a75cc0bae135df3114b66589c7ed3cb61559ae5a384f162' + + 'bfa80dbe4617f86c3f1d010c94fe2c9bf019a6e63b3efc028d43cee611c85ec263c90' + + '6c463772c6911b19eec096ca76ec5e31e1e3020301000102820101008b375ccb87c82' + + '5c5ff3d53d009916e9641057e18527227a07ab226be1088813a3b38bb7b48f3770551' + + '65fa2a9339d24dc667d5c5ba3427e6a481176eac15ffd49068311e1c283b9f3a8e0cb' + + '809b4630c50aa8f3e45a60b359e19bf8cbb5ecad64e761f1095743ff36aaf5cf0ecb9' + + '7fedaddda60b5bf35d811a75b822230cfaa0192fad40547e275448aa3316bf8e2b4ce' + + '0854fc7708b537ba22d13210b09aec37a2759efc082a1531b23a91730037dde4ef26b' + + '5f96efdcc39fd34c345ad51cbbe44fe58b8a3b4ec997866c086dff1b8831ef0a1fea2' + + '63cf7dacd03c04cbcc2b279e57fa5b953996bfb1dd68817af7fb42cdef7a5294a57fa' + + 'c2b8ad739f1b029902818100fbf833c2c631c970240c8e7485f06a3ea2a84822511a8' + + '627dd464ef8afaf7148d1a425b6b8657ddd5246832b8e533020c5bbb568855a6aec3e' + + '4221d793f1dc5b2f2584e2415e48e9a2bd292b134031f99c8eb42fc0bcd0449bf22ce' + + '6dec97014efe5ac93ebe835877656252cbbb16c415b67b184d2284568a277d5933558' + + '5cfd02818100d6b8ce27c7295d5d16fc3570ed64c8da9303fad29488c1a65e9ad711f' + + '90370187dbbfd81316d69648bc88cc5c83551afff45debacfb61105f709e4c30809b9' + + '0031ebd686244496c6f69e692ebdc814f64239f4ad15756ecb78c5a5b09931db18307' + + '7c546a38c4c743889ad3d3ed079b5622ed0120fa0e1f93b593db7d852e05f02818038' + + '874b9d83f78178ce2d9efc175c83897fd67f306bbfa69f64ee342368ced47c80c3f1c' + + 'e177a758d64bafb0c9786a44285fa01cdec3507cdee7dc9b7e2b21d3cbbcc100eee99' + + '67843b057329fdcca62998ed0f11b38ce8b0abc7de39017c71cfd0ae57546c559144c' + + 'dd0afd0645f7ea8ff07b974d1ed44fd1f8e00f560bf6d45028181008529ef9073cf8f' + + '7b5ff9e21abadf3a4173d3900670dfaf59426abcdf0493c13d2f1d1b46b824a6ac189' + + '4b3d925250c181e3472c16078056eb19a8d28f71f308092753481d49444fdf78c9ea6' + + 'c24407dc018e77d3afef385b2ff7439e96237941332dd446cebeffdb4404fe4f71595' + + '161d016402c334d0f57c61abe4ff9f4cbf90281810087d87708d46763e4ccbeb2d1e9' + + '712e5bf0216d70de9420a5b2069b7459b99f5d9f7f2fad7cd79aaee67a7f9a34437e3' + + 'c79a84af0cd8de9dff268eb0c4793f501f988d540f6d3475c2079b8227a23d968dec4' + + 'e3c66503187193459630472bfdb6ba1de786c797fa6f4ea65a2a8419262f29678856c' + + 'b73c9bd4bc89b5e041b2277', 'hex'); + + const spki = Buffer.from( + '30820122300d06092a864886f70d01010105000382010f003082010a0282010100d35' + + '76092e62957364544e7e4233b7bdb293db2085122c479328546f9f0f712f657c4b178' + + '68c930908cc594f7ed00c01442c1af04c2f678a48ba2c80fd1713e30b5ac50787ac35' + + '16589f171967f6386ada34900a6bb04eecea42bf043ced9a0f94d0cc09e919b9d7166' + + 'c08ab6ce204640aea4c4920db6d86eb916d0dcc0f4341a10380429e7e1032144ea949' + + 'de8f6c0ccbf95fa8e928d70d8a38ce168db45f6f134663d6f656f5ceabc725da8c02a' + + 'abeaaa13ac36a75cc0bae135df3114b66589c7ed3cb61559ae5a384f162bfa80dbe46' + + '17f86c3f1d010c94fe2c9bf019a6e63b3efc028d43cee611c85ec263c906c463772c6' + + '911b19eec096ca76ec5e31e1e30203010001', 'hex'); + + const plaintext = Buffer.from( + '5f4dba4f320c0ce876725afce5fbd25bf83e5a7125a08cafe73c3ebac421779df9d55' + + 'd180c3ae9942645e1d82fee8c9d294b3cb1a08a9931201b3c0e81fc47cacf8315a2af' + + '66324113c3b66230c34608c4f4593634ce02b267362277f0a840ca74bc3d1a6236952' + + 'c5ed7aaf8a8fecbddfa7584e6978cea5d2a5b9fb7f1b48c8b0be58a305202754d8376' + + '107374793cf026aaee5300727d836cd71e71b345ddb2e44446ffc5b901635413890d9' + + '10ea380984a90191031323f16dbcc9d6be168b84885384ca03e12600ac1c248028af3' + + '726cc93463882ea8c02aab', 'hex'); + + const signatures = { + 'sha-1, no salt': Buffer.from( + '1f1cd81ecb3bb31df2e5f0f64c5c0a310c7cf88d19eb512a5078e156d823727af88' + '68a12e5dfbfa976386784ba982c39928789b134952a4a28c6241177bcf2248f2adb' + '60077f545dd17e4f809b3b859fd430d1681e8047126d77369519eed5b618f3297a5' + '75085f0c931ed248cf60bbd7efffa0a8c2b874ba7f81ecd6bf391d01f1e881d827a' + '7b95df874d9adabb7b07f131ab33142a8b0b6d5ca9685671d49b982b67651909eaa' + '17b96b393e04fb36d972f9b258f1b79123df212d39924a4deaec506cf640f1dedd0' + '2d28845f3548d8488652788e2e2146f3ce8a86a556d84b4578f10da29abdb176a68' + '718cc1b2270b0735c2e5ca6c6bb0afac23a5bfa817a', 'hex'), + 'sha-256, no salt': Buffer.from( + '6157d668ed655d978b4c158c8419eb80718dfdfc7d4b34357f9917e9e116b6f3b65' + '040c9d16155c081d6887abcb3ba4ffa0191e4807ee206681aa1d4809ea20de5186b' + '77e3caced07fc9b3d71b9df0ac81b5c3273ff3f74f32a7ad34c65062a31540ced30' + '527efa4b7aa2d27ff7f80535f3e65ce352eb9e18b5054416de959354a4dcccb2542' + 'e33a8358eda620a8653dd6458f56ab94fee1dc01ef42fb8958aa134810e4d8fe1dd' + '4feee6af04742f80da5793875a78a2a4cc08d4e0a68ab03f1c022a0e8a7d3096089' + '92d24ecdd7e8f1895e3e5cd36e49906b531932d9ff958618b1a50f98455f515e0c6' + '3103d2e4e1651afc566eb9cad1e7efae1a9750c3880', 'hex'), + 'sha-384, no salt': Buffer.from( + '7b95aab6b34c0962d228409e30df9b043c1b0baada08e73d887422552b8f1522e2e' + '42bf2b9ff2c6c9aa3eb0cd2370618e8f1a36873595e00bde75a9ce062ec32b5f639' + '4f2267a3f5c11840ff92e6e15bf31cc53e917ca8efc0895fb112c2ef8f681cbb6a4' + '10152f6e930caff1f260e31f983542e68cd15dea17ed3139cac735106fb05fc163b' + '2ed05a0ded939059a10c5cd7619e21b2d206907994274b34a4daefa1ce59b6b319f' + '73955a0918a5e237e1bbfdadb45c907a50083577e7192818845995b4a6d3ff1978e' + '0f9a42695853282e35c3b78133b3e0c624125aff14a1873d198f6304ffec7fc1cf2' + 'adecc6cd14b1f89b1a637f72ed1ff5de7c6b4d96599', 'hex'), + 'sha-512, no salt': Buffer.from( + 'af1bc07fa70add19f3ce1f1bef8dfc6e24af43671cfb97e6b869e86b7ef03550a65' + '81318fff6449afa8b67e73e2a6a14e20677d8b067145a84422574ae0cfd2a5dff70' + 'c6d7e97f6a0e166505079eb4264a43c493f2eb3fb06facc01be60774c277646a280' + '81247679622b220227e9249754867aa8fe1804015c4f98700982eda40e84d0ba033' + '6cf44f582fb8781374804e8fb43eb9d577acf4723587a39a2b4a9e168b767632b7a' + '554f77bc5272821c938c0994b162f7482636f7ffac564a19bd733f4877801dc324d' + 'c47196ef12ca9a8f4921a5496cd6737935ca555b73466ddd817eaff03feda0eb2d6' + '12e3cdb59b1989eeffdc18101d46e56b9ff5c91f95d', 'hex'), + 'sha-1, salted': Buffer.from( + '1f608a71d1884cfe2183b49037aa8555b0139a8a1267a5c5b9cce20701f2ad4bbd5' + 'b329740bff31accc34bf9afd1439a0536bb32b6d427d26968dbc9e9c80d2111d948' + 'c481cb1731778acd3110463241c4f23b3e13b855d162cb153851290fd95f781519e' + '2cef93745a413cfeec8e94fba7822b725d4744318458cf6b4a917b65b15ee6f54b9' + 'c391f6064a9e031f7009f592449c0b46d5457a2799cb0ebd78a102a055ee0470b26' + '0c2b3d8ffbdee0fd47644822090ec55ae6233be1062f441c432ed3c275e74d62013' + '2681ec2e801e9b5b6acc1ad71f8935388f7e2c03370d12e944e3418c2ab63bb42ab' + 'e1bb9e69530f02458ba28400b36806ff78da5791ace', 'hex'), + 'sha-256, salted': Buffer.from( + '8c3d03bde8c42d9453631b0baac89e6296da20543713c004df35bc1a6fae205ab2b' + 'f585369689073cdee345ad6e2783b2dda187b4979ea0457463758156e103eedd0ef' + '1834d35bd6ad540d9b8b225fd1770e514ea0af35f707f2e7a0382be6f5ed9d6b591' + 'd536ce1215b17ef3eeb450bb48a0017497c67be0240470addd2891a81a8f1cf6e80' + 'e3f837fe42376292df555b8b05931b69530597fae36dcd01b1c81767d4ecd4caf06' + 'befc035224bdd2a5e6b89d51539235ac95570e757dbd70fdc15040001b07b937bf0' + '148ccc005f4c272acf5f8fc096a37d26208e96ac341c2d1d212c44d6d5156c934f6' + '6ef42fdbac77a208681550b048b466e32c76c7a7b07', 'hex'), + 'sha-384, salted': Buffer.from( + '79f7284bb4216de68429854edb4218ef78ad1740848567377315db8867a15733c70' + '42e8bf90762e673c90c0e2c58c6c5cef497568bd92a6d219612c4756c55fac45507' + 'f81608bc2720da4eedd5b23e1f3c8740c6b4cd7e4cf0e04342b184c1110199e6508' + '0d73b985e611d66f8e97990816e4917badbb0425dd94383892e2aa96de4db0de093' + '6aee84d5482a3da31b27319f43830fc48703cc7d4eaedb20fd30323dbf3f22608db' + '51637d3b305b3197962658d80935c266d33ccfb297590621f4a967c7245e92b0158' + 'c0dcea943e2ace719ebdb196a9bae7df3ed9cc62765e27b63571743e28a0538db08' + '25cad2539eb5de5e6a320a88b573ec1972c26401530', 'hex'), + 'sha-512, salted': Buffer.from( + 'b74f3099d80787118b1f9de79fc207893e0d2d75c4110f4b159b85ba07d63a0256f' + 'c3cd0f66ce8d9a2e3cf7a3d5a7b9c0befac6638894a3e36ce75e649ee069dd8dd98' + 'aa8b602474c98b14bb03492de551a9e8e77934ef9b684583934f218d9576be240b5' + 'c4f362eaf5e0140c8ea92639085a6269653505dcfa004226db9f63277653a64a182' + '6e4babb17ab54dd8543dcf1ce809706d6816e6a75ff846a3d4c18d11bdeb1f31b10' + 'd55a3795b6496319e6e751504d86a4e7bb6535b9f0415e815d8c789c5b1e387f2a8' + 'c00fef6e327462cb7e525b8f945be5b17248e0e0a4d855d397e22d067ce4539373d' + 'fba46d1799250afc70f535006cacd2766f5ddcf8f91', 'hex') + } + + const vectors = [ + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 0 }, + hash: 'SHA-1', + plaintext, + signature: signatures['sha-1, no salt'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 0 }, + hash: 'SHA-256', + plaintext, + signature: signatures['sha-256, no salt'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 0 }, + hash: 'SHA-384', + plaintext, + signature: signatures['sha-384, no salt'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 0 }, + hash: 'SHA-512', + plaintext, + signature: signatures['sha-512, no salt'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 20 }, + hash: 'SHA-1', + plaintext, + signature: signatures['sha-1, salted'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 32 }, + hash: 'SHA-256', + plaintext, + signature: signatures['sha-256, salted'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 48 }, + hash: 'SHA-384', + plaintext, + signature: signatures['sha-384, salted'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 64 }, + hash: 'SHA-512', + plaintext, + signature: signatures['sha-512, salted'] + } + ]; + + return vectors; +}; diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js index 6cf14f70e4d437..da638135314fbb 100644 --- a/test/parallel/test-crypto-authenticated.js +++ b/test/parallel/test-crypto-authenticated.js @@ -18,7 +18,7 @@ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. - +// Flags: --no-warnings 'use strict'; const common = require('../common'); if (!common.hasCrypto) @@ -43,8 +43,8 @@ const errMessages = { auth: / auth/, state: / state/, FIPS: /not supported in FIPS mode/, - length: /Invalid IV length/, - authTagLength: /Invalid authentication tag length/ + length: /Invalid initialization vector/, + authTagLength: /Invalid authentication tag/ }; const ciphers = crypto.getCiphers(); @@ -244,8 +244,8 @@ for (const test of TEST_CASES) { 'qkuZpJWCewa6Szih'); decrypt.setAuthTag(Buffer.from('1'.repeat(length))); }, { - name: 'Error', - message: `Invalid authentication tag length: ${length}` + name: 'TypeError', + message: /Invalid authentication tag length/ }); assert.throws(() => { @@ -256,8 +256,8 @@ for (const test of TEST_CASES) { authTagLength: length }); }, { - name: 'Error', - message: `Invalid authentication tag length: ${length}` + name: 'TypeError', + message: /Invalid authentication tag length/ }); assert.throws(() => { @@ -268,8 +268,8 @@ for (const test of TEST_CASES) { authTagLength: length }); }, { - name: 'Error', - message: `Invalid authentication tag length: ${length}` + name: 'TypeError', + message: /Invalid authentication tag length/ }); } } @@ -303,8 +303,8 @@ for (const test of TEST_CASES) { // This tag would normally be allowed. decipher.setAuthTag(Buffer.from('1'.repeat(12))); }, { - name: 'Error', - message: 'Invalid authentication tag length: 12' + name: 'TypeError', + message: /Invalid authentication tag length/ }); // The Decipher object should be left intact. @@ -475,12 +475,12 @@ for (const test of TEST_CASES) { cipher().setAAD(Buffer.alloc(0), { plaintextLength: maxMessageSize + 1 }); - }, /^Error: Message exceeds maximum size$/); + }, /Invalid message length$/); const msg = Buffer.alloc(maxMessageSize + 1); assert.throws(() => { cipher().update(msg); - }, /^Error: Message exceeds maximum size$/); + }, /Invalid message length/); const c = cipher(); c.setAAD(Buffer.alloc(0), { @@ -501,7 +501,7 @@ for (const test of TEST_CASES) { authTagLength: 10 }); cipher.setAAD(Buffer.from('0123456789', 'hex')); - }, /^Error: plaintextLength required for CCM mode with AAD$/); + }, /options\.plaintextLength required for CCM mode with AAD/); if (!common.hasFipsCrypto) { assert.throws(() => { @@ -512,7 +512,7 @@ for (const test of TEST_CASES) { authTagLength: 10 }); cipher.setAAD(Buffer.from('0123456789', 'hex')); - }, /^Error: plaintextLength required for CCM mode with AAD$/); + }, /options\.plaintextLength required for CCM mode with AAD/); } } diff --git a/test/parallel/test-crypto-certificate.js b/test/parallel/test-crypto-certificate.js index 8d3219059d039e..4a5f1f149fe6c7 100644 --- a/test/parallel/test-crypto-certificate.js +++ b/test/parallel/test-crypto-certificate.js @@ -35,6 +35,10 @@ const spkacChallenge = 'this-is-a-challenge'; const spkacFail = fixtures.readKey('rsa_spkac_invalid.spkac'); const spkacPublicPem = fixtures.readKey('rsa_public.pem'); +function copyArrayBuffer(buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + function checkMethods(certificate) { assert.strictEqual(certificate.verifySpkac(spkacValid), true); @@ -51,6 +55,39 @@ function checkMethods(certificate) { spkacChallenge ); assert.strictEqual(certificate.exportChallenge(spkacFail), ''); + + const ab = copyArrayBuffer(spkacValid); + assert.strictEqual(certificate.verifySpkac(ab), true); + assert.strictEqual(certificate.verifySpkac(new Uint8Array(ab)), true); + assert.strictEqual(certificate.verifySpkac(new DataView(ab)), true); +} + +{ + // Test maximum size of input buffer + let buf; + let skip = false; + try { + buf = Buffer.alloc(2 ** 31); + } catch { + // The allocation may fail on some systems. That is expected due + // to architecture and memory constraints. If it does, go ahead + // and skip this test. + skip = true; + } + if (!skip) { + assert.throws( + () => Certificate.verifySpkac(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws( + () => Certificate.exportChallenge(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws( + () => Certificate.exportPublicKey(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + } } { @@ -70,24 +107,15 @@ function stripLineEndings(obj) { // Direct call Certificate() should return instance assert(Certificate() instanceof Certificate); -[1, {}, [], Infinity, true, 'test', undefined, null].forEach((val) => { +[1, {}, [], Infinity, true, undefined, null].forEach((val) => { assert.throws( () => Certificate.verifySpkac(val), - { - code: 'ERR_INVALID_ARG_TYPE', - message: 'The "spkac" argument must be an instance of Buffer, ' + - `TypedArray, or DataView.${common.invalidArgTypeHelper(val)}` - } + { code: 'ERR_INVALID_ARG_TYPE' } ); }); [1, {}, [], Infinity, true, undefined, null].forEach((val) => { - const errObj = { - code: 'ERR_INVALID_ARG_TYPE', - message: 'The "spkac" argument must be of type string or an instance of ' + - 'Buffer, TypedArray, or DataView.' + - common.invalidArgTypeHelper(val) - }; + const errObj = { code: 'ERR_INVALID_ARG_TYPE' }; assert.throws(() => Certificate.exportPublicKey(val), errObj); assert.throws(() => Certificate.exportChallenge(val), errObj); }); diff --git a/test/parallel/test-crypto-cipher-decipher.js b/test/parallel/test-crypto-cipher-decipher.js index 4bb765d9ca408b..603d90ff67941c 100644 --- a/test/parallel/test-crypto-cipher-decipher.js +++ b/test/parallel/test-crypto-cipher-decipher.js @@ -98,9 +98,7 @@ testCipher2(Buffer.from('0123456789abcdef')); () => crypto.createCipher('aes-256-cbc', null), { code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: 'The "password" argument must be of type string or an instance' + - ' of Buffer, TypedArray, or DataView. Received null' + name: 'TypeError' }); assert.throws( @@ -108,8 +106,6 @@ testCipher2(Buffer.from('0123456789abcdef')); { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "data" argument must be of type string or an instance' + - ' of Buffer, TypedArray, or DataView. Received null' }); assert.throws( @@ -117,8 +113,6 @@ testCipher2(Buffer.from('0123456789abcdef')); { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "buffer" argument must be an instance' + - ' of Buffer, TypedArray, or DataView. Received null' }); } @@ -142,8 +136,6 @@ testCipher2(Buffer.from('0123456789abcdef')); { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "buffer" argument must be an instance of Buffer, ' + - 'TypedArray, or DataView. Received null' }); assert.throws( @@ -151,8 +143,6 @@ testCipher2(Buffer.from('0123456789abcdef')); { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "password" argument must be of type string or an ' + - 'instance of Buffer, TypedArray, or DataView. Received null' }); } diff --git a/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index 07591fdfffd072..ea5b675db13487 100644 --- a/test/parallel/test-crypto-cipheriv-decipheriv.js +++ b/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -101,8 +101,6 @@ function testCipher3(key, iv) { { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "key" argument must be of type string or an instance of ' + - 'Buffer, TypedArray, DataView, or KeyObject. Received null' }); assert.throws( @@ -110,8 +108,6 @@ function testCipher3(key, iv) { { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "iv" argument must be of type string or an instance of ' + - 'Buffer, TypedArray, or DataView. Received type number (10)' }); } @@ -138,8 +134,6 @@ function testCipher3(key, iv) { { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "key" argument must be of type string or an instance of ' + - 'Buffer, TypedArray, DataView, or KeyObject. Received null' }); assert.throws( @@ -147,8 +141,6 @@ function testCipher3(key, iv) { { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "iv" argument must be of type string or an instance of ' + - 'Buffer, TypedArray, or DataView. Received type number (10)' }); } @@ -167,7 +159,7 @@ if (!common.hasFipsCrypto) { crypto.createCipheriv('aes-128-ecb', Buffer.alloc(16), Buffer.alloc(0)); crypto.createCipheriv('aes-128-ecb', Buffer.alloc(16), null); -const errMessage = /Invalid IV length/; +const errMessage = /Invalid initialization vector/; // But non-empty IVs should be rejected. for (let n = 1; n < 256; n += 1) { @@ -192,7 +184,7 @@ for (let n = 0; n < 256; n += 1) { // And so should null be. assert.throws(() => { crypto.createCipheriv('aes-128-cbc', Buffer.alloc(16), null); -}, /Missing IV for cipher aes-128-cbc/); +}, /Invalid initialization vector/); // Zero-sized IV should be rejected in GCM mode. assert.throws( diff --git a/test/parallel/test-crypto-dh.js b/test/parallel/test-crypto-dh.js index f51ffba042421a..5336301391b6c7 100644 --- a/test/parallel/test-crypto-dh.js +++ b/test/parallel/test-crypto-dh.js @@ -109,9 +109,6 @@ for (const g of [Buffer.from([]), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "sizeOrKey" argument must be one of type number or string' + - ' or an instance of Buffer, TypedArray, or DataView.' + - common.invalidArgTypeHelper(input) } ); }); @@ -387,14 +384,14 @@ if (availableCurves.has('prime256v1') && availableCurves.has('secp256k1')) { assert.throws(() => { // Error because the public key does not match the private key anymore. ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'); - }, /^Error: Invalid key pair$/); + }, /Invalid key pair/); // Set to a valid key to show that later attempts to set an invalid key are // rejected. ecdh5.setPrivateKey(cafebabeKey, 'hex'); // Some invalid private keys for the secp256k1 curve. - const errMessage = /^Error: Private key is not valid for specified curve\.$/; + const errMessage = /Private key is not valid for specified curve/; ['0000000000000000000000000000000000000000000000000000000000000000', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', diff --git a/test/parallel/test-crypto-ecdh-convert-key.js b/test/parallel/test-crypto-ecdh-convert-key.js index 69ee339aa7a653..f4d5a651ed6b88 100644 --- a/test/parallel/test-crypto-ecdh-convert-key.js +++ b/test/parallel/test-crypto-ecdh-convert-key.js @@ -37,7 +37,7 @@ assert.throws( () => ECDH.convertKey(cafebabePubPtComp, 'badcurve'), { name: 'TypeError', - message: 'Invalid ECDH curve name' + message: 'Invalid EC curve name' }); if (getCurves().includes('secp256k1')) { diff --git a/test/parallel/test-crypto-hash.js b/test/parallel/test-crypto-hash.js index f3f4df928c4bc4..af2146982c7a3b 100644 --- a/test/parallel/test-crypto-hash.js +++ b/test/parallel/test-crypto-hash.js @@ -129,8 +129,6 @@ assert.throws( { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "data" argument must be of type string or an instance of ' + - 'Buffer, TypedArray, or DataView. Received undefined' }); // Default UTF-8 encoding diff --git a/test/parallel/test-crypto-hkdf.js b/test/parallel/test-crypto-hkdf.js new file mode 100644 index 00000000000000..5f8bde384c565f --- /dev/null +++ b/test/parallel/test-crypto-hkdf.js @@ -0,0 +1,220 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { kMaxLength } = require('buffer'); +const assert = require('assert'); +const { + createSecretKey, + hkdf, + hkdfSync, + getHashes +} = require('crypto'); + +{ + [1, {}, [], false, Infinity].forEach((i) => { + assert.throws(() => hkdf(i, 'a'), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "digest" argument must be of type string/ + }); + assert.throws(() => hkdfSync(i, 'a'), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "digest" argument must be of type string/ + }); + }); + + [1, {}, [], false, Infinity].forEach((i) => { + assert.throws(() => hkdf('sha256', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "key" argument must be / + }); + assert.throws(() => hkdfSync('sha256', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "key" argument must be / + }); + }); + + [1, {}, [], false, Infinity].forEach((i) => { + assert.throws(() => hkdf('sha256', 'secret', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "salt" argument must be / + }); + assert.throws(() => hkdfSync('sha256', 'secret', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "salt" argument must be / + }); + }); + + [1, {}, [], false, Infinity].forEach((i) => { + assert.throws(() => hkdf('sha256', 'secret', 'salt', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "info" argument must be / + }); + assert.throws(() => hkdfSync('sha256', 'secret', 'salt', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "info" argument must be / + }); + }); + + ['test', {}, [], false].forEach((i) => { + assert.throws(() => hkdf('sha256', 'secret', 'salt', 'info', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "length" argument must be of type number/ + }); + assert.throws(() => hkdfSync('sha256', 'secret', 'salt', 'info', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "length" argument must be of type number/ + }); + }); + + assert.throws(() => hkdf('sha256', 'secret', 'salt', 'info', -1), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws(() => hkdfSync('sha256', 'secret', 'salt', 'info', -1), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws(() => hkdf('sha256', 'secret', 'salt', 'info', + kMaxLength + 1), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws(() => hkdfSync('sha256', 'secret', 'salt', 'info', + kMaxLength + 1), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws(() => hkdfSync('unknown', 'a', '', '', 10), { + code: 'ERR_CRYPTO_INVALID_DIGEST' + }); + + assert.throws(() => hkdf('unknown', 'a', '', '', 10, common.mustNotCall()), { + code: 'ERR_CRYPTO_INVALID_DIGEST' + }); + + assert.throws(() => hkdf('unknown', 'a', '', Buffer.alloc(1025), 10, + common.mustNotCall()), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws(() => hkdfSync('unknown', 'a', '', Buffer.alloc(1025), 10), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws( + () => hkdf('sha512', 'a', '', '', 64 * 255 + 1, common.mustNotCall()), { + code: 'ERR_CRYPTO_INVALID_KEYLEN' + }); + + assert.throws( + () => hkdfSync('sha512', 'a', '', '', 64 * 255 + 1), { + code: 'ERR_CRYPTO_INVALID_KEYLEN' + }); +} + +[ + ['sha256', 'secret', 'salt', 'info', 10], + ['sha512', 'secret', 'salt', '', 15], + ['whirlpool', 'secret', '', 'info', 20], +].forEach(([ hash, secret, salt, info, length ]) => { + { + const syncResult = hkdfSync(hash, secret, salt, info, length); + assert(syncResult instanceof ArrayBuffer); + let is_async = false; + hkdf(hash, secret, salt, info, length, + common.mustCall((err, asyncResult) => { + assert.ifError(err); + assert(is_async); + assert(asyncResult instanceof ArrayBuffer); + assert.deepStrictEqual(syncResult, asyncResult); + })); + // Keep this after the hkdf call above. This verifies + // that the callback is invoked asynchronously. + is_async = true; + } + + { + const buf_secret = Buffer.from(secret); + const buf_salt = Buffer.from(salt); + const buf_info = Buffer.from(info); + + const syncResult = hkdfSync(hash, buf_secret, buf_salt, buf_info, length); + hkdf(hash, buf_secret, buf_salt, buf_info, length, + common.mustCall((err, asyncResult) => { + assert.ifError(err); + assert.deepStrictEqual(syncResult, asyncResult); + })); + } + + { + const key_secret = createSecretKey(Buffer.from(secret)); + const buf_salt = Buffer.from(salt); + const buf_info = Buffer.from(info); + + const syncResult = hkdfSync(hash, key_secret, buf_salt, buf_info, length); + hkdf(hash, key_secret, buf_salt, buf_info, length, + common.mustCall((err, asyncResult) => { + assert.ifError(err); + assert.deepStrictEqual(syncResult, asyncResult); + })); + } + + { + const ta_secret = new Uint8Array(Buffer.from(secret)); + const ta_salt = new Uint16Array(Buffer.from(salt)); + const ta_info = new Uint32Array(Buffer.from(info)); + + const syncResult = hkdfSync(hash, ta_secret, ta_salt, ta_info, length); + hkdf(hash, ta_secret, ta_salt, ta_info, length, + common.mustCall((err, asyncResult) => { + assert.ifError(err); + assert.deepStrictEqual(syncResult, asyncResult); + })); + } + + { + const ta_secret = new Uint8Array(Buffer.from(secret)); + const ta_salt = new Uint16Array(Buffer.from(salt)); + const ta_info = new Uint32Array(Buffer.from(info)); + + const syncResult = hkdfSync( + hash, + ta_secret.buffer, + ta_salt.buffer, + ta_info.buffer, + length); + hkdf(hash, ta_secret, ta_salt, ta_info, length, + common.mustCall((err, asyncResult) => { + assert.ifError(err); + assert.deepStrictEqual(syncResult, asyncResult); + })); + } + + { + const ta_secret = new Uint8Array(Buffer.from(secret)); + const sa_salt = new SharedArrayBuffer(0); + const sa_info = new SharedArrayBuffer(1); + + const syncResult = hkdfSync( + hash, + ta_secret.buffer, + sa_salt, + sa_info, + length); + hkdf(hash, ta_secret, sa_salt, sa_info, length, + common.mustCall((err, asyncResult) => { + assert.ifError(err); + assert.deepStrictEqual(syncResult, asyncResult); + })); + } +}); + +{ + const kKnownUnsupported = ['shake128', 'shake256']; + getHashes() + .filter((hash) => !kKnownUnsupported.includes(hash)) + .forEach((hash) => { + assert(hkdfSync(hash, 'key', 'salt', 'info', 5)); + }); +} diff --git a/test/parallel/test-crypto-hmac.js b/test/parallel/test-crypto-hmac.js index 160d09036dbe7f..2f60d8124f03f6 100644 --- a/test/parallel/test-crypto-hmac.js +++ b/test/parallel/test-crypto-hmac.js @@ -36,8 +36,6 @@ assert.throws( { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "key" argument must be of type string or an instance of ' + - 'Buffer, TypedArray, DataView, or KeyObject. Received null' }); function testHmac(algo, key, data, expected) { @@ -450,5 +448,5 @@ assert.strictEqual( { assert.throws( () => crypto.createHmac('sha7', 'key'), - /Unknown message digest/); + /Invalid digest/); } diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index ae818152a567f1..fdb63afa7659f3 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -103,9 +103,6 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.throws(() => createPrivateKey(createPublicKey(privatePem)), { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE', - message: 'The "key" argument must be of type string or an instance of ' + - 'Buffer, TypedArray, or DataView. Received an instance of ' + - 'PublicKeyObject' }); // Similarly, passing an existing private key object to createPrivateKey @@ -114,9 +111,6 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.throws(() => createPrivateKey(privateKey), { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE', - message: 'The "key" argument must be of type string or an instance of ' + - 'Buffer, TypedArray, or DataView. Received an instance of ' + - 'PrivateKeyObject' }); } @@ -212,11 +206,11 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.throws(() => { createPrivateKey({ key: '' }); }, { - message: 'error:2007E073:BIO routines:BIO_new_mem_buf:null parameter', - code: 'ERR_OSSL_BIO_NULL_PARAMETER', - reason: 'null parameter', - library: 'BIO routines', - function: 'BIO_new_mem_buf', + message: 'error:0909006C:PEM routines:get_name:no start line', + code: 'ERR_OSSL_PEM_NO_START_LINE', + reason: 'no start line', + library: 'PEM routines', + function: 'get_name', }); // This should not abort either: https://github.com/nodejs/node/issues/29904 diff --git a/test/parallel/test-crypto-keygen.js b/test/parallel/test-crypto-keygen.js index ed4750682d75c2..ae62db23c0940f 100644 --- a/test/parallel/test-crypto-keygen.js +++ b/test/parallel/test-crypto-keygen.js @@ -869,7 +869,7 @@ const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); for (const modulusLength of [undefined, null, 'a', true, {}, [], 512.1, -1]) { assert.throws(() => generateKeyPair('rsa', { modulusLength - }), { + }, common.mustNotCall()), { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE', message: "The property 'options.modulusLength' is invalid. " + @@ -882,7 +882,7 @@ const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); assert.throws(() => generateKeyPair('rsa', { modulusLength: 4096, publicExponent - }), { + }, common.mustNotCall()), { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE', message: "The property 'options.publicExponent' is invalid. " + @@ -897,7 +897,7 @@ const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); for (const modulusLength of [undefined, null, 'a', true, {}, [], 4096.1]) { assert.throws(() => generateKeyPair('dsa', { modulusLength - }), { + }, common.mustNotCall()), { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE', message: "The property 'options.modulusLength' is invalid. " + @@ -910,7 +910,7 @@ const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); assert.throws(() => generateKeyPair('dsa', { modulusLength: 2048, divisorLength - }), { + }, common.mustNotCall()), { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE', message: "The property 'options.divisorLength' is invalid. " + @@ -930,7 +930,7 @@ const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); }); }, { name: 'TypeError', - message: 'Invalid ECDH curve name' + message: 'Invalid EC curve name' }); // Test error type when curve is not a string @@ -1166,6 +1166,7 @@ const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); }); } } + { // Test RSA-PSS. assert.throws( @@ -1192,7 +1193,7 @@ const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); saltLength: 16, hash: 'sha256', mgf1Hash - }); + }, common.mustNotCall()); }, { name: 'TypeError', diff --git a/test/parallel/test-crypto-pbkdf2.js b/test/parallel/test-crypto-pbkdf2.js index fe7f316d4ae746..d963395c03388f 100644 --- a/test/parallel/test-crypto-pbkdf2.js +++ b/test/parallel/test-crypto-pbkdf2.js @@ -144,14 +144,11 @@ assert.throws( 'Received null' }); [1, {}, [], true, undefined, null].forEach((input) => { - const msgPart2 = 'an instance of Buffer, TypedArray, or DataView.' + - common.invalidArgTypeHelper(input); assert.throws( () => crypto.pbkdf2(input, 'salt', 8, 8, 'sha256', common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: `The "password" argument must be of type string or ${msgPart2}` } ); @@ -160,7 +157,6 @@ assert.throws( { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: `The "salt" argument must be of type string or ${msgPart2}` } ); @@ -169,7 +165,6 @@ assert.throws( { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: `The "password" argument must be of type string or ${msgPart2}` } ); @@ -178,7 +173,6 @@ assert.throws( { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: `The "salt" argument must be of type string or ${msgPart2}` } ); }); @@ -215,6 +209,12 @@ crypto.pbkdf2(new Float32Array(10), 'salt', 8, 8, 'sha256', common.mustCall()); crypto.pbkdf2('pass', new Float32Array(10), 8, 8, 'sha256', common.mustCall()); crypto.pbkdf2(new Float64Array(10), 'salt', 8, 8, 'sha256', common.mustCall()); crypto.pbkdf2('pass', new Float64Array(10), 8, 8, 'sha256', common.mustCall()); +crypto.pbkdf2(new ArrayBuffer(10), 'salt', 8, 8, 'sha256', common.mustCall()); +crypto.pbkdf2('pass', new ArrayBuffer(10), 8, 8, 'sha256', common.mustCall()); +crypto.pbkdf2(new SharedArrayBuffer(10), 'salt', 8, 8, 'sha256', + common.mustCall()); +crypto.pbkdf2('pass', new SharedArrayBuffer(10), 8, 8, 'sha256', + common.mustCall()); crypto.pbkdf2Sync(new Uint8Array(10), 'salt', 8, 8, 'sha256'); crypto.pbkdf2Sync('pass', new Uint8Array(10), 8, 8, 'sha256'); @@ -226,6 +226,10 @@ crypto.pbkdf2Sync(new Float32Array(10), 'salt', 8, 8, 'sha256'); crypto.pbkdf2Sync('pass', new Float32Array(10), 8, 8, 'sha256'); crypto.pbkdf2Sync(new Float64Array(10), 'salt', 8, 8, 'sha256'); crypto.pbkdf2Sync('pass', new Float64Array(10), 8, 8, 'sha256'); +crypto.pbkdf2Sync(new ArrayBuffer(10), 'salt', 8, 8, 'sha256'); +crypto.pbkdf2Sync('pass', new ArrayBuffer(10), 8, 8, 'sha256'); +crypto.pbkdf2Sync(new SharedArrayBuffer(10), 'salt', 8, 8, 'sha256'); +crypto.pbkdf2Sync('pass', new SharedArrayBuffer(10), 8, 8, 'sha256'); assert.throws( () => crypto.pbkdf2('pass', 'salt', 8, 8, 'md55', common.mustNotCall()), @@ -244,3 +248,11 @@ assert.throws( message: 'Invalid digest: md55' } ); + +const kNotPBKDF2Supported = ['shake128', 'shake256']; +crypto.getHashes() + .filter((hash) => !kNotPBKDF2Supported.includes(hash)) + .forEach((hash) => { + crypto.pbkdf2Sync(new Uint8Array(10), 'salt', 8, 8, hash); + crypto.pbkdf2(new Uint8Array(10), 'salt', 8, 8, hash, common.mustCall()); + }); diff --git a/test/parallel/test-crypto-random.js b/test/parallel/test-crypto-random.js index e175550c4b4c12..523accb76d55e6 100644 --- a/test/parallel/test-crypto-random.js +++ b/test/parallel/test-crypto-random.js @@ -28,6 +28,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const cryptop = require('crypto').webcrypto; const { kMaxLength } = require('buffer'); const { inspect } = require('util'); @@ -101,6 +102,30 @@ common.expectWarning('DeprecationWarning', }); } +{ + [ + new Uint16Array(10), + new Uint32Array(10), + ].forEach((buf) => { + const before = Buffer.from(buf.buffer).toString('hex'); + cryptop.getRandomValues(buf); + const after = Buffer.from(buf.buffer).toString('hex'); + assert.notStrictEqual(before, after); + }); +} + +{ + [ + new ArrayBuffer(10), + new SharedArrayBuffer(10) + ].forEach((buf) => { + const before = Buffer.from(buf).toString('hex'); + crypto.randomFillSync(buf); + const after = Buffer.from(buf).toString('hex'); + assert.notStrictEqual(before, after); + }); +} + { const buf = Buffer.alloc(10); const before = buf.toString('hex'); @@ -138,6 +163,20 @@ common.expectWarning('DeprecationWarning', }); } +{ + [ + new ArrayBuffer(10), + new SharedArrayBuffer(10) + ].forEach((buf) => { + const before = Buffer.from(buf).toString('hex'); + crypto.randomFill(buf, common.mustCall((err, buf) => { + assert.ifError(err); + const after = Buffer.from(buf).toString('hex'); + assert.notStrictEqual(before, after); + })); + }); +} + { const buf = Buffer.alloc(10); const before = buf.toString('hex'); @@ -304,7 +343,6 @@ assert.throws( ); }); - ['pseudoRandomBytes', 'prng', 'rng'].forEach((f) => { const desc = Object.getOwnPropertyDescriptor(crypto, f); assert.ok(desc); diff --git a/test/parallel/test-crypto-rsa-dsa.js b/test/parallel/test-crypto-rsa-dsa.js index 9b8c3f67a242f2..04de044be69aaa 100644 --- a/test/parallel/test-crypto-rsa-dsa.js +++ b/test/parallel/test-crypto-rsa-dsa.js @@ -25,6 +25,8 @@ const dsaKeyPemEncrypted = fixtures.readKey('dsa_private_encrypted.pem', const rsaPkcs8KeyPem = fixtures.readKey('rsa_private_pkcs8.pem'); const dsaPkcs8KeyPem = fixtures.readKey('dsa_private_pkcs8.pem'); +const ec = new TextEncoder(); + const decryptError = { message: 'error:06065064:digital envelope routines:EVP_DecryptFinal_ex:' + 'bad decrypt', @@ -34,6 +36,10 @@ const decryptError = { library: 'digital envelope routines', }; +function getBufferCopy(buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + // Test RSA encryption/decryption { const input = 'I AM THE WALRUS'; @@ -42,8 +48,25 @@ const decryptError = { let encryptedBuffer = crypto.publicEncrypt(rsaPubPem, bufferToEncrypt); + // Test other input types + let otherEncrypted; + { + const ab = getBufferCopy(ec.encode(rsaPubPem)); + const ab2enc = getBufferCopy(bufferToEncrypt); + + crypto.publicEncrypt(ab, ab2enc); + crypto.publicEncrypt(new Uint8Array(ab), new Uint8Array(ab2enc)); + crypto.publicEncrypt(new DataView(ab), new DataView(ab2enc)); + otherEncrypted = crypto.publicEncrypt({ + key: Buffer.from(ab).toString('hex'), + encoding: 'hex' + }, Buffer.from(ab2enc).toString('hex')); + } + let decryptedBuffer = crypto.privateDecrypt(rsaKeyPem, encryptedBuffer); + const otherDecrypted = crypto.privateDecrypt(rsaKeyPem, otherEncrypted); assert.strictEqual(decryptedBuffer.toString(), input); + assert.strictEqual(otherDecrypted.toString(), input); decryptedBuffer = crypto.privateDecrypt(rsaPkcs8KeyPem, encryptedBuffer); assert.strictEqual(decryptedBuffer.toString(), input); @@ -52,6 +75,21 @@ const decryptError = { key: rsaKeyPemEncrypted, passphrase: 'password' }, encryptedBuffer); + + const otherDecryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: ec.encode('password') + }, encryptedBuffer); + + assert.strictEqual( + otherDecryptedBufferWithPassword.toString(), + decryptedBufferWithPassword.toString()); + + decryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, encryptedBuffer); + assert.strictEqual(decryptedBufferWithPassword.toString(), input); encryptedBuffer = crypto.publicEncrypt({ @@ -206,13 +244,24 @@ assert.throws(() => { JSON.parse(fixtures.readSync('rsa-oaep-test-vectors.js', 'utf8')); for (const { ct, oaepHash, oaepLabel } of decryptionTests) { + const label = oaepLabel ? Buffer.from(oaepLabel, 'hex') : undefined; + const copiedLabel = oaepLabel ? getBufferCopy(label) : undefined; + const decrypted = crypto.privateDecrypt({ key: rsaPkcs8KeyPem, oaepHash, - oaepLabel: oaepLabel ? Buffer.from(oaepLabel, 'hex') : undefined + oaepLabel: oaepLabel ? label : undefined }, Buffer.from(ct, 'hex')); assert.strictEqual(decrypted.toString('utf8'), 'Hello Node.js'); + + const otherDecrypted = crypto.privateDecrypt({ + key: rsaPkcs8KeyPem, + oaepHash, + oaepLabel: copiedLabel + }, Buffer.from(ct, 'hex')); + + assert.strictEqual(otherDecrypted.toString('utf8'), 'Hello Node.js'); } } @@ -238,7 +287,7 @@ for (const fn of [crypto.publicEncrypt, crypto.privateDecrypt]) { }); } - for (const oaepLabel of [0, false, null, Symbol(), () => {}, {}, 'foo']) { + for (const oaepLabel of [0, false, null, Symbol(), () => {}, {}]) { assert.throws(() => { fn({ key: rsaPubPem, diff --git a/test/parallel/test-crypto-secret-keygen.js b/test/parallel/test-crypto-secret-keygen.js new file mode 100644 index 00000000000000..10a776ff0598a2 --- /dev/null +++ b/test/parallel/test-crypto-secret-keygen.js @@ -0,0 +1,124 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +const { + generateKey, + generateKeySync +} = require('crypto'); + +[1, true, [], {}, Infinity, null, undefined].forEach((i) => { + assert.throws(() => generateKey(i, 1, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "type" argument must be / + }); + assert.throws(() => generateKeySync(i, 1), { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "type" argument must be / + }); +}); + +['', true, [], null, undefined].forEach((i) => { + assert.throws(() => generateKey('aes', i, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options" argument must be / + }); + assert.throws(() => generateKeySync('aes', i), { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options" argument must be / + }); +}); + +['', true, {}, [], null, undefined].forEach((length) => { + assert.throws(() => generateKey('hmac', { length }, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.length" property must be / + }); + assert.throws(() => generateKeySync('hmac', { length }), { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.length" property must be / + }); +}); + +assert.throws(() => generateKey('aes', { length: 256 }), { + code: 'ERR_INVALID_CALLBACK' +}); + +assert.throws(() => generateKey('hmac', { length: -1 }, common.mustNotCall()), { + code: 'ERR_OUT_OF_RANGE' +}); + +assert.throws( + () => generateKey('hmac', { length: 2 ** 31 }, common.mustNotCall()), { + code: 'ERR_OUT_OF_RANGE' + }); + +assert.throws(() => generateKeySync('hmac', { length: -1 }), { + code: 'ERR_OUT_OF_RANGE' +}); + +assert.throws( + () => generateKeySync('hmac', { length: 2 ** 31 }), { + code: 'ERR_OUT_OF_RANGE' + }); + +assert.throws(() => generateKeySync('aes', { length: 123 }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /The property 'options\.length' must be one of: 128, 192, 256/ +}); + +{ + const key = generateKeySync('aes', { length: 128 }); + assert(key); + const keybuf = key.export(); + assert.strictEqual(keybuf.byteLength, 128 / 8); + + generateKey('aes', { length: 128 }, common.mustCall((err, key) => { + assert.ifError(err); + assert(key); + const keybuf = key.export(); + assert.strictEqual(keybuf.byteLength, 128 / 8); + })); +} + +{ + const key = generateKeySync('aes', { length: 256 }); + assert(key); + const keybuf = key.export(); + assert.strictEqual(keybuf.byteLength, 256 / 8); + + generateKey('aes', { length: 256 }, common.mustCall((err, key) => { + assert.ifError(err); + assert(key); + const keybuf = key.export(); + assert.strictEqual(keybuf.byteLength, 256 / 8); + })); +} + +{ + const key = generateKeySync('hmac', { length: 123 }); + assert(key); + const keybuf = key.export(); + assert.strictEqual(keybuf.byteLength, Math.floor(123 / 8)); + + generateKey('hmac', { length: 123 }, common.mustCall((err, key) => { + assert.ifError(err); + assert(key); + const keybuf = key.export(); + assert.strictEqual(keybuf.byteLength, Math.floor(123 / 8)); + })); +} + +assert.throws( + () => generateKey('unknown', { length: 123 }, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + message: /The argument 'type' must be a supported key type/ + }); + +assert.throws(() => generateKeySync('unknown', { length: 123 }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /The argument 'type' must be a supported key type/ +}); diff --git a/test/parallel/test-crypto-sign-verify.js b/test/parallel/test-crypto-sign-verify.js index 2313dfef7f594f..8240e3685a42ee 100644 --- a/test/parallel/test-crypto-sign-verify.js +++ b/test/parallel/test-crypto-sign-verify.js @@ -391,26 +391,12 @@ assert.throws( }); [1, {}, [], Infinity].forEach((input) => { - let prop = '"key" argument'; - let value = input; - if (typeof input === 'object') { - prop = '"key.key" property'; - value = undefined; - } const errObj = { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: `The ${prop} must be of type string or ` + - 'an instance of Buffer, TypedArray, DataView, or KeyObject.' + - common.invalidArgTypeHelper(value) }; - assert.throws(() => sign.sign(input), errObj); assert.throws(() => verify.verify(input), errObj); - - errObj.message = 'The "signature" argument must be of type string or an ' + - 'instance of Buffer, TypedArray, or DataView.' + - common.invalidArgTypeHelper(input); assert.throws(() => verify.verify('test', input), errObj); }); } @@ -418,10 +404,10 @@ assert.throws( { assert.throws( () => crypto.createSign('sha8'), - /Unknown message digest/); + /Invalid digest/); assert.throws( () => crypto.sign('sha8', Buffer.alloc(1), keyPem), - /Unknown message digest/); + /Invalid digest/); } [ @@ -487,24 +473,11 @@ assert.throws( const errObj = { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "data" argument must be an instance of Buffer, ' + - 'TypedArray, or DataView.' + - common.invalidArgTypeHelper(input) }; assert.throws(() => crypto.sign(null, input, 'asdf'), errObj); assert.throws(() => crypto.verify(null, input, 'asdf', sig), errObj); - let prop = '"key" argument'; - let value = input; - if (typeof input === 'object') { - prop = '"key.key" property'; - value = undefined; - } - errObj.message = `The ${prop} must be of type string or ` + - 'an instance of Buffer, TypedArray, DataView, or KeyObject.' + - common.invalidArgTypeHelper(value); - assert.throws(() => crypto.sign(null, data, input), errObj); assert.throws(() => crypto.verify(null, data, input, sig), errObj); diff --git a/test/parallel/test-webcrypto-cryptokey-workers.js b/test/parallel/test-webcrypto-cryptokey-workers.js new file mode 100644 index 00000000000000..7f54481149dd59 --- /dev/null +++ b/test/parallel/test-webcrypto-cryptokey-workers.js @@ -0,0 +1,56 @@ +'use strict'; + +// This test ensures that CryptoKey instances can be correctly +// sent to a Worker via postMessage. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; +const { once } = require('events'); + +const { + Worker, + parentPort, +} = require('worker_threads'); + +const keyData = + Buffer.from( + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', 'hex'); + +const sig = '13691a79fb55a0417e4d6699a32f91ad29283fa2c1439865cc0632931f4f48dc'; + +async function doSig(key) { + const signature = await subtle.sign({ + name: 'HMAC' + }, key, 'some data'); + assert.strictEqual(Buffer.from(signature).toString('hex'), sig); +} + +if (process.env.HAS_STARTED_WORKER) { + return parentPort.once('message', (key) => { + assert.strictEqual(key.algorithm.name, 'HMAC'); + doSig(key).then(common.mustCall()); + }); +} + +// Don't use isMainThread to allow running this test inside a worker. +process.env.HAS_STARTED_WORKER = 1; + +(async function() { + const worker = new Worker(__filename); + + await once(worker, 'online'); + + const key = await subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-256' }, + true, ['sign', 'verify']); + + worker.postMessage(key); + + await doSig(key); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-derivebits-ecdh.js b/test/parallel/test-webcrypto-derivebits-ecdh.js new file mode 100644 index 00000000000000..e86217d0672ac2 --- /dev/null +++ b/test/parallel/test-webcrypto-derivebits-ecdh.js @@ -0,0 +1,266 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle, getRandomValues } = require('crypto').webcrypto; + +const kTests = [ + { + namedCurve: 'P-521', + size: 66, + pkcs8: '3081ee020100301006072a8648ce3d020106052b810400230481d63081d302010' + + '1044201a67ed321915a64aa359b7d648ddc2618fa8e8d1867e8f71830b10d25ed' + + '2891faf12f3c7e75421a2ea264f9a915320d274fe1470742b984e96b98912081f' + + 'acd478da18189038186000400209d483f28666881c6641f3a126f400f51e46511' + + '70fe678c75e85712e2868adc850824997bebf0bc82b43028a6d2ec1777ca45279' + + 'f7206a3ea8b5cd2073f493e45000cb54c3a5acaa268c56710428878d98b8afbf6' + + '8a612153632846d807e92672698f1b9c611de7d38e34cd6c73889092c56e52d68' + + '0f1dfd092b87ac8ef9ff3c8fb48', + spki: '30819b301006072a8648ce3d020106052b81040023038186000400ee69f94715d7' + + '01e9e2011333d4f4f96cba7d91f88b112baf75cf09cc1f8aca97618da9389822d2' + + '9b6fe9996a61203ef752b771e8958fc4677bb3778565ab60d6ed00deab6761895b' + + '935e3ad325fb8549e56f13786aa73f88a2ecfe40933473d8aef240c4dfd7d506f2' + + '2cdd0e55558f3fbf05ebf7efef7a72d78f46469b8448f26e2712', + result: '009c2bce57be80adab3b07385b8e5990eb7d6fdebdb01bf35371a4f6075e9d28' + + '8ac12a6dfe03aa5743bc81709d49a822940219b64b768acd520fa1368ea0af8d' + + '475d', + }, + { + namedCurve: 'P-384', + size: 48, + pkcs8: '3081b6020100301006072a8648ce3d020106052b8104002204819e30819b02010' + + '10430f871a5666589c14a5747263ef85b319cc023db6e35676c3d781eef8b055f' + + 'cfbe86fa0d06d056b5195fb1323af8de25b3a16403620004f11965df7dd4594d0' + + '419c5086482a3b826b9797f9be0bd0d109c9e1e9989c1b9a92b8f269f98e17ad1' + + '84ba73c1f79762af45af8141602642da271a6bb0ffeb0cb4478fcf707e661aa6d' + + '6cdf51549c88c3f130be9e8201f6f6a09f4185aaf95c4', + spki: '3076301006072a8648ce3d020106052b810400220362000491822dc2af59c18f5b' + + '67f80df61a2603c2a8f0b3c0af822d63c279701a824560404401dde9a56ee52757' + + 'ea8bc748d4c82b5337b48d7b65583a3d572438880036bac6730f42ca5278966bd5' + + 'f21e86e21d30c5a6d0463ec513dd509ffcdcaf1ff5', + result: 'e0bd6bce0aef8ca48838a6e2fcc57e67b9c5e8860c5f0be9dabec53e454e18a0' + + 'a174c48888a26488115b2dc9f1dfa52d', + }, +]; + +async function prepareKeys() { + const keys = {}; + await Promise.all( + kTests.map(async ({ namedCurve, size, pkcs8, spki, result }) => { + const [ + privateKey, + publicKey + ] = await Promise.all([ + subtle.importKey( + 'pkcs8', + Buffer.from(pkcs8, 'hex'), + { + name: 'ECDH', + namedCurve + }, + true, + ['deriveKey', 'deriveBits']), + subtle.importKey( + 'spki', + Buffer.from(spki, 'hex'), + { + name: 'ECDH', + namedCurve + }, + true, + ['deriveKey', 'deriveBits']) + ]); + keys[namedCurve] = { + privateKey, + publicKey, + size, + result, + }; + })); + return keys; +} + +(async function() { + const keys = await prepareKeys(); + + await Promise.all( + Object.keys(keys).map(async (namedCurve) => { + const { size, result, privateKey, publicKey } = keys[namedCurve]; + + { + // Good parameters + const bits = await subtle.deriveBits({ + name: 'ECDH', + public: publicKey + }, privateKey, 8 * size); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Case insensitivity + const bits = await subtle.deriveBits({ + name: 'eCdH', + public: publicKey + }, privateKey, 8 * size); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Null length + const bits = await subtle.deriveBits({ + name: 'ECDH', + public: publicKey + }, privateKey, null); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Short Result + const bits = await subtle.deriveBits({ + name: 'ECDH', + public: publicKey + }, privateKey, 8 * size - 32); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + result.slice(0, -8)); + } + + { + // Too long result + await assert.rejects(subtle.deriveBits({ + name: 'ECDH', + public: publicKey + }, privateKey, 8 * size + 8), { + message: /derived bit length is too small/ + }); + } + + { + // Non-multiple of 8 + const bits = await subtle.deriveBits({ + name: 'ECDH', + public: publicKey + }, privateKey, 8 * size - 11); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + result.slice(0, -4)); + } + })); + + // Error tests + { + // Missing public property + await assert.rejects( + subtle.deriveBits( + { name: 'ECDH' }, + keys['P-384'].privateKey, + 8 * keys['P-384'].size), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // The public property is not a CryptoKey + await assert.rejects( + subtle.deriveBits( + { + name: 'ECDH', + public: { message: 'Not a CryptoKey' } + }, + keys['P-384'].privateKey, + 8 * keys['P-384'].size), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // Mismatched named curves + await assert.rejects( + subtle.deriveBits( + { + name: 'ECDH', + public: keys['P-384'].publicKey + }, + keys['P-521'].privateKey, + 8 * keys['P-521'].size), + { message: /Named curve mismatch/ }); + } + + { + // Incorrect public key algorithm + const { publicKey } = await subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-521' + }, false, ['verify']); + + await assert.rejects(subtle.deriveBits({ + name: 'ECDH', + public: publicKey + }, keys['P-521'].privateKey, null), { + message: /Keys must be ECDH keys/ + }); + } + + { + // Private key does not have correct usages + const privateKey = await subtle.importKey( + 'pkcs8', + Buffer.from(kTests[0].pkcs8, 'hex'), + { + name: 'ECDH', + namedCurve: 'P-521' + }, false, ['deriveKey']); + + await assert.rejects(subtle.deriveBits({ + name: 'ECDH', + public: keys['P-521'].publicKey, + }, privateKey, null), { + message: /baseKey does not have deriveBits usage/ + }); + } + + { + // Base key is not a private key + await assert.rejects(subtle.deriveBits({ + name: 'ECDH', + public: keys['P-521'].publicKey + }, keys['P-521'].publicKey, null), { + message: /baseKey must be a private key/ + }); + } + + { + // Base key is not a private key + await assert.rejects(subtle.deriveBits({ + name: 'ECDH', + public: keys['P-521'].privateKey + }, keys['P-521'].publicKey, null), { + message: /algorithm\.public must be a public key/ + }); + } + + { + // Public is a secret key + const keyData = getRandomValues(new Uint8Array(32)); + const key = await subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC', length: 256 }, + false, ['encrypt']); + + await assert.rejects(subtle.deriveBits({ + name: 'ECDH', + public: key + }, keys['P-521'].publicKey, null), { + message: /algorithm\.public must be a public key/ + }); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js new file mode 100644 index 00000000000000..f1efbb562bb357 --- /dev/null +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -0,0 +1,543 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +function getDeriveKeyInfo(name, length, hash, ...usages) { + return [{ name, length, hash }, usages]; +} + +const kDerivedKeyTypes = [ + ['AES-CBC', 128, undefined, 'encrypt', 'decrypt'], + ['AES-CBC', 256, undefined, 'encrypt', 'decrypt'], + ['AES-CTR', 128, undefined, 'encrypt', 'decrypt'], + ['AES-CTR', 256, undefined, 'encrypt', 'decrypt'], + ['AES-GCM', 128, undefined, 'encrypt', 'decrypt'], + ['AES-GCM', 256, undefined, 'encrypt', 'decrypt'], + ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], + ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], + ['HMAC', 256, 'SHA-1', 'sign', 'verify'], + ['HMAC', 256, 'SHA-256', 'sign', 'verify'], + ['HMAC', 256, 'SHA-384', 'sign', 'verify'], + ['HMAC', 256, 'SHA-512', 'sign', 'verify'], +]; + +const kDerivedKeys = { + short: '5040737377307264', + long: '55736572732073686f756c64207069636b206c6f6e6720706173737068726' + + '173657320286e6f74207573652073686f72742070617373776f7264732921', + // empty: '' +}; + +const kSalts = { + normal: '536f6469756d2043686c6f7269646520636f6d706f756e64', + empty: '' +}; + +const kInfos = { + normal: '484b444620657874726120696e666f', + empty: '' +}; + +const kDerivations = { + short: { + normal: { + 'SHA-384': { + normal: '19ba74368e6b993390f27fe9a7d02bc3' + + '38173f72be71a19fc744fcdb3fd4b84b', + empty: '97601f4e0c53a5d3f3a2810099bc6820' + + 'ec50083434769b59fc24a417a9543734' + }, + 'SHA-512': { + normal: '4bbd6db2435fb696157f6089c977c3c7' + + '3f3eac5ef3dd6baae604cb53bfbb153e', + empty: '2f3157e7fe0c10b01298c8f0886a90ed' + + 'cf80abdef5dbc1df2b1482532b52b934' + }, + 'SHA-1': { + normal: '05ad22ed2138c9600e4d9e2725ded301' + + 'f5d287fbfb5702f999bc6536d3edef98', + empty: 'd51b6fb7e599ca30c5ee264593e4b85f' + + '2220c7c3ab003157bff8cb4f369c7560' + }, + 'SHA-256': { + normal: '2af5901e28849c28443857386aa1ac3b' + + 'b127e92631c1c051482d6690941772b4', + empty: '9e4b719033742101e90f1ad61e2ff3b4' + + '256863667296d74389f1f02af2c4e6a6' + } + }, + empty: { + 'SHA-384': { + normal: 'fb482ff22c4f8d466c4dfe6e29f2cc2e' + + 'cdabf5884328fbf08a738fd945f166cb', + empty: '1e023c17b340533ceaef39230cb8b3bb' + + 'dbf663a13d6075d0dd326c049478fba5' + }, + 'SHA-512': { + normal: 'f17b5bdcd8d7d3d4601036a19436317d' + + '1644f9a4e0956efc0e372b83acdacfdb', + empty: 'c7b474942f31f83faf5d14731802b1bd' + + '49478549cb3a8f3dbfedc4d3209cf5b6' + }, + 'SHA-1': { + normal: 'c126f1e6f25a9de42cf7d427059a52ed' + + '9601f29a5815cbfbc64bc7f668c6a341', + empty: '3215c3f08de70549b051b7033745a818' + + '4f8cbaa6b1735330d2bcb6b16f4642ef' + }, + 'SHA-256': { + normal: '733c8b6bcfac875c7f08982a6e3ffb56' + + '0acea6f165476eb83460b9353ed41dfe', + empty: 'c8e12774135305c9147f2cc4766e5ead' + + '25d8f457b9a1953d52677361ced558fb' + } + } + }, + long: { + normal: { + 'SHA-384': { + normal: 'f91571b521f7eef13e573aa46378659e' + + 'f3b7f36ffdd1bb055db2cd77d260c467', + empty: '68af1c2cf6b9370d2054344798bdbb18' + + '47ccf407b7652b793dd136d4640e0348' + }, + 'SHA-512': { + normal: '710aae2fdf889e45fe0fb995b2c26b33' + + 'eb988650ec0faef167028a7a6ccb3638', + empty: 'e5de568081c71e562750829871c34275' + + '8104765ed6f306f0613c9d4bb336f2aa' + }, + 'SHA-1': { + normal: '7f957edcbce3cb0b70566e1eb60efd1e' + + '405a13304c661d3663778109bf06899c', + empty: '3062f3cf1a730b9cef51f02c1dfac85e' + + 'd91e4b0065eb50ca9fd8b0107e728733' + }, + 'SHA-256': { + normal: '31b7d68530a863e717c081ca6917b686' + + '50b3dd9a29f30606e2cad199bec14d13', + empty: 'e579d1f9e7f08e6f990ffcfcce1ed201' + + 'c5e37e62cdf606f0ba4aca80427fbc44' + } + }, + empty: { + 'SHA-384': { + normal: '619eb6f9287395bbd5ed6a67c968465a' + + 'd82b6c559f3c38b604bbb08f58320b03', + empty: 'ff447b423d83fe76836c32337228b56b' + + '5bd9bf68d58e7dca4b7cca842a45e11a' + }, + 'SHA-512': { + normal: '133e8a7f7ff433690cc88432c2a338c2' + + '77e5c13756ff878f46753fe6a564e3e5', + empty: 'de54f7eec80c9cc66d349fc987f80d46' + + '1db2ef4ff4e18505d28bd80cb42c7d76' + }, + 'SHA-1': { + normal: 'adb93cdbce79b7d51159b6c0131a2b62' + + 'f23828d26acd685e34c06535e6f77496', + empty: '47710d2a7507e05a1ddcc87a7c2f9061' + + '77a266efb9e622510cccb3713cd08d58' + }, + 'SHA-256': { + normal: 'a401d7c9158a29e5c7193ab9730f0748' + + '851cc5baadb42cad024b6290fe213436', + empty: 'b4f7e7557674d501cbfbc0148ad800c0' + + '750189fe295a2aca5e1bf4122c85edf9' + } + } + }, +}; + +async function setupBaseKeys() { + const promises = []; + + const baseKeys = {}; + const noBits = {}; + const noKey = {}; + let wrongKey = null; + + Object.keys(kDerivedKeys).forEach((size) => { + const keyData = Buffer.from(kDerivedKeys[size], 'hex'); + promises.push( + subtle.importKey( + 'raw', + keyData, + { name: 'HKDF' }, + false, + ['deriveKey', 'deriveBits']) + .then( + (baseKey) => baseKeys[size] = baseKey, + (err) => assert.ifError(err))); + + promises.push( + subtle.importKey( + 'raw', + keyData, + { name: 'HKDF' }, + false, ['deriveBits']) + .then( + (baseKey) => noKey[size] = baseKey, + (err) => assert.ifError(err))); + + promises.push( + subtle.importKey( + 'raw', + keyData, + { name: 'HKDF' }, + false, ['deriveKey']) + .then( + (baseKey) => noBits[size] = baseKey, + (err) => assert.ifError(err))); + }); + + promises.push( + subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'P-521' + }, + false, + ['deriveKey', 'deriveBits']) + .then( + (baseKey) => wrongKey = baseKey.privateKey, + (err) => assert.ifError(err))); + + await Promise.all(promises); + + return { + baseKeys, + noBits, + noKey, + wrongKey + }; +} + +async function testDeriveBits( + baseKeys, + size, + saltSize, + hash, + infoSize) { + const algorithm = { + name: 'HKDF', + salt: Buffer.from(kSalts[saltSize], 'hex'), + info: Buffer.from(kInfos[infoSize], 'hex'), + hash + }; + + const bits = await subtle.deriveBits( + algorithm, + baseKeys[size], + 256); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + kDerivations[size][saltSize][hash][infoSize]); +} + +async function testDeriveBitsBadLengths( + baseKeys, + size, + saltSize, + hash, + infoSize) { + const algorithm = { + name: 'HKDF', + salt: Buffer.from(kSalts[saltSize], 'hex'), + info: Buffer.from(kInfos[infoSize], 'hex'), + hash + }; + + return Promise.all([ + assert.rejects( + subtle.deriveBits(algorithm, baseKeys[size], 0), { + message: /length cannot be zero/ + }), + assert.rejects( + subtle.deriveBits(algorithm, baseKeys[size], null), { + code: 'ERR_INVALID_ARG_TYPE' + }), + assert.rejects( + subtle.deriveBits(algorithm, baseKeys[size], 15), { + message: /length must be a multiple of 8/ + }), + ]); +} + +async function testDeriveBitsBadHash( + baseKeys, + size, + saltSize, + hash, + infoSize) { + const salt = Buffer.from(kSalts[saltSize], 'hex'); + const info = Buffer.from(kInfos[infoSize], 'hex'); + const algorithm = { name: 'HKDF', salt, info }; + + return Promise.all([ + assert.rejects( + subtle.deriveBits( + { + ...algorithm, + hash: hash.substring(0, 3) + hash.substring(4) + }, baseKeys[size], 256), { + message: /Unrecognized name/ + }), + assert.rejects( + subtle.deriveBits( + { + ...algorithm, + hash: 'PBKDF2' + }, + baseKeys[size], 256), { + message: /Unrecognized name/ + }) + ]); +} + +async function testDeriveBitsBadUsage( + noBits, + size, + saltSize, + hash, + infoSize) { + const algorithm = { + name: 'HKDF', + salt: Buffer.from(kSalts[saltSize], 'hex'), + info: Buffer.from(kInfos[infoSize], 'hex'), + hash + }; + + return assert.rejects( + subtle.deriveBits(algorithm, noBits[size], 256), { + message: /baseKey does not have deriveBits usage/ + }); +} + +async function testDeriveBitsMissingSalt( + baseKeys, + size, + saltSize, + hash, + infoSize) { + const algorithm = { + name: 'HKDF', + info: Buffer.from(kInfos[infoSize], 'hex'), + hash + }; + + return assert.rejects( + subtle.deriveBits(algorithm, baseKeys[size], 0), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +async function testDeriveBitsMissingInfo( + baseKeys, + size, + saltSize, + hash, + infoSize) { + const algorithm = { + name: 'HKDF', + salt: Buffer.from(kSalts[infoSize], 'hex'), + hash + }; + + return assert.rejects( + subtle.deriveBits(algorithm, baseKeys[size], 0), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +async function testBitsWrongKeyType( + wrongKey, + saltSize, + hash, + infoSize) { + const algorithm = { + name: 'HKDF', + salt: Buffer.from(kSalts[saltSize], 'hex'), + info: Buffer.from(kInfos[infoSize], 'hex'), + hash + }; + + return assert.rejects( + subtle.deriveBits(algorithm, wrongKey, 256), { + message: /Key algorithm mismatch/ + }); +} + +async function testDeriveKey( + baseKeys, + size, + saltSize, + hash, + infoSize, + keyType, + usages) { + const algorithm = { + name: 'HKDF', + salt: Buffer.from(kSalts[saltSize], 'hex'), + info: Buffer.from(kInfos[infoSize], 'hex'), + hash + }; + + const key = await subtle.deriveKey( + algorithm, + baseKeys[size], + keyType, + true, + usages); + + const bits = await subtle.exportKey('raw', key); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + kDerivations[size][saltSize][hash][infoSize].slice(0, keyType.length / 4)); +} + +async function testDeriveKeyBadHash( + baseKeys, + size, + saltSize, + hash, + infoSize, + keyType, + usages) { + const salt = Buffer.from(kSalts[saltSize], 'hex'); + const info = Buffer.from(kInfos[infoSize], 'hex'); + const algorithm = { name: 'HKDF', salt, info }; + + return Promise.all([ + assert.rejects( + subtle.deriveKey( + { + ...algorithm, + hash: hash.substring(0, 3) + hash.substring(4) + }, + baseKeys[size], + keyType, + true, + usages), + { message: /Unrecognized name/ }), + assert.rejects( + subtle.deriveKey( + { + ...algorithm, + hash: 'PBKDF2' + }, + baseKeys[size], + keyType, + true, + usages), + { message: /Unrecognized name/ }) + ]); +} + +async function testDeriveKeyBadUsage( + noKey, + size, + saltSize, + hash, + infoSize, + keyType, + usages) { + const algorithm = { + name: 'HKDF', + salt: Buffer.from(kSalts[saltSize], 'hex'), + info: Buffer.from(kInfos[infoSize], 'hex'), + hash + }; + + return assert.rejects( + subtle.deriveKey(algorithm, noKey[size], keyType, true, usages), { + message: /baseKey does not have deriveKey usage/ + }); +} + +async function testWrongKeyType( + wrongKey, + saltSize, + hash, + infoSize, + keyType, + usages +) { + const algorithm = { + name: 'HKDF', + salt: Buffer.from(kSalts[saltSize], 'hex'), + info: Buffer.from(kInfos[infoSize], 'hex'), + hash + }; + + return assert.rejects( + subtle.deriveKey(algorithm, wrongKey, keyType, true, usages), { + message: /Key algorithm mismatch/ + }); +} + +(async function() { + const { + baseKeys, + noBits, + noKey, + wrongKey, + } = await setupBaseKeys(); + + const variations = []; + + Object.keys(kDerivations).forEach((size) => { + Object.keys(kDerivations[size]).forEach((saltSize) => { + Object.keys(kDerivations[size][saltSize]).forEach((hash) => { + Object.keys(kDerivations[size][saltSize][hash]).forEach( + async (infoSize) => { + const args = [baseKeys, size, saltSize, hash, infoSize]; + variations.push(testDeriveBits(...args)); + variations.push(testDeriveBitsBadLengths(...args)); + variations.push(testDeriveBitsMissingSalt(...args)); + variations.push(testDeriveBitsMissingInfo(...args)); + variations.push(testDeriveBitsBadHash(...args)); + variations.push( + testDeriveBitsBadUsage( + noBits, + size, + saltSize, + hash, + infoSize)); + variations.push( + testBitsWrongKeyType( + wrongKey, + saltSize, + hash, + infoSize)); + + kDerivedKeyTypes.forEach((keyType) => { + const keyArgs = getDeriveKeyInfo(...keyType); + variations.push(testDeriveKey(...args, ...keyArgs)); + variations.push(testDeriveKeyBadHash(...args, ...keyArgs)); + variations.push(testDeriveKeyBadUsage( + noKey, size, saltSize, hash, infoSize, ...keyArgs)); + variations.push( + testWrongKeyType( + wrongKey, + saltSize, + hash, + infoSize, + ...keyArgs)); + }); + }); + }); + }); + }); + + await Promise.all(variations); + +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-derivebits-node-dh.js b/test/parallel/test-webcrypto-derivebits-node-dh.js new file mode 100644 index 00000000000000..5140c218f09ffc --- /dev/null +++ b/test/parallel/test-webcrypto-derivebits-node-dh.js @@ -0,0 +1,218 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const kTestData = { + pkcs8: '308203260201003082019706092a864886f70d010301308201880282018100ff' + + 'ffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc7402' + + '0bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374f' + + 'e1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee' + + '386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598' + + 'da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9e' + + 'd529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be3' + + '9e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf69558171839' + + '95497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a8' + + '5521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7ab' + + 'f5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d8' + + '7602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208' + + 'e24fa074e5ab3143db5bfce0fd108e4b82d120a93ad2caffffffffffffffff02' + + '010204820184028201806ba21bec44fe29bee2d1f6f5d717b9af1c62973b342c' + + '28b850d2e39b31abf3ce7f58a26da0cdf538b3328648ee8738e49434bdf697ff' + + '2ac5da64d308fa0abbd75f0554bcef58fd1a688bf93af00ffbdb9ba05558a8e6' + + '16db9818769e2c7e37ba0c07bca10fffd19cb7aa44876170f0a8fe0c1bdba2f9' + + 'b0606d479796e1b6bdab153188b03f684584fd909814c5055fc184eaf05577f7' + + 'f447fcc2dd90889ab853830993d8fbf087f942d8e7a9f331570e7eee5954fb7a' + + 'a6920f4df2f8a33d5cb59961162b1216a4382f984cce02512f0100e20f15e480' + + 'a9d19bb01414a75c74d0854595e58ed060f7bb0f4b451f82b476dc039cc1bb8c' + + '1a05999e6abb20915a3cfca40f314538c00f42d0c4f2070cb163788d6dea0adb' + + '03c75d8001c7057ef61e60b407272adffc2669b82e634ebebda45826229bb2f8' + + 'ed742ee97429e34de06c41c25563025285d5f8a43acdc57cf12779222ae125d5' + + '438857d6bf6341815b9aebf6878fd23944cfd240e74caea13419163fde5ec1ec' + + 'fe2fb2e740b9301a9c04', + spki: '308203253082019706092a864886f70d010301308201880282018100fffffffff' + + 'fffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b' + + '139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d5' + + '1c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a89' + + '9fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d' + + '39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed52907709696' + + '6d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e860' + + '39b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae5' + + '15d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64e' + + 'cfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e' + + '8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521' + + 'f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db' + + '5bfce0fd108e4b82d120a93ad2caffffffffffffffff020102038201860002820' + + '1810098b01807f77a722cd30fe0390f8cdd0b3f7e0fb33fd3e0ca3d573a7b481e' + + 'dc0fc9f7bc58bfc4509f34efe118ba48bcefbbd57c4878e87b4d0fbb4b661d771' + + '19055cf190b5e8ad05c4821ff0445f4e12049e708ee83e2272c2c269b8b3bfa1b' + + '37df8dc4ec77a430ed6b89beb69093ceeca48497d8ab3ceac25429df58e96d2df' + + 'd01a94b1d164697b93475ed3d5962fd7c300de959474568aecc15c880c36b14a7' + + '9c2144ad4207c9760c11fbcc9ccbb29727a7f5a7789d47f1fa7a154289f83c528' + + '1fb97e01c37f937a25b4057017961ebd52b936f6b8f6d9ad35e3e094c3850cde2' + + '9e98895eaa9b56bff17dc6d8c03edce51c0aa009864da9cbe49a2063bddc64f11' + + '67beb0ab1c5d9bccf9a10078d0236237986f53c4074f30399deb829c544026fe0' + + '532e54e1bc9d933bd4db8dbfd43cf7839a9f64c135d2188896294d54cb7812e6e' + + '9586a03e91893edf7240011e8cda8f59a4dde0ce6d3c40d85847a3985fa25967f' + + 'a2c9598b269ac28da067704f15eb5bb4d8afdea8c67f56f9b604e172bc', + result: '42e77c77568f4387858771b006bebf213a2894efed8efdcfe23640bbd6df765' + + '1b2afa9f5acfba5b1d3db5401fd3427cd9226f6c2fdbd2b8fd7948e12cc7e016' + + 'eb2ed0e0fb02ce434c68c5d511ded05c1150718cf4c8b9db0adcc639a8b52b74' + + 'c2c90e2df5f6f55462d38e2d2769b4bd23bf0db8b4a82253addc1d5d6d19289d' + + '9e60ddabe1203aa3f45956f4dd32683f55fc2f7400e92712e5bb4dea737e03ad' + + '4b94640ccd63a0b8485dfa63f3b98232a34354aa91aebb3e86bb67a48b50ac21' + + 'e62dbce80c0913a9dba92c1c2333f4a43efe9c6c24ce23c252c7ff37e9e74a78' + + '6a8c5b82453fda8b6b40463461d22765e26d4f444272254ee6a492d3e6737e52' + + 'a94440aeddde3c0b449c52ca64bdc698f683ba6cb27794b84edcab7694cadfe8' + + 'f76ce94bf127ca42323b1995a863bcecab455f5d6283eb0eed44942aad3d1a46' + + '79713ed4757917f95d1a61ecae45b9978a863b16e199418bfe6e98435ad146a4' + + '7bd9c8cad9f787a29888954ae58fa683ea163921eb9d3e7a5aa2f52f74ec15333', +}; + +async function prepareKeys() { + const [ + privateKey, + publicKey + ] = await Promise.all([ + subtle.importKey( + 'pkcs8', + Buffer.from(kTestData.pkcs8, 'hex'), + { name: 'NODE-DH' }, + true, + ['deriveKey', 'deriveBits']), + subtle.importKey( + 'spki', + Buffer.from(kTestData.spki, 'hex'), + { name: 'NODE-DH' }, + true, []) + ]); + return { + privateKey, + publicKey, + result: kTestData.result, + }; +} + +(async function() { + const { + publicKey, + privateKey, + result + } = await prepareKeys(); + + { + // Good parameters + const bits = await subtle.deriveBits({ + name: 'NODE-DH', + public: publicKey + }, privateKey, null); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Case insensitivity + const bits = await subtle.deriveBits({ + name: 'node-dH', + public: publicKey + }, privateKey, null); + + assert.strictEqual(Buffer.from(bits).toString('hex'), result); + } + + { + // Short Result + const bits = await subtle.deriveBits({ + name: 'NODE-DH', + public: publicKey + }, privateKey, 16); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + result.slice(0, 4)); + } + + { + // Too long result + await assert.rejects(subtle.deriveBits({ + name: 'NODE-DH', + public: publicKey + }, privateKey, result.length * 16), { + message: /derived bit length is too small/ + }); + } + + { + // Non-multiple of 8 + const bits = await subtle.deriveBits({ + name: 'NODE-DH', + public: publicKey + }, privateKey, 15); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + result.slice(0, 2)); + } + + // Error tests + { + // Missing public property + await assert.rejects( + subtle.deriveBits( + { name: 'NODE-DH' }, + privateKey, + null), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // The public property is not a CryptoKey + await assert.rejects( + subtle.deriveBits( + { + name: 'NODE-DH', + public: { message: 'Not a CryptoKey' } + }, + privateKey, + null), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // Incorrect public key algorithm + const { publicKey } = await subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-521' + }, false, ['verify']); + + await assert.rejects(subtle.deriveBits({ + name: 'NODE-DH', + public: publicKey + }, privateKey, null), { + message: /Keys must be DH keys/ + }); + } + + { + // Private key does not have correct usages + const privateKey = await subtle.importKey( + 'pkcs8', + Buffer.from(kTestData.pkcs8, 'hex'), + { + name: 'NODE-DH', + }, false, ['deriveKey']); + + await assert.rejects(subtle.deriveBits({ + name: 'NODE-DH', + public: publicKey, + }, privateKey, null), { + message: /baseKey does not have deriveBits usage/ + }); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-derivebits-pbkdf2.js b/test/parallel/test-webcrypto-derivebits-pbkdf2.js new file mode 100644 index 00000000000000..e14cad287badaf --- /dev/null +++ b/test/parallel/test-webcrypto-derivebits-pbkdf2.js @@ -0,0 +1,670 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +function getDeriveKeyInfo(name, length, hash, ...usages) { + return [{ name, length, hash }, usages]; +} + +const kDerivedKeyTypes = [ + ['AES-CBC', 128, undefined, 'encrypt', 'decrypt'], + ['AES-CBC', 256, undefined, 'encrypt', 'decrypt'], + ['AES-CTR', 128, undefined, 'encrypt', 'decrypt'], + ['AES-CTR', 256, undefined, 'encrypt', 'decrypt'], + ['AES-GCM', 128, undefined, 'encrypt', 'decrypt'], + ['AES-GCM', 256, undefined, 'encrypt', 'decrypt'], + ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], + ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], + ['HMAC', 256, 'SHA-1', 'sign', 'verify'], + ['HMAC', 256, 'SHA-256', 'sign', 'verify'], + ['HMAC', 256, 'SHA-384', 'sign', 'verify'], + ['HMAC', 256, 'SHA-512', 'sign', 'verify'], +]; + +const kPasswords = { + short: '5040737377307264', + long: '55736572732073686f756c64207069636b206c6f6' + + 'e6720706173737068726173657320286e6f742075' + + '73652073686f72742070617373776f7264732921', + // TODO(@jasnell): Zero-length password not currently supported + // empty: '' +}; + +const kSalts = { + short: '4e61436c', + long: '536f6469756d2043686c6f7269646520636f6d706f756e64', + empty: '' +}; + +const kDerivations = { + short: { + short: { + 'SHA-384': { + '1': '80cd0f15364366a72551c37975f7b637ba89' + + 'c29b4639ec720f69a70dbbed515c', + '1000': 'aaec5a976d4d35cb2024486fc9f9bb9aa' + + '3eae7cef2bce62664b5b3751cf50ff1', + '100000': '6f5ea3c6c6f5e4833467b47c3a671e6' + + '5714e87071bd1e36d716f846b5cd28980' + }, + 'SHA-512': { + '1': '69f4d5cef5c7d8ba938e880388c8f63b6b2' + + '448b2626d1343fc5cb68bbd7f27b2', + '1000': '865c5945e11f5bf3ddf002e7cb1748f6' + + '224d2671e806dad4aaf090a04367da29', + '100000': '483ba7f2e2fe382cf61d20b29812e2' + + 'd49610a60041ae40ecf9fc7ef138e93876' + }, + 'SHA-1': { + '1': '4624dbd21373ee5659c125b184eedaa26a3' + + '3b77ca11314b9f0c9dae1e44e9b04', + '1000': '5388ea5e62e1b557981abe5ce4132127' + + '58aa6a9d2c5bf08c019d459dba666b90', + '100000': 'f58f435fbc5c05865c914fd972108a' + + '09457d5f9a48f14e75e4cc02d98983038a' + }, + 'SHA-256': { + '1': 'c6bc55a404adcea36a1ab56798085e0aaf6' + + '97f6bb2c16a5072f838f17dfe6cb6', + '1000': '4e6ca57957439be3a75370424225e221' + + '1d55f05af00561df3f3efee9116bc34c', + '100000': 'ab25796598e74b29c324f5ba4d90ea' + + '7dc89fc6891041b4d56c94156505f722c0' + } + }, + long: { + 'SHA-384': { + '1': '6807346cc53eded1cb964a72628589a6bd4' + + '8355990bfdfe7465109710207059d', + '1000': 'a310ef3c6b3a95e6d8ca6644e3dcfd88' + + '222a59fe8e00c52d6a12631d82c1d24b', + '100000': '2c8c6674c879cf1850bc9b7fbdcc6e' + + 'a7abb0a1522196a866875305dea57486f3' + }, + 'SHA-512': { + '1': '5777027aff4051fb9b43c1f1ef0463bd677' + + '51175d428a13da3da845a591332cd', + '1000': '9c17fe96895eadbfd1cc095fc1bb834f' + + '28e5ccc9ec96ca814cff941a4bf40727', + '100000': 'b479c9715c421638dce0a705fc0b7b' + + 'a7d56fa3063188063580e070dff1db497c' + }, + 'SHA-1': { + '1': '576f7c165825bef9ef14b4bc2c82469d1e4' + + '08ff8e7ba306694797f9e45b766ed', + '1000': '89d3b27b5f6e8af015f2f87cf368a143' + + '8a206c4ecf5fe681fc3bf94c56213ef6', + '100000': '1e39e8bf6676fcd3156655457afa14' + + 'bee771dbcbfcd07241c7cee209a7cb1fe9' + }, + 'SHA-256': { + '1': '12b90f594f0908cf912d655c948f9c2a1ea' + + 'b855765bc12785ef18aa02b8e7edc', + '1000': 'b1a7b7dc20df174a4a0e410dbfaf03b4' + + 'c375c450a89d7a9ed349b4e52e64dfd8', + '100000': 'd4594d8a1b59520a48878922a65d66' + + '3d28f6a5fa49e931d300d8f9baf93d0aeb' + } + }, + empty: { + 'SHA-384': { + '1': '4f1089c01e438bde649a379fa418fbc3b85' + + '6258772dfe911806f9bd0809fbc7e', + '1000': 'aeb5f97d6627eebcde6b139a00895500' + + '30f7401c67e01c057a3338175e3f3a17', + '100000': 'd7687df6c781dc88d64ef9cbaf95d3' + + 'd5d1155f66b230239e6e81c1550c8840cf' + }, + 'SHA-512': { + '1': '8f7b7d459c752f64bf12be625b65d496ac2' + + '4ea36516b168e16fb026845b4e82e', + '1000': 'b5ac720b7abe0832fc51a31b1ec5673b' + + 'eb1e41840adfd3d606e8638f4006eb48', + '100000': 'ba1a0f36bad771526564051eb9ca20' + + '7da19b62e53762349976a9a3d1b0ef7e20' + }, + 'SHA-1': { + '1': 'c0cffb0ce5db351faa24dad5902583cfc30' + + 'a9f54d9aa6991fe821d03122127e9', + '1000': '736f3c3d6ebcc2a7b970403e2696c0eb' + + '4cd1770f55f196fc7089e666c11f77da', + '100000': '1c5095ac9a7bd410ef0f72c993eca9' + + '1bb0e571e9b2fbab704f8c131191fad16c' + }, + 'SHA-256': { + '1': '019e54ab42f00485d3aa1b26fcde21ae5f5' + + '2cb0f0960ffc9767f25c65e2db2f9', + '1000': 'b9d2f2217b4ee5a8bf0345f36b2c9887' + + '33f503a975dfeac7b7135f54a5f29971', + '100000': 'a7a2869829797807b3e576c17878b4' + + '66449e89e60447d541775a96eb7c1a5ded' + } + } + }, + long: { + short: { + 'SHA-384': { + '1': '5ede8836fdabeec5d3733b434abac443d41' + + '5193b599e0926193b000f406a5a7d', + '1000': 'faa442fbabf4058cc65368b53d7ec511' + + '3c09ea7e5e3743312f4bebedd980ba37', + '100000': 'f62ae6c7871b181aa71232f5eb8837' + + '2498ef32ac0a7d715119e8f052eb102d29' + }, + 'SHA-512': { + '1': '3e9c12b3f6dfb6441594ec7063fca962ffd' + + 'a10b6cf30b898a31ef9f1306b1119', + '1000': 'f0928f50a155f26a8c9c1bc7f3b5cb53' + + '1c53a8f51040c9ce5fc79d430ff0c0f4', + '100000': '974acfbb0f0f20c81ec92829f38c3d' + + 'af086a7df58b912b856d1f5ecc9355ef1b' + }, + 'SHA-1': { + '1': '8ae72f94e6fcd54fcbfca66200a211a51b2' + + 'f846787d20b6808bedf156ce46ca0', + '1000': '53b42161134e15c871abd71aba1390d0' + + '1f4c6a940caaf5c179258d8f1b1d680b', + '100000': 'a7fda4c79dd3ba1a875f65e9248b21' + + '0899ca0814ae38995d8ce5a53560cbac31' + }, + 'SHA-256': { + '1': 'ffa1e9a727a92c27ae6f74b1c7978f9e1af' + + '860e106376340ac43d969d136405b', + '1000': 'eeeb7714420a00b18acec2b5979d1da6' + + '1373202b7f8ba71b086293aab859e0a0', + '100000': 'deac70cbe3f1720e353b4e8016ddb5' + + '9475efb70b6a2385e735d2d6ea6d624a4d' + } + }, + long: { + 'SHA-384': { + '1': 'cf55422cef6e1bc49e6d082b2273d480e8f' + + '2e8822dadd1469c2a32d9657d12f1', + '1000': '35658551f0ec13398a7b45e0261cfd65' + + '4c1e52411e6e457dee68f4aeabe925a7', + '100000': '1abab5f1e461df378b88c0a22be76e' + + 'f2f1627df74ac7cbfb84bdccb354bc8889' + }, + 'SHA-512': { + '1': 'de4afbc0add3e4d32f4bc6e122a88ae44a2' + + 'b3ccf0148e7762bac05c43e94ef7f', + '1000': '43e12024c4d354727f7e58842ccb6033' + + 'a161d60dc5ae516f076e4a58a1880d38', + '100000': 'f9a92384a4eadfc3560649b37fb676' + + 'e83c453cbbd99f80bba6f0a10ebd150b52' + }, + 'SHA-1': { + '1': '1d104ea5d235006a12a80f71b80ee528048' + + 'b64cc1a7a0f30f7df4ba26b8320c7', + '1000': '6e90c86ee07b873e965071025673ff05' + + '429f678c30f91b37e1e2da512036d320', + '100000': '141030763bf983c8564d5d4c935fe3' + + 'ca3549608159ac1934c1599040668c2363' + }, + 'SHA-256': { + '1': 'fd5caeb8b3abe589bc159c4e51f800570e7' + + '4f64397a6c5ee131dfed93f0511aa', + '1000': '3fd587c94ba946b8b9dccddd2a5b74f6' + + '778d4f61e691f83ac47a2fa9580bfdf8', + '100000': '11992d8b813311244c544b62292945' + + 'e208d403cebd6b9552a1a562065d9958ea' + } + }, + empty: { + 'SHA-384': { + '1': '49ab3f9f882fdb9e528b4d9f1b3e8c71d26' + + '39abf1701d56eb99bd51201e420ff', + '1000': 'f9ca148b0c041890bff8831db6174719' + + '7e94ce68f190edf269694b4d644861ca', + '100000': '1749dfcd77e5258519ea2231ba2cd6' + + '543b073339ac9b1545bb643153faf6d17b' + }, + 'SHA-512': { + '1': 'd1bfa1a6b8a977839f8c3f9d52dd02104e2' + + '029c0eb2a6208cc408816e7768a8c', + '1000': '457a7955ebecec71a51efb6237e5b1d6' + + '2f4deab5c93d7b3d11d1e70faffa417e', + '100000': 'e805ac9cc1d8412c42446d237d1b50' + + '4f9540b362bd1b75e451531e853e24753d' + }, + 'SHA-1': { + '1': 'a46a62986d9c3909f41014dd72cfe34a261' + + '247854d7312cf4fbead60b9b69edd', + '1000': 'e7375de5036766c40cb85f43b53fce4f' + + 'fa402ab6be3571007ef5d55453fd7f0a', + '100000': '7a403d9a13aed8164e9c072c545462' + + '251fd942f1736a6bf03ce1c88330048e04' + }, + 'SHA-256': { + '1': '4f510c5181ac5c2c5fd4bd141f9712495be' + + 'ca279624742b4d6d30d08b96c0a69', + '1000': '7e66c84bea888f92c348d9145585186c' + + 'ae472b12fba7f0ad28179575c1aa815a', + '100000': '5f1a6ac4a56d9796a7309a78daaaf9' + + '18badaf5ed1eecc3f0b8a3a44c3d38d654' + } + } + }, + // empty: { + // short: { + // 'SHA-384': { + // '1': 'e9f0da1e97dfa455f858ce6b9af1ecc0299' + + // 'f125ff1a847eb5d4955866f43e604', + // '1000': '7ff7954aeddf41795fc8300666786d49' + + // '74269aa91cc7e93811c953331d56d609', + // '100000': '1c73132b6a55e9d9de2cdbfe1f55bf' + + // '0ab59fd91f78f109c50096038b8557b147' + // }, + // 'SHA-512': { + // '1': 'e7e2b41f4887421bcb764eb4a56f63d2502' + + // 'e33c764fbdf60626ad42ed9672342', + // '1000': 'd561c4c84e9c60ba4752a2d383bf55ef' + + // 'f643fc9e452252d6821e39449350cf72', + // '100000': 'efd00752bc9ffafb5a399dd1d5834e' + + // '8d2c2b676ecd4b2063fb1fe581d0f1380b' + // }, + // 'SHA-1': { + // '1': 'a667da47b8f857b7c65f70a6c8e7a06ce0d' + + // '25211a2b6ebaf58dcaaf268b46b1d', + // '1000': '72c92bbd3ddab4789e88e42ad1cda83c' + + // 'c0729e6cb5106a577e50d5cf61782481', + // '100000': '06e19e1b83e6480b1554df2b31a2c9' + + // '2d1bfcf9bc1bdbc8751ff8685bdeef7dc9' + // }, + // 'SHA-256': { + // '1': '2ddb49243eb3b5912cb260cdd87fb04ef0d' + + // '111bfa44d40a45e02a8a5c3c1518d', + // '1000': '2835f3ed53565420c90951509b0c1173' + + // 'b645174f1546ab3ac3e6c85cb471b53b', + // '100000': '80aed905ca32ae0bb2a9d8f532f048' + + // 'a0e672463eef9f83dfa7d88bca726553ea' + // } + // }, + // long: { + // 'SHA-384': { + // '1': '7b0bcca81dd637a3b3398666619716c5f2b1' + + // 'f4a5c24e85c18a9955559e4d7692', + // '1000': '8bb89cf71972fe5acc16fdc5f8cffd2c2' + + // 'e7178c086b3bbe61cc1314619135958', + // '100000': '26c6a8ae4bd1fbe715ae478efff3eca' + + // 'e83afa617ed35bd4a3f63c3da76a42d22' + // }, + // 'SHA-512': { + // '1': 'bb73f8168a8f391d3d54ca892fb72b8e603' + + // '5e37f891e5a70491b94dc05510bc4', + // '1000': '5cacc16cdfbe052cfd73a9891b8c0e78' + + // 'b19b2e07eae2423d48fed5e08aa8494b', + // '100000': '87fdfc293392cbf33ecc9b5141a2fe' + + // 'fa74d150499756863c484c0a78b6274d7f' + // }, + // 'SHA-1': { + // '1': '1f46b40cf2fb3dc41a3d9ced8897b861050' + + // '36810e2bfac7040814bd65d428d67', + // '1000': 'cc5748ecc41288a0e13368543aaa2ef6' + + // '2c97ba7518fa88f6e11c35763fc930b4', + // '100000': '33e2993bf4729dc993fff66e69cc55' + + // '777135ebfabce533575bce4a96645a742c' + // }, + // 'SHA-256': { + // '1': '61c935c462c3321c89663545d13a4f6b52b' + + // '5191cfb7479e58dcfe6444d43106c', + // '1000': '1353f7458237ab332ee052e29f829a2a' + + // 'b90e72630ea10493b4eecffb9ff89e1d', + // '100000': '79baf80ec582920538801e9d929ce0' + + // '7084277987488d733a026852c452f06fb4' + // } + // }, + // empty: { + // 'SHA-384': { + // '1': '4bb042a5c28cee6f66f991c717fd7702677' + + // '87e2bb3031eae270d87d63ad99534', + // '1000': '9cbfe72d194da34e17c821dd1569ef50' + + // 'a86eb4d893591776adc6a5c21e0031cf', + // '100000': 'ed6bd7282567abe48d542d067d09f4' + + // '04bd044ae2cefe11dacc531c4764cd35cd' + // }, + // 'SHA-512': { + // '1': '6d2ecbbbfb2e6dcd7056faf9af6aa06eae5' + + // '94391db983279a6bf27e0eb228614', + // '1000': 'cb93096c3a02beeb1c5fac36765c9011' + + // 'fe99f8d8ea62366048fc98cb98dfea8f', + // '100000': '89e16254ebad5cba72e0aebe1614c7' + + // 'f9b795a7505f2637206ce10a3449a2b8bb' + // }, + // 'SHA-1': { + // '1': '1e437a1c79d75be61e91141dae20affc489' + + // '2cc99abcc3fe753887bccc8920176', + // '1000': '6e40910ac02ec89cebb9d898b13a09d1' + + // 'cd7adf6f8cc08cc473302c8973aa2e19', + // '100000': 'a9e1bebb36bc26d7c997d5483cbc8d' + + // 'e4a419d1e706571342632586ec330a7290' + // }, + // 'SHA-256': { + // '1': 'f7ce0b653d2d72a4108cf5abe912ffdd777' + + // '616dbbb27a70e8204f3ae2d0f6fad', + // '1000': '4fc58a21c100ce1835b8f9991d738b56' + + // '965d14b24e1761fbdffc69ac5e0b667a', + // '100000': '64a868d4b23af696d3734d0b814d04' + + // 'cdd1ac280128e97653a05f32b49c13a29a' + // } + // } + // } +}; + +async function setupBaseKeys() { + const promises = []; + + const baseKeys = {}; + const noBits = {}; + const noKey = {}; + let wrongKey = null; + + Object.keys(kPasswords).forEach((size) => { + promises.push( + subtle.importKey( + 'raw', + Buffer.from(kPasswords[size], 'hex'), + { name: 'PBKDF2' }, + false, + ['deriveKey', 'deriveBits']) + .then((key) => baseKeys[size] = key)); + + promises.push( + subtle.importKey( + 'raw', + kPasswords[size], + { name: 'PBKDF2' }, + false, + ['deriveBits']) + .then((key) => noKey[size] = key)); + + promises.push( + subtle.importKey( + 'raw', + kPasswords[size], + { name: 'PBKDF2' }, + false, + ['deriveKey']) + .then((key) => noBits[size] = key)); + }); + + promises.push( + subtle.generateKey( + { name: 'ECDH', namedCurve: 'P-521' }, + false, + ['deriveKey', 'deriveBits']) + .then((key) => wrongKey = key.privateKey)); + + await Promise.all(promises); + + return { baseKeys, noBits, noKey, wrongKey }; +} + +async function testDeriveBits( + baseKeys, + size, + saltSize, + hash, + iterations) { + const algorithm = { + name: 'PBKDF2', + salt: Buffer.from(kSalts[saltSize], 'hex'), + hash, + iterations + }; + + const bits = await subtle.deriveBits(algorithm, baseKeys[size], 256); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + kDerivations[size][saltSize][hash][iterations]); +} + +async function testDeriveBitsBadLengths( + baseKeys, + size, + saltSize, + hash, + iterations) { + const algorithm = { + name: 'PBKDF2', + salt: Buffer.from(kSalts[saltSize], 'hex'), + iterations, + hash + }; + + return Promise.all([ + assert.rejects( + subtle.deriveBits(algorithm, baseKeys[size], 0), { + message: /length cannot be zero/ + }), + assert.rejects( + subtle.deriveBits(algorithm, baseKeys[size], null), { + code: 'ERR_INVALID_ARG_TYPE' + }), + assert.rejects( + subtle.deriveBits(algorithm, baseKeys[size], 15), { + message: /length must be a multiple of 8/ + }), + ]); +} + +async function testDeriveBitsBadHash( + baseKeys, + size, + saltSize, + hash, + iterations) { + const salt = Buffer.from(kSalts[saltSize], 'hex'); + const algorithm = { name: 'HKDF', salt, iterations }; + + return Promise.all([ + assert.rejects( + subtle.deriveBits( + { + ...algorithm, + hash: hash.substring(0, 3) + hash.substring(4) + }, baseKeys[size], 256), { + message: /Unrecognized name/ + }), + assert.rejects( + subtle.deriveBits( + { + ...algorithm, + hash: 'HKDF' + }, + baseKeys[size], 256), { + message: /Unrecognized name/ + }) + ]); +} + +async function testDeriveBitsBadUsage( + noBits, + size, + saltSize, + hash, + iterations) { + const algorithm = { + name: 'PBKDF2', + salt: Buffer.from(kSalts[saltSize], 'hex'), + iterations, + hash + }; + + return assert.rejects( + subtle.deriveBits(algorithm, noBits[size], 256), { + message: /baseKey does not have deriveBits usage/ + }); +} + + +async function testDeriveKey( + baseKeys, + size, + saltSize, + hash, + iterations, + keyType, + usages) { + const algorithm = { + name: 'PBKDF2', + salt: Buffer.from(kSalts[saltSize], 'hex'), + hash, + iterations + }; + + const key = await subtle.deriveKey( + algorithm, + baseKeys[size], + keyType, + true, + usages); + + const bits = await subtle.exportKey('raw', key); + + assert.strictEqual( + Buffer.from(bits).toString('hex'), + kDerivations[size][saltSize][hash][iterations] + .slice(0, keyType.length / 4)); +} + +async function testDeriveKeyBadHash( + baseKeys, + size, + saltSize, + hash, + iterations, + keyType, + usages) { + const salt = Buffer.from(kSalts[saltSize], 'hex'); + const algorithm = { name: 'PBKDF2', salt, iterations }; + + return Promise.all([ + assert.rejects( + subtle.deriveKey( + { + ...algorithm, + hash: hash.substring(0, 3) + hash.substring(4) + }, + baseKeys[size], + keyType, + true, + usages), + { message: /Unrecognized name/ }), + assert.rejects( + subtle.deriveKey( + { + ...algorithm, + hash: 'HKDF' + }, + baseKeys[size], + keyType, + true, + usages), + { message: /Unrecognized name/ }) + ]); +} + +async function testDeriveKeyBadUsage( + noKey, + size, + saltSize, + hash, + iterations, + keyType, + usages) { + const algorithm = { + name: 'PBKDF2', + salt: Buffer.from(kSalts[saltSize], 'hex'), + iterations, + hash + }; + + return assert.rejects( + subtle.deriveKey(algorithm, noKey[size], keyType, true, usages), { + message: /baseKey does not have deriveKey usage/ + }); +} + +async function testWrongKeyType( + wrongKey, + saltSize, + hash, + iterations, + keyType, + usages +) { + const algorithm = { + name: 'PBKDF2', + salt: Buffer.from(kSalts[saltSize], 'hex'), + iterations, + hash + }; + + return assert.rejects( + subtle.deriveKey(algorithm, wrongKey, keyType, true, usages), { + message: /Key algorithm mismatch/ + }); +} + +(async function() { + const { + baseKeys, + noBits, + noKey, + wrongKey + } = await setupBaseKeys(); + + const variations = []; + + Object.keys(kDerivations).forEach((size) => { + Object.keys(kDerivations[size]).forEach((saltSize) => { + Object.keys(kDerivations[size][saltSize]).forEach((hash) => { + Object.keys(kDerivations[size][saltSize][hash]) + .forEach((iterations) => { + const args = [baseKeys, size, saltSize, hash, iterations | 0]; + variations.push(testDeriveBits(...args)); + variations.push(testDeriveBitsBadLengths(...args)); + variations.push(testDeriveBitsBadHash(...args)); + variations.push( + testDeriveBitsBadUsage( + noBits, + size, + saltSize, + hash, + iterations | 0)); + + kDerivedKeyTypes.forEach((keyType) => { + const keyArgs = getDeriveKeyInfo(...keyType); + variations.push(testDeriveKey(...args, ...keyArgs)); + variations.push(testDeriveKeyBadHash(...args, ...keyArgs)); + variations.push( + testDeriveKeyBadUsage( + noKey, + size, + saltSize, + hash, + iterations | 0, + ...keyArgs)); + variations.push( + testWrongKeyType( + wrongKey, + saltSize, + hash, + iterations, + ...keyArgs)); + }); + }); + }); + }); + }); + + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-derivebits.js b/test/parallel/test-webcrypto-derivebits.js new file mode 100644 index 00000000000000..a8d4d6e673862f --- /dev/null +++ b/test/parallel/test-webcrypto-derivebits.js @@ -0,0 +1,131 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const { internalBinding } = require('internal/test/binding'); + +// This is only a partial test. The WebCrypto Web Platform Tests +// will provide much greater coverage. + +// Test ECDH bit derivation +{ + async function test(namedCurve) { + const [alice, bob] = await Promise.all([ + subtle.generateKey({ name: 'ECDH', namedCurve }, true, ['deriveBits']), + subtle.generateKey({ name: 'ECDH', namedCurve }, true, ['deriveBits']) + ]); + + const [secret1, secret2] = await Promise.all([ + subtle.deriveBits({ + name: 'ECDH', namedCurve, public: alice.publicKey + }, bob.privateKey, 128), + subtle.deriveBits({ + name: 'ECDH', namedCurve, public: bob.publicKey + }, alice.privateKey, 128) + ]); + + assert.deepStrictEqual(secret1, secret2); + } + + test('P-521').then(common.mustCall()); +} + +// Test HKDF bit derivation +{ + async function test(pass, info, salt, hash, length, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'HKDF', hash }, + false, ['deriveBits']); + const secret = await subtle.deriveBits({ + name: 'HKDF', + hash, + salt: ec.encode(salt), + info: ec.encode(info) + }, key, length); + assert.strictEqual(Buffer.from(secret).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 'my friend', 'SHA-256', 512, + '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e329' + + '41ef21bdeb0fa87b6b6bfa5c54466aa5bf76cdc2685fb' + + 'a4408ea5b94c049fe035649b46f92fdc519374'], + ['hello', 'there', 'my friend', 'SHA-384', 128, + 'e36cf2cf943d8f3a88adb80f478745c3'] + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test PBKDF2 bit derivation +{ + async function test(pass, salt, iterations, hash, length, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'PBKDF2', hash }, + false, ['deriveBits']); + const secret = await subtle.deriveBits({ + name: 'PBKDF2', + hash, + salt: ec.encode(salt), + iterations, + }, key, length); + assert.strictEqual(Buffer.from(secret).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 10, 'SHA-256', 512, + 'f72d1cf4853fffbd16a42751765d11f8dc7939498ee7b7' + + 'ce7678b4cb16fad88098110a83e71f4483ce73203f7a64' + + '719d293280f780f9fafdcf46925c5c0588b3'], + ['hello', 'there', 5, 'SHA-384', 128, + '201509b012c9cd2fbe7ea938f0c509b3'] + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test Scrypt bit derivation +if (typeof internalBinding('crypto').scrypt === 'function') { + async function test(pass, salt, length, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'NODE-SCRYPT' }, + false, ['deriveBits']); + const secret = await subtle.deriveBits({ + name: 'SCRYPT', + salt: ec.encode(salt), + }, key, length); + assert.strictEqual(Buffer.from(secret).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 512, + '30ddda6feabaac788eb81cc38f496cd5d9a165d320c537ea05331fe720db1061b3a27' + + 'b91a8428e49d44078c1fa395cb1c6db336ba44ccb80faa6d74918769374'], + ['hello', 'there', 128, + '30ddda6feabaac788eb81cc38f496cd5'] + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-derivekey-ecdh.js b/test/parallel/test-webcrypto-derivekey-ecdh.js new file mode 100644 index 00000000000000..f63c8fd64ed4c8 --- /dev/null +++ b/test/parallel/test-webcrypto-derivekey-ecdh.js @@ -0,0 +1,246 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle, getRandomValues } = require('crypto').webcrypto; + +const kTests = [ + { + namedCurve: 'P-521', + pkcs8: '3081ee020100301006072a8648ce3d020106052b810400230481d63081d302010' + + '1044201a67ed321915a64aa359b7d648ddc2618fa8e8d1867e8f71830b10d25ed' + + '2891faf12f3c7e75421a2ea264f9a915320d274fe1470742b984e96b98912081f' + + 'acd478da18189038186000400209d483f28666881c6641f3a126f400f51e46511' + + '70fe678c75e85712e2868adc850824997bebf0bc82b43028a6d2ec1777ca45279' + + 'f7206a3ea8b5cd2073f493e45000cb54c3a5acaa268c56710428878d98b8afbf6' + + '8a612153632846d807e92672698f1b9c611de7d38e34cd6c73889092c56e52d68' + + '0f1dfd092b87ac8ef9ff3c8fb48', + spki: '30819b301006072a8648ce3d020106052b81040023038186000400ee69f94715d7' + + '01e9e2011333d4f4f96cba7d91f88b112baf75cf09cc1f8aca97618da9389822d2' + + '9b6fe9996a61203ef752b771e8958fc4677bb3778565ab60d6ed00deab6761895b' + + '935e3ad325fb8549e56f13786aa73f88a2ecfe40933473d8aef240c4dfd7d506f2' + + '2cdd0e55558f3fbf05ebf7efef7a72d78f46469b8448f26e2712', + result: '009c2bce57be80adab3b07385b8e5990eb7d6fdebdb01bf35371a4f6075e9d28', + }, + { + namedCurve: 'P-384', + pkcs8: '3081b6020100301006072a8648ce3d020106052b8104002204819e30819b02010' + + '10430f871a5666589c14a5747263ef85b319cc023db6e35676c3d781eef8b055f' + + 'cfbe86fa0d06d056b5195fb1323af8de25b3a16403620004f11965df7dd4594d0' + + '419c5086482a3b826b9797f9be0bd0d109c9e1e9989c1b9a92b8f269f98e17ad1' + + '84ba73c1f79762af45af8141602642da271a6bb0ffeb0cb4478fcf707e661aa6d' + + '6cdf51549c88c3f130be9e8201f6f6a09f4185aaf95c4', + spki: '3076301006072a8648ce3d020106052b810400220362000491822dc2af59c18f5b' + + '67f80df61a2603c2a8f0b3c0af822d63c279701a824560404401dde9a56ee52757' + + 'ea8bc748d4c82b5337b48d7b65583a3d572438880036bac6730f42ca5278966bd5' + + 'f21e86e21d30c5a6d0463ec513dd509ffcdcaf1ff5', + result: 'e0bd6bce0aef8ca48838a6e2fcc57e67b9c5e8860c5f0be9dabec53e454e18a0', + }, +]; + +async function prepareKeys() { + const keys = {}; + await Promise.all( + kTests.map(async ({ namedCurve, size, pkcs8, spki, result }) => { + const [ + privateKey, + publicKey + ] = await Promise.all([ + subtle.importKey( + 'pkcs8', + Buffer.from(pkcs8, 'hex'), + { + name: 'ECDH', + namedCurve + }, + true, + ['deriveKey', 'deriveBits']), + subtle.importKey( + 'spki', + Buffer.from(spki, 'hex'), + { + name: 'ECDH', + namedCurve + }, + true, + ['deriveKey', 'deriveBits']) + ]); + keys[namedCurve] = { + privateKey, + publicKey, + size, + result, + }; + })); + return keys; +} + +(async function() { + const keys = await prepareKeys(); + const otherArgs = [ + { name: 'HMAC', hash: 'SHA-256', length: 256 }, + true, + ['sign', 'verify']]; + + await Promise.all( + Object.keys(keys).map(async (namedCurve) => { + const { result, privateKey, publicKey } = keys[namedCurve]; + + { + // Good parameters + const key = await subtle.deriveKey({ + name: 'ECDH', + public: publicKey + }, privateKey, ...otherArgs); + + const raw = await subtle.exportKey('raw', key); + + assert.strictEqual(Buffer.from(raw).toString('hex'), result); + } + + { + // Case insensitivity + const key = await subtle.deriveKey({ + name: 'eCdH', + public: publicKey + }, privateKey, { + name: 'HmAc', + hash: 'SHA-256', + length: 256 + }, true, ['sign', 'verify']); + + const raw = await subtle.exportKey('raw', key); + + assert.strictEqual(Buffer.from(raw).toString('hex'), result); + } + })); + + // Error tests + { + // Missing public property + await assert.rejects( + subtle.deriveKey( + { name: 'ECDH' }, + keys['P-384'].privateKey, + ...otherArgs), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // The public property is not a CryptoKey + await assert.rejects( + subtle.deriveKey( + { + name: 'ECDH', + public: { message: 'Not a CryptoKey' } + }, + keys['P-384'].privateKey, + ...otherArgs), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + { + // Mismatched named curves + await assert.rejects( + subtle.deriveKey( + { + name: 'ECDH', + public: keys['P-384'].publicKey + }, + keys['P-521'].privateKey, + ...otherArgs), + { message: /Named curve mismatch/ }); + } + + { + // Incorrect public key algorithm + const { publicKey } = await subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-521' + }, + false, + ['verify']); + + await assert.rejects( + subtle.deriveKey( + { + name: 'ECDH', + public: publicKey + }, + keys['P-521'].privateKey, + ...otherArgs), + { message: /Keys must be ECDH keys/ }); + } + + { + // Private key does not have correct usages + const privateKey = await subtle.importKey( + 'pkcs8', + Buffer.from(kTests[0].pkcs8, 'hex'), + { + name: 'ECDH', + namedCurve: 'P-521' + }, false, ['deriveBits']); + + await assert.rejects( + subtle.deriveKey( + { + name: 'ECDH', + public: keys['P-521'].publicKey, + }, + privateKey, + ...otherArgs), + { message: /baseKey does not have deriveKey usage/ }); + } + + { + // Base key is not a private key + await assert.rejects( + subtle.deriveKey( + { + name: 'ECDH', + public: keys['P-521'].publicKey + }, + keys['P-521'].publicKey, + ...otherArgs), + { message: /baseKey must be a private key/ }); + } + + { + // Base key is not a private key + await assert.rejects( + subtle.deriveKey( + { + name: 'ECDH', + public: keys['P-521'].privateKey + }, + keys['P-521'].publicKey, + ...otherArgs), + { message: /algorithm\.public must be a public key/ }); + } + + { + // Public is a secret key + const keyData = getRandomValues(new Uint8Array(32)); + const key = await subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC', length: 256 }, + false, ['encrypt']); + + await assert.rejects( + subtle.deriveKey( + { + name: 'ECDH', + public: key + }, + keys['P-521'].publicKey, + ...otherArgs), + { message: /algorithm\.public must be a public key/ }); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js new file mode 100644 index 00000000000000..9434da1fb672fb --- /dev/null +++ b/test/parallel/test-webcrypto-derivekey.js @@ -0,0 +1,154 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const { internalBinding } = require('internal/test/binding'); + +// This is only a partial test. The WebCrypto Web Platform Tests +// will provide much greater coverage. + +// Test ECDH bit derivation +{ + async function test(namedCurve) { + const [alice, bob] = await Promise.all([ + subtle.generateKey({ name: 'ECDH', namedCurve }, true, ['deriveKey']), + subtle.generateKey({ name: 'ECDH', namedCurve }, true, ['deriveKey']) + ]); + + const [secret1, secret2] = await Promise.all([ + subtle.deriveKey({ + name: 'ECDH', namedCurve, public: alice.publicKey + }, bob.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']), + subtle.deriveKey({ + name: 'ECDH', namedCurve, public: bob.publicKey + }, alice.privateKey, { + name: 'AES-CBC', + length: 256 + }, true, ['encrypt']) + ]); + + const [raw1, raw2] = await Promise.all([ + subtle.exportKey('raw', secret1), + subtle.exportKey('raw', secret2) + ]); + + assert.deepStrictEqual(raw1, raw2); + } + + test('P-521').then(common.mustCall()); +} + +// Test HKDF bit derivation +{ + async function test(pass, info, salt, hash, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'HKDF', hash }, + false, ['deriveKey']); + + const secret = await subtle.deriveKey({ + name: 'HKDF', + hash, + salt: ec.encode(salt), + info: ec.encode(info) + }, key, { + name: 'AES-CTR', + length: 256 + }, true, ['encrypt']); + + const raw = await subtle.exportKey('raw', secret); + + assert.strictEqual(Buffer.from(raw).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 'my friend', 'SHA-256', + '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e32941ef21bdeb0fa87b6b6'], + ['hello', 'there', 'my friend', 'SHA-384', + 'e36cf2cf943d8f3a88adb80f478745c336ac811b1a86d03a7d10eb0b6b52295c'] + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test PBKDF2 bit derivation +{ + async function test(pass, salt, iterations, hash, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'PBKDF2', hash }, + false, ['deriveKey']); + const secret = await subtle.deriveKey({ + name: 'PBKDF2', + hash, + salt: ec.encode(salt), + iterations, + }, key, { + name: 'AES-CTR', + length: 256 + }, true, ['encrypt']); + + const raw = await subtle.exportKey('raw', secret); + + assert.strictEqual(Buffer.from(raw).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 10, 'SHA-256', + 'f72d1cf4853fffbd16a42751765d11f8dc7939498ee7b7ce7678b4cb16fad880'], + ['hello', 'there', 5, 'SHA-384', + '201509b012c9cd2fbe7ea938f0c509b36ecb140f38bf9130e96923f55f46756d'] + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test Scrypt bit derivation +if (typeof internalBinding('crypto').scrypt === 'function') { + async function test(pass, salt, expected) { + const ec = new TextEncoder(); + const key = await subtle.importKey( + 'raw', + ec.encode(pass), + { name: 'NODE-SCRYPT' }, + false, ['deriveKey']); + const secret = await subtle.deriveKey({ + name: 'NODE-SCRYPT', + salt: ec.encode(salt), + }, key, { + name: 'AES-CTR', + length: 256 + }, true, ['encrypt']); + + const raw = await subtle.exportKey('raw', secret); + + assert.strictEqual(Buffer.from(raw).toString('hex'), expected); + } + + const kTests = [ + ['hello', 'there', 10, 'SHA-256', + '30ddda6feabaac788eb81cc38f496cd5d9a165d320c537ea05331fe720db1061'] + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js new file mode 100644 index 00000000000000..7ca85908d63e9c --- /dev/null +++ b/test/parallel/test-webcrypto-digest.js @@ -0,0 +1,170 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { Buffer } = require('buffer'); +const { subtle } = require('crypto').webcrypto; +const { createHash } = require('crypto'); + +const kTests = [ + ['SHA-1', 'sha1', 160], + ['SHA-256', 'sha256', 256], + ['SHA-384', 'sha384', 384], + ['SHA-512', 'sha512', 512] +]; + +// Empty hash just works, not checking result +subtle.digest('SHA-512', Buffer.alloc(0)) + .then(common.mustCall()); + +// TODO(@jasnell): Need to move this particular test to pummel +// // Careful, this is an expensive operation because of both the memory +// // allocation and the cost of performing the hash on such a large +// // input. +// subtle.digest('SHA-512', new ArrayBuffer(2 ** 31 - 1)) +// .then(common.mustCall()); + +// TODO(@jasnell): Change max to 2 ** 31 - 1 +// assert.rejects(subtle.digest('SHA-512', new ArrayBuffer(kMaxLength + 1)), { +// code: 'ERR_OUT_OF_RANGE' +// }); + +const kData = (new TextEncoder()).encode('hello'); +(async function() { + await Promise.all(kTests.map(async (test) => { + // Get the digest using the legacy crypto API + const checkValue = + createHash(test[1]).update(kData).digest().toString('hex'); + + // Get the digest using the SubtleCrypto API + const values = Promise.all([ + subtle.digest({ name: test[0] }, kData), + subtle.digest({ name: test[0], length: test[2] }, kData), + subtle.digest(test[0], kData), + subtle.digest(test[0], kData.buffer), + subtle.digest(test[0], new DataView(kData.buffer)), + subtle.digest(test[0], Buffer.from(kData)), + ]); + + // subtle.digest copies the input data, so changing it + // while we're waiting on the Promises should never + // cause the test to fail. + kData[0] = 0x1; + kData[2] = 0x2; + kData[4] = 0x3; + + // Compare that the legacy crypto API and SubtleCrypto API + // produce the same results + (await values).forEach((v) => { + assert(v instanceof ArrayBuffer); + assert.strictEqual(checkValue, Buffer.from(v).toString('hex')); + }); + })); +})().then(common.mustCall()); + +[1, [], {}, null, undefined].forEach((i) => { + assert.rejects(subtle.digest(i), { message: /Unrecognized name/ }); +}); + +assert.rejects(subtle.digest(''), { message: /Unrecognized name/ }); + +[1, [], {}, null, undefined].forEach((i) => { + assert.rejects(subtle.digest('SHA-256', i), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// If there is a mismatch between length and the expected digest size for +// the selected algorithm, we fail. The length is a Node.js specific +// addition to the API, and is added as a support for future additional +// hash algorithms that support variable digest output lengths. +assert.rejects(subtle.digest({ name: 'SHA-512', length: 510 }, kData), { + message: /Digest method not supported/ +}); + +const kSourceData = { + empty: '', + short: '156eea7cc14c56cb94db030a4a9d95ff', + medium: 'b6c8f9df648cd088b70f38e74197b18cb81e1e435' + + '0d50bccb8fb5a7379c87bb2e3d6ed5461ed1e9f36' + + 'f340a3962a446b815b794b4bd43a4403502077b22' + + '56cc807837f3aacd118eb4b9c2baeb897068625ab' + + 'aca193', + long: null +}; + +kSourceData.long = kSourceData.medium.repeat(1024); + +const kDigestedData = { + 'sha-1': { + empty: 'da39a3ee5e6b4b0d3255bfef95601890afd80709', + short: 'c91318cdf2396a015e3f4e6a86a0ba65b8635944', + medium: 'e541060870eb16bf33b68e51f513526893986729', + long: '3098b50037ecd02ebd657653b2bfa01eee27a2ea' + }, + 'sha-256': { + empty: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + short: 'a2831186984792c7d32d59c89740687f19addc1b959e71a1cc538a3b7ed843f2', + medium: '535367877ef014d7fc717e5cb7843e59b61aee62c7029cec7ec6c12fd924e0e4', + long: '14cdea9dc75f5a6274d9fc1e64009912f1dcd306b48fe8e9cf122de671571781' + }, + 'sha-384': { + empty: '38b060a751ac96384cd9327eb1b1e36a21fdb71114b' + + 'e07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2' + + 'f14898b95b', + short: '6bf5ea6524d1cddc43f7cf3b56ee059227404a2f538' + + 'f022a3db7447a782c06c1ed05e8ab4f5edc17f37114' + + '40dfe97731', + medium: 'cbc2c588fe5b25f916da28b4e47a484ae6fc1fe490' + + '2dd5c9939a6bfd034ab3b48b39087436011f6a9987' + + '9d279540e977', + long: '49f4fdb3981968f97d57370f85345067cd5296a97dd1' + + 'a18e06911e756e9608492529870e1ad130998d57cbfb' + + 'b7c1d09e' + }, + 'sha-512': { + empty: 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5' + + '715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318' + + 'd2877eec2f63b931bd47417a81a538327af927da3e', + short: '375248be5ff34be74cab4ff1c3bc8dc68bd5f8dff40' + + '23e98f87b865cff2c724292df189443a64ff4134a65' + + 'cd4635b9d4f5dc0d3fb67528002a63acf26c9da575', + medium: 'b9109f839e8ea43c890f293ce11dc6e2798d1e2431' + + 'f1e4b919e3b20c4f36303ba39c916db306c45a3b65' + + '761ff5be85328eeaf42c3830f1d95e7a41165b7d2d36', + long: '4b02caf650276030ea5617e597c5d53fd9daa68b78bfe' + + '60b22aab8d36a4c2a3affdb71234f49276737c575ddf7' + + '4d14054cbd6fdb98fd0ddcbcb46f91ad76b6ee' + } +}; + +async function testDigest(size, name) { + const digest = await subtle.digest( + name, + Buffer.from(kSourceData[size], 'hex')); + + assert.strictEqual( + Buffer.from(digest).toString('hex'), + kDigestedData[name.toLowerCase()][size]); +} + +(async function() { + const variations = []; + Object.keys(kSourceData).forEach((size) => { + Object.keys(kDigestedData).forEach((alg) => { + const upCase = alg.toUpperCase(); + const downCase = alg.toLowerCase(); + const mixedCase = upCase.substr(0, 1) + downCase.substr(1); + + variations.push(testDigest(size, upCase)); + variations.push(testDigest(size, downCase)); + variations.push(testDigest(size, mixedCase)); + }); + }); + + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js new file mode 100644 index 00000000000000..ec1635c991ce13 --- /dev/null +++ b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js @@ -0,0 +1,198 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt', 'decrypt']); + + const output = await subtle.encrypt(algorithm, key, plaintext); + plaintext[0] = 255 - plaintext[0]; + + assert.strictEqual( + Buffer.from(output).toString('hex'), + Buffer.from(result).toString('hex')); + + const check = await subtle.decrypt(algorithm, key, output); + output[0] = 255 - output[0]; + + assert.strictEqual( + Buffer.from(check).toString('hex'), + Buffer.from(plaintext).toString('hex')); +} + +async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['decrypt']); + + return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { + message: /The requested operation is not valid for the provided key/ + }); +} + +async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt']); + + const output = await subtle.encrypt(algorithm, key, plaintext); + + return assert.rejects(subtle.decrypt(algorithm, key, output), { + message: /The requested operation is not valid for the provided key/ + }); +} + +async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { + assert.notStrictEqual(algorithm.name, alg); + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: alg }, + false, + ['encrypt']); + + return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { + message: /The requested operation is not valid for the provided key/ + }); +} + +async function testDecrypt({ keyBuffer, algorithm, result }) { + const key = await subtle.importKey( + 'raw', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt', 'decrypt']); + + await subtle.decrypt(algorithm, key, result); +} + +// Test aes-cbc vectors +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_cbc')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-CTR')); + }); + + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /algorithm\.iv must contain exactly 16 bytes/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /algorithm\.iv must contain exactly 16 bytes/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + message: /bad decrypt/ + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + +// Test aes-ctr vectors +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_ctr')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-CBC')); + }); + + // TODO(@jasnell): These fail for different reasons. Need to + // make them consistent + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /.*/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /.*/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + message: /bad decrypt/ + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + +// Test aes-gcm vectors +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_gcm')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-CBC')); + }); + + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /is not a valid AES-GCM tag length/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /is not a valid AES-GCM tag length/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + message: /bad decrypt/ + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-rsa.js b/test/parallel/test-webcrypto-encrypt-decrypt-rsa.js new file mode 100644 index 00000000000000..1099930385553a --- /dev/null +++ b/test/parallel/test-webcrypto-encrypt-decrypt-rsa.js @@ -0,0 +1,244 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const { + passing +} = require('../fixtures/crypto/rsa')(); + +async function importVectorKey( + publicKeyBuffer, + privateKeyBuffer, + name, + hash, + publicUsages, + privateUsages) { + const [publicKey, privateKey] = await Promise.all([ + subtle.importKey( + 'spki', publicKeyBuffer, { name, hash }, false, publicUsages), + subtle.importKey( + 'pkcs8', privateKeyBuffer, { name, hash }, false, privateUsages) + ]); + + return { publicKey, privateKey }; +} + +async function testDecryption({ + ciphertext, + algorithm, + plaintext, + hash, + publicKeyBuffer, + privateKeyBuffer }) { + if (ciphertext === undefined) + return; + + const { + privateKey + } = await importVectorKey( + publicKeyBuffer, + privateKeyBuffer, + algorithm.name, + hash, + ['encrypt'], + ['decrypt']); + + const encodedPlaintext = Buffer.from(plaintext).toString('hex'); + const result = await subtle.decrypt(algorithm, privateKey, ciphertext); + + assert.strictEqual( + Buffer.from(result).toString('hex'), + encodedPlaintext); + + const ciphercopy = Buffer.from(ciphertext); + + // Modifying the ciphercopy after calling decrypt should just work + const result2 = await subtle.decrypt(algorithm, privateKey, ciphercopy); + ciphercopy[0] = 255 - ciphercopy[0]; + + assert.strictEqual( + Buffer.from(result2).toString('hex'), + encodedPlaintext); +} + +async function testEncryption( + { + ciphertext, + algorithm, + plaintext, + hash, + publicKeyBuffer, + privateKeyBuffer + }, + modify = false) { + const { + publicKey, + privateKey + } = await importVectorKey( + publicKeyBuffer, + privateKeyBuffer, + algorithm.name, + hash, + ['encrypt'], + ['decrypt']); + + if (modify) + plaintext = Buffer.from(plaintext); // make a copy + + const encodedPlaintext = Buffer.from(plaintext).toString('hex'); + + const result = await subtle.encrypt(algorithm, publicKey, plaintext); + if (modify) + plaintext[0] = 255 - plaintext[0]; + + assert.strictEqual( + result.byteLength * 8, + privateKey.algorithm.modulusLength); + + const out = await subtle.decrypt(algorithm, privateKey, result); + + assert.strictEqual( + Buffer.from(out).toString('hex'), + encodedPlaintext); +} + +async function testEncryptionLongPlaintext({ + algorithm, + plaintext, + hash, + publicKeyBuffer, + privateKeyBuffer }) { + const { + publicKey, + } = await importVectorKey( + publicKeyBuffer, + privateKeyBuffer, + algorithm.name, + hash, + ['encrypt'], + ['decrypt']); + const newplaintext = new Uint8Array(plaintext.byteLength + 1); + newplaintext.set(plaintext, 0); + newplaintext[plaintext.byteLength] = 32; + + return assert.rejects( + subtle.encrypt(algorithm, publicKey, newplaintext), { + message: /data too large/ + }); +} + +async function testEncryptionWrongKey({ + algorithm, + plaintext, + hash, + publicKeyBuffer, + privateKeyBuffer }) { + const { + privateKey, + } = await importVectorKey( + publicKeyBuffer, + privateKeyBuffer, + algorithm.name, + hash, + ['encrypt'], + ['decrypt']); + return assert.rejects( + subtle.encrypt(algorithm, privateKey, plaintext), { + message: /The requested operation is not valid/ + }); +} + +async function testEncryptionBadUsage({ + algorithm, + plaintext, + hash, + publicKeyBuffer, + privateKeyBuffer }) { + const { + publicKey, + } = await importVectorKey( + publicKeyBuffer, + privateKeyBuffer, + algorithm.name, + hash, + ['wrapKey'], + ['decrypt']); + return assert.rejects( + subtle.encrypt(algorithm, publicKey, plaintext), { + message: /The requested operation is not valid/ + }); +} + +async function testDecryptionWrongKey({ + ciphertext, + algorithm, + hash, + publicKeyBuffer, + privateKeyBuffer }) { + if (ciphertext === undefined) + return; + + const { + publicKey + } = await importVectorKey( + publicKeyBuffer, + privateKeyBuffer, + algorithm.name, + hash, + ['encrypt'], + ['decrypt']); + + return assert.rejects( + subtle.decrypt(algorithm, publicKey, ciphertext), { + message: /The requested operation is not valid/ + }); +} + +async function testDecryptionBadUsage({ + ciphertext, + algorithm, + hash, + publicKeyBuffer, + privateKeyBuffer }) { + if (ciphertext === undefined) + return; + + const { + publicKey + } = await importVectorKey( + publicKeyBuffer, + privateKeyBuffer, + algorithm.name, + hash, + ['encrypt'], + ['unwrapKey']); + + return assert.rejects( + subtle.decrypt(algorithm, publicKey, ciphertext), { + message: /The requested operation is not valid/ + }); +} + +(async function() { + const variations = []; + + // Test decryption + passing.forEach(async (vector) => { + variations.push(testDecryption(vector)); + variations.push(testDecryptionWrongKey(vector)); + variations.push(testDecryptionBadUsage(vector)); + variations.push(testEncryption(vector)); + variations.push(testEncryption(vector, true)); + variations.push(testEncryptionLongPlaintext(vector)); + variations.push(testEncryptionWrongKey(vector)); + variations.push(testEncryptionBadUsage(vector)); + }); + + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js new file mode 100644 index 00000000000000..05157039746d4b --- /dev/null +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -0,0 +1,118 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle, getRandomValues } = require('crypto').webcrypto; + +// This is only a partial test. The WebCrypto Web Platform Tests +// will provide much greater coverage. + +// Test Encrypt/Decrypt RSA-OAEP +{ + const buf = getRandomValues(new Uint8Array(50)); + + async function test() { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-384', + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt({ + name: 'RSA-OAEP', + label: ec.encode('a label') + }, publicKey, buf); + + const plaintext = await subtle.decrypt({ + name: 'RSA-OAEP', + label: ec.encode('a label') + }, privateKey, ciphertext); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} + +// Test Encrypt/Decrypt AES-CTR +{ + const buf = getRandomValues(new Uint8Array(50)); + const counter = getRandomValues(new Uint8Array(16)); + + async function test() { + const key = await subtle.generateKey({ + name: 'AES-CTR', + length: 256 + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt({ + name: 'AES-CTR', counter, length: 64 }, key, buf); + + const plaintext = await subtle.decrypt({ + name: 'AES-CTR', counter, length: 64 }, key, ciphertext); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} + +// Test Encrypt/Decrypt AES-CBC +{ + const buf = getRandomValues(new Uint8Array(50)); + const iv = getRandomValues(new Uint8Array(16)); + + async function test() { + const key = await subtle.generateKey({ + name: 'AES-CBC', + length: 256 + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt({ + name: 'AES-CBC', iv }, key, buf); + + const plaintext = await subtle.decrypt({ + name: 'AES-CBC', iv }, key, ciphertext); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} + +// Test Encrypt/Decrypt AES-GCM +{ + const buf = getRandomValues(new Uint8Array(50)); + const iv = getRandomValues(new Uint8Array(12)); + + async function test() { + const key = await subtle.generateKey({ + name: 'AES-GCM', + length: 256 + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt({ + name: 'AES-GCM', iv }, key, buf); + + const plaintext = await subtle.decrypt({ + name: 'AES-GCM', iv }, key, ciphertext); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-export-import-dsa.js b/test/parallel/test-webcrypto-export-import-dsa.js new file mode 100644 index 00000000000000..f73e6513dddaff --- /dev/null +++ b/test/parallel/test-webcrypto-export-import-dsa.js @@ -0,0 +1,224 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const sizes = [1024]; + +const hashes = [ + 'SHA-1', + 'SHA-256', + 'SHA-384', + 'SHA-512' +]; + +const keyData = { + 1024: { + spki: Buffer.from( + '308201c03082013406072a8648ce3804013082012702818100d5f35aa5730e26166fd' + + '3ea81f8f0eeb05bd1250e164b7c76b180b6dae95096d13dee6956e15a9aea7cf18a0d' + + 'f7c5dc326ccef1cbf97636d22f870b76f2607f9a867db2756aecf65505aa48fdea5f5' + + 'ee54f508a05d9dae76bf262b4ca3662cc176b7c628c7bee2076df07f9a64e0402630d' + + 'fee63eaf0ed64d48b469fe1c9ac4a1021d00b14213226cfcfb59e3a0379e559c74ff8' + + 'a7383eb4c41cecb6f3732b702818100a0865b7f8954e7ae587c8e6a89e391e82657c5' + + '8f05ccd94de61748e89e217efab3d9b5fa842ebc62525966916ad2b7af422a9b24078' + + '17a5b382b6581434fd1a169c75ad4d0e3862a3f484e9f9f2a816f943a8e6060f26fe2' + + '7c533587b765e57948439084e76fd6a4fd004f5c78d972cf7f100ec9494a902645bac' + + 'a4b4c6f399303818500028181009a8df69f2fe321869e2094e387bc1dc2b5f3bff2a2' + + 'e23cfba51d3c119fba6b4c15a49485fa811b6955d91d28c9e2e0445a79ddc5426b2fe' + + '44e00a6c9254c776f13fd10dbc934262077b1df72c16bc848817c61fb6a607abe60c7' + + 'd11528ab9bdf55de45495733a047bd75a48b8166f1aa3deab681a2574a4f35106f0d7' + + '8b641d7', 'hex'), + pkcs8: Buffer.from( + '3082015b0201003082013406072a8648ce3804013082012702818100d5f35aa5730e2' + + '6166fd3ea81f8f0eeb05bd1250e164b7c76b180b6dae95096d13dee6956e15a9aea7c' + + 'f18a0df7c5dc326ccef1cbf97636d22f870b76f2607f9a867db2756aecf65505aa48f' + + 'dea5f5ee54f508a05d9dae76bf262b4ca3662cc176b7c628c7bee2076df07f9a64e04' + + '02630dfee63eaf0ed64d48b469fe1c9ac4a1021d00b14213226cfcfb59e3a0379e559' + + 'c74ff8a7383eb4c41cecb6f3732b702818100a0865b7f8954e7ae587c8e6a89e391e8' + + '2657c58f05ccd94de61748e89e217efab3d9b5fa842ebc62525966916ad2b7af422a9' + + 'b2407817a5b382b6581434fd1a169c75ad4d0e3862a3f484e9f9f2a816f943a8e6060' + + 'f26fe27c533587b765e57948439084e76fd6a4fd004f5c78d972cf7f100ec9494a902' + + '645baca4b4c6f3993041e021c600daa0a9c4cc674c98bb07956374c84ac1c33af8816' + + '3ea7e2587876', 'hex'), + jwk: { + kty: 'DSA', + y: 'mo32ny_jIYaeIJTjh7wdwrXzv_Ki4jz7pR08EZ-6a0wVpJSF-oEbaVXZHSjJ4uBEWn' + + 'ndxUJrL-ROAKbJJUx3bxP9ENvJNCYgd7HfcsFryEiBfGH7amB6vmDH0RUoq5vfVd5F' + + 'SVczoEe9daSLgWbxqj3qtoGiV0pPNRBvDXi2Qdc', + p: '1fNapXMOJhZv0-qB-PDusFvRJQ4WS3x2sYC22ulQltE97mlW4Vqa6nzxig33xdwybM' + + '7xy_l2NtIvhwt28mB_moZ9snVq7PZVBapI_epfXuVPUIoF2drna_JitMo2YswXa3xi' + + 'jHvuIHbfB_mmTgQCYw3-5j6vDtZNSLRp_hyaxKE', + q: 'sUITImz8-1njoDeeVZx0_4pzg-tMQc7Lbzcytw', + g: 'oIZbf4lU565YfI5qieOR6CZXxY8FzNlN5hdI6J4hfvqz2bX6hC68YlJZZpFq0revQi' + + 'qbJAeBels4K2WBQ0_RoWnHWtTQ44YqP0hOn58qgW-UOo5gYPJv4nxTNYe3ZeV5SEOQ' + + 'hOdv1qT9AE9ceNlyz38QDslJSpAmRbrKS0xvOZM', + x: 'YA2qCpxMxnTJi7B5VjdMhKwcM6-IFj6n4lh4dg', + } + }, +}; + +async function testImportSpki({ name, publicUsages }, size, hash, extractable) { + const key = await subtle.importKey( + 'spki', + keyData[size].spki, + { name, hash }, + extractable, + publicUsages); + + assert.strictEqual(key.type, 'public'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, publicUsages); + assert.strictEqual(key.algorithm.name, name); + assert.strictEqual(key.algorithm.modulusLength, size); + assert.strictEqual(key.algorithm.hash.name, hash); + + if (extractable) { + const spki = await subtle.exportKey('spki', key); + assert.strictEqual( + Buffer.from(spki).toString('hex'), + keyData[size].spki.toString('hex')); + } else { + await assert.rejects( + subtle.exportKey('spki', key), { + message: /key is not extractable/ + }); + } +} + +async function testImportPkcs8( + { name, privateUsages }, + size, + hash, + extractable) { + const key = await subtle.importKey( + 'pkcs8', + keyData[size].pkcs8, + { name, hash }, + extractable, + privateUsages); + + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, privateUsages); + assert.strictEqual(key.algorithm.name, name); + assert.strictEqual(key.algorithm.modulusLength, size); + assert.strictEqual(key.algorithm.hash.name, hash); + + if (extractable) { + const pkcs8 = await subtle.exportKey('pkcs8', key); + assert.strictEqual( + Buffer.from(pkcs8).toString('hex'), + keyData[size].pkcs8.toString('hex')); + } else { + await assert.rejects( + subtle.exportKey('pkcs8', key), { + message: /key is not extractable/ + }); + } +} + +async function testImportJwk( + { name, publicUsages, privateUsages }, + size, + hash, + extractable) { + + const jwk = keyData[size].jwk; + + const [ + publicKey, + privateKey, + ] = await Promise.all([ + subtle.importKey( + 'jwk', + { + kty: jwk.kty, + y: jwk.y, + p: jwk.p, + q: jwk.q, + g: jwk.g, + alg: `NODE-DSA-${hash}` + }, + { name, hash }, + extractable, + publicUsages), + subtle.importKey( + 'jwk', + { ...jwk, alg: `NODE-DSA-${hash}` }, + { name, hash }, + extractable, + privateUsages) + ]); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.extractable, extractable); + assert.strictEqual(privateKey.extractable, extractable); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(publicKey.algorithm.modulusLength, size); + assert.strictEqual(privateKey.algorithm.modulusLength, size); + + if (extractable) { + const [ + pubJwk, + pvtJwk + ] = await Promise.all([ + subtle.exportKey('jwk', publicKey), + subtle.exportKey('jwk', privateKey) + ]); + + assert.strictEqual(pubJwk.kty, 'DSA'); + assert.strictEqual(pvtJwk.kty, 'DSA'); + assert.strictEqual(pubJwk.y, jwk.y); + assert.strictEqual(pvtJwk.y, jwk.y); + assert.strictEqual(pubJwk.p, jwk.p); + assert.strictEqual(pvtJwk.p, jwk.p); + assert.strictEqual(pubJwk.q, jwk.q); + assert.strictEqual(pvtJwk.q, jwk.q); + assert.strictEqual(pubJwk.g, jwk.g); + assert.strictEqual(pvtJwk.g, jwk.g); + assert.strictEqual(pvtJwk.x, jwk.x); + assert.strictEqual(pubJwk.x, undefined); + } else { + await assert.rejects( + subtle.exportKey('jwk', publicKey), { + message: /key is not extractable/ + }); + await assert.rejects( + subtle.exportKey('jwk', privateKey), { + message: /key is not extractable/ + }); + } +} + +// combinations to test +const testVectors = [ + { + name: 'NODE-DSA', + privateUsages: ['sign'], + publicUsages: ['verify'] + } +]; + +(async function() { + const variations = []; + sizes.forEach((size) => { + hashes.forEach((hash) => { + [true, false].forEach((extractable) => { + testVectors.forEach((vector) => { + variations.push(testImportSpki(vector, size, hash, extractable)); + variations.push(testImportPkcs8(vector, size, hash, extractable)); + variations.push(testImportJwk(vector, size, hash, extractable)); + }); + }); + }); + }); + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js new file mode 100644 index 00000000000000..1a17c794eed53a --- /dev/null +++ b/test/parallel/test-webcrypto-export-import-ec.js @@ -0,0 +1,234 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const curves = ['P-384', 'P-521']; + +const keyData = { + 'P-521': { + spki: Buffer.from( + '30819b301006072a8648ce3d020106052b8104002303818600040156f479f8df' + + '1e20a7ffc04ce420c3e154ae251996bee42f034b84d41b743f34e45f311b813a' + + '9cdec8cda59bbbbd31d460b3292521e7c1b722e5667c03db2fae753f01501736' + + 'cfe247394320d8e4afc2fd39b5a9331061b81e2241282b9e17891822b5b79e05' + + '2f4597b59643fd39379c51bd5125c4f48bc3f025ce3cd36953286ccb38fb', + 'hex'), + pkcs8: Buffer.from( + '3081ee020100301006072a8648ce3d020106052b810400230481d63081d3020' + + '101044200f408758368ba930f30f76ae054fe5cd2ce7fda2c9f76a6d436cf75' + + 'd66c440bfe6331c7c172a12478193c8251487bc91263fa50217f85ff636f59c' + + 'd546e3ab483b4a1818903818600040156f479f8df1e20a7ffc04ce420c3e154' + + 'ae251996bee42f034b84d41b743f34e45f311b813a9cdec8cda59bbbbd31d46' + + '0b3292521e7c1b722e5667c03db2fae753f01501736cfe247394320d8e4afc2' + + 'fd39b5a9331061b81e2241282b9e17891822b5b79e052f4597b59643fd39379' + + 'c51bd5125c4f48bc3f025ce3cd36953286ccb38fb', 'hex'), + jwk: { + kty: 'EC', + crv: 'P-521', + x: 'AVb0efjfHiCn_8BM5CDD4VSuJRmWvuQvA0uE1Bt0PzTkXzEbgTqc3sjN' + + 'pZu7vTHUYLMpJSHnwbci5WZ8A9svrnU_', + y: 'AVAXNs_iRzlDINjkr8L9ObWpMxBhuB4iQSgrnheJGCK1t54FL0W' + + 'XtZZD_Tk3nFG9USXE9IvD8CXOPNNpUyhsyzj7', + d: 'APQIdYNoupMPMPdq4FT-XNLOf9osn3am1DbPddZsRAv-YzHHw' + + 'XKhJHgZPIJRSHvJEmP6UCF_hf9jb1nNVG46tIO0' + } + }, + 'P-384': { + spki: Buffer.from( + '3076301006072a8648ce3d020106052b8104002203620004219c14d66617b36e' + + 'c6d8856b385b73a74d344fd8ae75ef046435dda54e3b44bd5fbdebd1d08dd69e' + + '2d7dc1dc218cb435bd28138cc778337a842f6bd61b240e74249f24667c2a5810' + + 'a76bfc28e0335f88a6501dec01976da85afb00869cb6ace8', 'hex'), + pkcs8: Buffer.from( + '3081b6020100301006072a8648ce3d020106052b8104002204819e30819b0201' + + '0104304537b5990784d3c2d22e96a8f92fa1aa492ee873e576a41582e144183c' + + '9888d10e6b9eb4ced4b2cc4012e4ac5ea84073a16403620004219c14d66617b3' + + '6ec6d8856b385b73a74d344fd8ae75ef046435dda54e3b44bd5fbdebd1d08dd6' + + '9e2d7dc1dc218cb435bd28138cc778337a842f6bd61b240e74249f24667c2a58' + + '10a76bfc28e0335f88a6501dec01976da85afb00869cb6ace8', 'hex'), + jwk: { + kty: 'EC', + crv: 'P-384', + x: 'IZwU1mYXs27G2IVrOFtzp000T9iude8EZDXdpU47RL1fvevR0I3Wni19wdwhjLQ1', + y: 'vSgTjMd4M3qEL2vWGyQOdCSfJGZ8KlgQp2v8KOAzX4imUB3sAZdtqFr7AIactqzo', + d: 'RTe1mQeE08LSLpao-S-hqkku6HPldqQVguFEGDyYiNEOa560ztSyzEAS5KxeqEBz' + } + }, +}; + +const testVectors = [ + { + name: 'ECDSA', + privateUsages: ['sign'], + publicUsages: ['verify'] + }, + { + name: 'ECDH', + privateUsages: ['deriveKey', 'deriveBits'], + publicUsages: [] + } +]; + +async function testImportSpki({ name, publicUsages }, namedCurve, extractable) { + const key = await subtle.importKey( + 'spki', + keyData[namedCurve].spki, + { name, namedCurve }, + extractable, + publicUsages); + assert.strictEqual(key.type, 'public'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, publicUsages); + assert.deepStrictEqual(key.algorithm.name, name); + assert.deepStrictEqual(key.algorithm.namedCurve, namedCurve); + + if (extractable) { + // Test the roundtrip + const spki = await subtle.exportKey('spki', key); + assert.strictEqual( + Buffer.from(spki).toString('hex'), + keyData[namedCurve].spki.toString('hex')); + } else { + await assert.rejects( + subtle.exportKey('spki', key), { + message: /key is not extractable/ + }); + } + + // Bad usage + await assert.rejects( + subtle.importKey( + 'spki', + keyData[namedCurve].spki, + { name, namedCurve }, + extractable, + ['wrapKey']), + { message: /Unsupported key usage/ }); +} + +async function testImportPkcs8( + { name, privateUsages }, + namedCurve, + extractable) { + const key = await subtle.importKey( + 'pkcs8', + keyData[namedCurve].pkcs8, + { name, namedCurve }, + extractable, + privateUsages); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, privateUsages); + assert.deepStrictEqual(key.algorithm.name, name); + assert.deepStrictEqual(key.algorithm.namedCurve, namedCurve); + + if (extractable) { + // Test the roundtrip + const pkcs8 = await subtle.exportKey('pkcs8', key); + assert.strictEqual( + Buffer.from(pkcs8).toString('hex'), + keyData[namedCurve].pkcs8.toString('hex')); + } else { + await assert.rejects( + subtle.exportKey('pkcs8', key), { + message: /key is not extractable/ + }); + } +} + +async function testImportJwk( + { name, publicUsages, privateUsages }, + namedCurve, + extractable) { + + const jwk = keyData[namedCurve].jwk; + + const [ + publicKey, + privateKey + ] = await Promise.all([ + subtle.importKey( + 'jwk', + { + kty: jwk.kty, + crv: jwk.crv, + x: jwk.x, + y: jwk.y, + }, + { name, namedCurve }, + extractable, publicUsages), + subtle.importKey( + 'jwk', + jwk, + { name, namedCurve }, + extractable, + privateUsages) + ]); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.extractable, extractable); + assert.strictEqual(privateKey.extractable, extractable); + assert.deepStrictEqual(publicKey.usages, publicUsages); + assert.deepStrictEqual(privateKey.usages, privateUsages); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(publicKey.algorithm.namedCurve, namedCurve); + assert.strictEqual(privateKey.algorithm.namedCurve, namedCurve); + + if (extractable) { + // Test the round trip + const [ + pubJwk, + pvtJwk + ] = await Promise.all([ + subtle.exportKey('jwk', publicKey), + subtle.exportKey('jwk', privateKey) + ]); + + assert.deepStrictEqual(pubJwk.key_ops, publicUsages); + assert.strictEqual(pubJwk.ext, true); + assert.strictEqual(pubJwk.kty, 'EC'); + assert.strictEqual(pubJwk.x, jwk.x); + assert.strictEqual(pubJwk.y, jwk.y); + assert.strictEqual(pubJwk.crv, jwk.crv); + + assert.deepStrictEqual(pvtJwk.key_ops, privateUsages); + assert.strictEqual(pvtJwk.ext, true); + assert.strictEqual(pvtJwk.kty, 'EC'); + assert.strictEqual(pvtJwk.x, jwk.x); + assert.strictEqual(pvtJwk.y, jwk.y); + assert.strictEqual(pvtJwk.crv, jwk.crv); + assert.strictEqual(pvtJwk.d, jwk.d); + } else { + await assert.rejects( + subtle.exportKey('jwk', publicKey), { + message: /key is not extractable/ + }); + await assert.rejects( + subtle.exportKey('jwk', privateKey), { + message: /key is not extractable/ + }); + } +} + +(async function() { + const tests = []; + testVectors.forEach((vector) => { + curves.forEach((namedCurve) => { + [true, false].forEach((extractable) => { + tests.push(testImportSpki(vector, namedCurve, extractable)); + tests.push(testImportPkcs8(vector, namedCurve, extractable)); + tests.push(testImportJwk(vector, namedCurve, extractable)); + }); + }); + }); + + await Promise.all(tests); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-export-import-rsa.js b/test/parallel/test-webcrypto-export-import-rsa.js new file mode 100644 index 00000000000000..1caf60f156c945 --- /dev/null +++ b/test/parallel/test-webcrypto-export-import-rsa.js @@ -0,0 +1,480 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const sizes = [1024, 2048, 4096]; + +const hashes = [ + 'SHA-1', + 'SHA-256', + 'SHA-384', + 'SHA-512' +]; + +const keyData = { + 1024: { + spki: Buffer.from( + '30819f300d06092a864886f70d010101050003818d0030818902818100cd99f8b111' + + '9f8d0a2ce7ac8bfd0cb547d348f931cc9c5ca79fde20e51c40eb01ab261e01253df1' + + 'e88f71d086e94b7abe77839103a476bee0cc87c743151afd4431fa5d8fa051271cf5' + + '4e49cf7500d8a9957ec09b9d43ef70098c57f10d03bfd31748af563b881687720d3c' + + '7b10a1cd553ac71d296b6edeeca5b99c8afb36dd970203010001', 'hex'), + pkcs8: Buffer.from( + '30820278020100300d06092a864886f70d0101010500048202623082025e02010002' + + '818100cd99f8b1119f8d0a2ce7ac8bfd0cb547d348f931cc9c5ca79fde20e51c40eb' + + '01ab261e01253df1e88f71d086e94b7abe77839103a476bee0cc87c743151afd4431' + + 'fa5d8fa051271cf54e49cf7500d8a9957ec09b9d43ef70098c57f10d03bfd31748af' + + '563b881687720d3c7b10a1cd553ac71d296b6edeeca5b99c8afb36dd970203010001' + + '02818062a20afc6747f3917e19665d81f826bf5e4d13bf2039a2f9876838bfb0de33' + + 'df890bb0393c748b28d627f3b1c519c0b8befd0f048051b72080fe62497c468658e4' + + '5508e5d206958d7a9318a62a39da7df0e6e8f951912c0676ed65cd04b5685517602e' + + 'a9aed56e22ab59c414120108f15d201390f8b72060f065eff7def97501024100f41a' + + 'c08392f5cdfa863ee5890ee0c2057f939ad65dace23762ce1968dfb230f9538f0592' + + '10f3b4aa77e3119730d958171e024999b55ca3a4f172424298462a79024100d79ee3' + + '0c9d586b99e642f4cf6e12803c078c5a88310b26904e406ba77d2910a77a986481df' + + 'ce61aabe01224f2cddfecc757a4cf944a9699814a13e28ff65448f024100a9d77f41' + + '4cdc681fba8e42a8d5483ed712880200cb16c22325451f5adfe21cbf2d8b62a5d9d3' + + 'a74dc0b2a6079b3e6e534f56ea1cdf9a80660074ae73a57d948902410084d45fc0e4' + + 'a994d7e12efc4b50dedadaa037c989bed4c4b3ff50d640feecae52ce46551c60f86d' + + 'd85666b2711e0dc02aca70463d051c6c6d80bff8601f3d8e67024100cdba49400862' + + '9ebc526d52b1050d846461540f67b75825db009458a64f07550e40039d8e84a4e270' + + 'ec9eda11079eb82914acc2f22ce74ec086dc5324bf0723e1', 'hex'), + jwk: { + kty: 'RSA', + n: 'zZn4sRGfjQos56yL_Qy1R9NI-THMnFynn94g5RxA6wGrJh4BJT3x6I9x0IbpS3q-d' + + '4ORA6R2vuDMh8dDFRr9RDH6XY-gUScc9U5Jz3UA2KmVfsCbnUPvcAmMV_ENA7_TF0' + + 'ivVjuIFodyDTx7EKHNVTrHHSlrbt7spbmcivs23Zc', + e: 'AQAB', + d: 'YqIK_GdH85F-GWZdgfgmv15NE78gOaL5h2g4v7DeM9-JC7A5PHSLKNYn87HFGcC4v' + + 'v0PBIBRtyCA_mJJfEaGWORVCOXSBpWNepMYpio52n3w5uj5UZEsBnbtZc0EtWhVF2' + + 'Auqa7VbiKrWcQUEgEI8V0gE5D4tyBg8GXv9975dQE', + p: '9BrAg5L1zfqGPuWJDuDCBX-TmtZdrOI3Ys4ZaN-yMPlTjwWSEPO0qnfjEZcw2VgXH' + + 'gJJmbVco6TxckJCmEYqeQ', + q: '157jDJ1Ya5nmQvTPbhKAPAeMWogxCyaQTkBrp30pEKd6mGSB385hqr4BIk8s3f7Md' + + 'XpM-USpaZgUoT4o_2VEjw', + dp: 'qdd_QUzcaB-6jkKo1Ug-1xKIAgDLFsIjJUUfWt_iHL8ti2Kl2dOnTcCypgebPm5T' + + 'T1bqHN-agGYAdK5zpX2UiQ', + dq: 'hNRfwOSplNfhLvxLUN7a2qA3yYm-1MSz_1DWQP7srlLORlUcYPht2FZmsnEeDcAq' + + 'ynBGPQUcbG2Av_hgHz2OZw', + qi: 'zbpJQAhinrxSbVKxBQ2EZGFUD2e3WCXbAJRYpk8HVQ5AA52OhKTicOye2hEHnrgp' + + 'FKzC8iznTsCG3FMkvwcj4Q' + } + }, + + 2048: { + spki: Buffer.from( + '30820122300d06092a864886f70d01010105000382010f003082010a0282010100d9' + + '8580eb2d1772f4a476bc5404bee60d9a3c2acbbcf24a74754d9f5a6812388f9e3f26' + + '0ad81687ddb366f8da559462b397f1c097896d0df6e6de31c04f8d47cd15600d11be' + + '4ec4e6309e200416257fabba8bbed33ab0c165da3c9b1fcec2c4e9e52aca6359a7cf' + + '54d5275b4486bf01a2b45f04fae20b717d01a794570728815297b2b7f22be00ef302' + + '3813ca87b7e0be8343335cfaf0769e366cf9256cf44239458bb47ebd6b32f0168980' + + '67009273f79d45b85b9f33f57318dfc5af981aa2964834e7f5b33012d369646a6738' + + 'b22bca55e59066f1e69f6a69f1eedecce881b7423fd44dfc7a7c989c426741d8813c' + + '3fcdc024b53d84290a3beda3c83872cafd0203010001', 'hex'), + pkcs8: Buffer.from( + '308204be020100300d06092a864886f70d0101010500048204a8308204a402010002' + + '82010100d98580eb2d1772f4a476bc5404bee60d9a3c2acbbcf24a74754d9f5a6812' + + '388f9e3f260ad81687ddb366f8da559462b397f1c097896d0df6e6de31c04f8d47cd' + + '15600d11be4ec4e6309e200416257fabba8bbed33ab0c165da3c9b1fcec2c4e9e52a' + + 'ca6359a7cf54d5275b4486bf01a2b45f04fae20b717d01a794570728815297b2b7f2' + + '2be00ef3023813ca87b7e0be8343335cfaf0769e366cf9256cf44239458bb47ebd6b' + + '32f016898067009273f79d45b85b9f33f57318dfc5af981aa2964834e7f5b33012d3' + + '69646a6738b22bca55e59066f1e69f6a69f1eedecce881b7423fd44dfc7a7c989c42' + + '6741d8813c3fcdc024b53d84290a3beda3c83872cafd0203010001028201005ad2a7' + + '758aaa53d15a2a49903b3b0a0b7beecb5fae50ec4d9bfd01205a7be129f6451fb93f' + + '6888ea44d225ede3f5c5107fcced41589c344c7731274cc8ea90a44cdc82187a81a1' + + '2d0bf7ba1e7ab0c5920a9df6db739201ee69250d1046e0841fb5141cd546c60e87b9' + + '48698f3f43d986fa11029f4e6ac0c41540c76b5f0dc690d445ffe2bf792e1e67996f' + + 'aba68958e5568e42ee881848f81b2b7465d76327f6d46ff184a907fc1368ace90828' + + 'e3ac2a2f248622d661e4b3d7c104de81a5013bd8ab32116444c7e272af31065f817a' + + 'bdc6981171467968334b12d21bed5d57683140707ac6223dd107067916bf5f97f87c' + + '07578f2d7b168099c582c4f4a4e1f102818100fcdf6d12d3df7c92438ad38e9c9966' + + 'c0c0ec81150e9e1ce40cb845efa5c3d109ecf0583b8f68c7c57c53a8c9a6f99e9c43' + + '9e0f749be053ac70bb01e17ffeafafd6d6246fda556d21e49dc03dc3cf19889af486' + + '451267e1ac8310a846031e0562a22f58bf63f17f5d24044861e307463c8d19964daa' + + 'c956811d603c29e7bec86b02818100dc36288ccc4f0795f128e5ed0d0376ac4c3d89' + + '08fd48df77bd1357c7033dc52d6f123ae079be902e8fe107810a9a188c60f6d4e0e8' + + '90436206bca711e0d7a0b6f984aef9154e8a3bbab8ef0a47922ebdcea5393226f1e6' + + '39a94d4ce5352db85716c25e3044f6abff49c519400d843878f164c5f3ab54f62056' + + '3737d8794034370281806dddbd0c2315c48fdfdc9f5224e3d96b01e73fa62075bde3' + + 'af4b18c7a863cd9cdc5f0856c8562405bfa0b182fb9314c09bf83e8ad176c3a3f64e' + + 'a9e089b5e42b27d25e7e62841f284ca5e5727072b88b4b97d606889aadc84021aa9a' + + 'd09be88714243210e5a1754ec8693bf19babfb6e2f77e07fda2623f97103f0dfdc1a' + + '5e05028181009571bbbb31bc406da5a817c1f41ef19ea46eee5cc76779208d945ef1' + + '94658b36f635ecf702282d392c338f2027cdc3f320aae2756fded79be2ee8c83398f' + + '9c661097d716fb3abddd232ef62a87bfd130c6d8a2244301cf383a8957320610ed15' + + '4d40c32306ea507783dcdaf1f93a4e08e5e979dd8fdcacdbed26b42398c5d5a90281' + + '81009d221bcb65a15be795dfffbab2afa85dc2a3ab65ba5f6e26fa172612d5572129' + + 'bb120015ca4446ec3fdb9ec980a661d2aad23850511898f07c148716095cd1bd60d6' + + '31464ac89b524660bd465952d2e57d8740b7c3f3db79492b16b87a5cd1767e13526e' + + 'f66d79c691e2c7f2528b69652c29ba210a5e679d23b21a680cbf0d07', 'hex'), + jwk: { + kty: 'RSA', + n: '2YWA6y0XcvSkdrxUBL7mDZo8Ksu88kp0dU2fWmgSOI-ePyYK2BaH3bNm-NpVlGKzl' + + '_HAl4ltDfbm3jHAT41HzRVgDRG-TsTmMJ4gBBYlf6u6i77TOrDBZdo8mx_OwsTp5S' + + 'rKY1mnz1TVJ1tEhr8BorRfBPriC3F9AaeUVwcogVKXsrfyK-AO8wI4E8qHt-C-g0M' + + 'zXPrwdp42bPklbPRCOUWLtH69azLwFomAZwCSc_edRbhbnzP1cxjfxa-YGqKWSDTn' + + '9bMwEtNpZGpnOLIrylXlkGbx5p9qafHu3szogbdCP9RN_Hp8mJxCZ0HYgTw_zcAkt' + + 'T2EKQo77aPIOHLK_Q', + e: 'AQAB', + d: 'WtKndYqqU9FaKkmQOzsKC3vuy1-uUOxNm_0BIFp74Sn2RR-5P2iI6kTSJe3j9cUQf' + + '8ztQVicNEx3MSdMyOqQpEzcghh6gaEtC_e6HnqwxZIKnfbbc5IB7mklDRBG4IQftR' + + 'Qc1UbGDoe5SGmPP0PZhvoRAp9OasDEFUDHa18NxpDURf_iv3kuHmeZb6umiVjlVo5' + + 'C7ogYSPgbK3Rl12Mn9tRv8YSpB_wTaKzpCCjjrCovJIYi1mHks9fBBN6BpQE72Ksy' + + 'EWREx-JyrzEGX4F6vcaYEXFGeWgzSxLSG-1dV2gxQHB6xiI90QcGeRa_X5f4fAdXj' + + 'y17FoCZxYLE9KTh8Q', + p: '_N9tEtPffJJDitOOnJlmwMDsgRUOnhzkDLhF76XD0Qns8Fg7j2jHxXxTqMmm-Z6cQ' + + '54PdJvgU6xwuwHhf_6vr9bWJG_aVW0h5J3APcPPGYia9IZFEmfhrIMQqEYDHgVioi' + + '9Yv2Pxf10kBEhh4wdGPI0Zlk2qyVaBHWA8Kee-yGs', + q: '3DYojMxPB5XxKOXtDQN2rEw9iQj9SN93vRNXxwM9xS1vEjrgeb6QLo_hB4EKmhiMY' + + 'PbU4OiQQ2IGvKcR4NegtvmErvkVToo7urjvCkeSLr3OpTkyJvHmOalNTOU1LbhXFs' + + 'JeMET2q_9JxRlADYQ4ePFkxfOrVPYgVjc32HlANDc', + dp: 'bd29DCMVxI_f3J9SJOPZawHnP6Ygdb3jr0sYx6hjzZzcXwhWyFYkBb-gsYL7kxTA' + + 'm_g-itF2w6P2TqngibXkKyfSXn5ihB8oTKXlcnByuItLl9YGiJqtyEAhqprQm-iH' + + 'FCQyEOWhdU7IaTvxm6v7bi934H_aJiP5cQPw39waXgU', + dq: 'lXG7uzG8QG2lqBfB9B7xnqRu7lzHZ3kgjZRe8ZRlizb2Nez3AigtOSwzjyAnzcPz' + + 'IKridW_e15vi7oyDOY-cZhCX1xb7Or3dIy72Koe_0TDG2KIkQwHPODqJVzIGEO0V' + + 'TUDDIwbqUHeD3Nrx-TpOCOXped2P3Kzb7Sa0I5jF1ak', + qi: 'nSIby2WhW-eV3_-6sq-oXcKjq2W6X24m-hcmEtVXISm7EgAVykRG7D_bnsmApmHS' + + 'qtI4UFEYmPB8FIcWCVzRvWDWMUZKyJtSRmC9RllS0uV9h0C3w_PbeUkrFrh6XNF2' + + 'fhNSbvZtecaR4sfyUotpZSwpuiEKXmedI7IaaAy_DQc' + } + }, + + 4096: { + spki: Buffer.from( + '30820222300d06092a864886f70d01010105000382020f003082020a0282020100da' + + 'aaf64cbd9cd8999bb0dd0e2c846768007f64a6f5f8687d1f4a9be25ac1b836aa916f' + + 'de14fc13f8922cbe7349bc34fb04b279eed4cc223e7a64cb6fe9e7d249359293d30e' + + 'a16d89d4afe212b7ad67671e801fda457eea4158e7a05b33f54d3604a7c02144f4a3' + + 'f2bb6fd1b4f1dd6bac0528862fd255087039ba1d83b05d74c6ca526cfbd103484b8f' + + '3b2cde385945679fd3a013d6ad4d850044dba44f40ee41bdc9f8adb492c4ee56e8d7' + + '6d27a5a210e62e86ea946a22e6c63fe78f10b3d06d1664369c6b841cd076cdd959e4' + + '4bc4a9b505559d906e81ba8d7768a2ceaa73076052f0218f51f3d7436089cfd116a2' + + 'fb6cd0e820eccda7aea1740df9bb16f0b9aca0675ea2931a0f8fb79362e77586b932' + + '40281e1b0d9884288a204e9ea2cfd4e5d2fb587443e5a4a4933b205ed9c5f295664a' + + 'db2e7f441c740a02f9e7827b1d2d493811c3d02d193cfc62bd6d1900fd97fe7cd330' + + '179c4ea39abc11450ebc10403bbe8846a2fded9c6f291b283fcdcc5e0032ed3e57d3' + + '735b44c26877486ae2a030a58a86028a99b526f93078480ff5e30fa440bc4a0454d5' + + '53434957b5485e2e36c1fcbc0ecf1c529f83a8eea8911ce61b7e975d0560447e42ae' + + '9b657b14da835c7c4e522c378b4d69b18879b12b4d0cf0004c14857981490fa0c896' + + '725f3b3ba5f0cc0d9c86c204469ed56fe567d8ef8410b897cefee53e173a7d3190d0' + + 'd70203010001', 'hex'), + pkcs8: Buffer.from( + '30820944020100300d06092a864886f70d01010105000482092e3082092a02010002' + + '82020100daaaf64cbd9cd8999bb0dd0e2c846768007f64a6f5f8687d1f4a9be25ac1' + + 'b836aa916fde14fc13f8922cbe7349bc34fb04b279eed4cc223e7a64cb6fe9e7d249' + + '359293d30ea16d89d4afe212b7ad67671e801fda457eea4158e7a05b33f54d3604a7' + + 'c02144f4a3f2bb6fd1b4f1dd6bac0528862fd255087039ba1d83b05d74c6ca526cfb' + + 'd103484b8f3b2cde385945679fd3a013d6ad4d850044dba44f40ee41bdc9f8adb492' + + 'c4ee56e8d76d27a5a210e62e86ea946a22e6c63fe78f10b3d06d1664369c6b841cd0' + + '76cdd959e44bc4a9b505559d906e81ba8d7768a2ceaa73076052f0218f51f3d74360' + + '89cfd116a2fb6cd0e820eccda7aea1740df9bb16f0b9aca0675ea2931a0f8fb79362' + + 'e77586b93240281e1b0d9884288a204e9ea2cfd4e5d2fb587443e5a4a4933b205ed9' + + 'c5f295664adb2e7f441c740a02f9e7827b1d2d493811c3d02d193cfc62bd6d1900fd' + + '97fe7cd330179c4ea39abc11450ebc10403bbe8846a2fded9c6f291b283fcdcc5e00' + + '32ed3e57d3735b44c26877486ae2a030a58a86028a99b526f93078480ff5e30fa440' + + 'bc4a0454d553434957b5485e2e36c1fcbc0ecf1c529f83a8eea8911ce61b7e975d05' + + '60447e42ae9b657b14da835c7c4e522c378b4d69b18879b12b4d0cf0004c14857981' + + '490fa0c896725f3b3ba5f0cc0d9c86c204469ed56fe567d8ef8410b897cefee53e17' + + '3a7d3190d0d702030100010282020100b973d15c185c139f8359a6c144a42e871814' + + 'f32a5ee604c849679f7983fb53de991eabbfb010726798a1760c94f69800646571e0' + + '4a7dae754a9c7da536bdb3acff50872ab2f7d9ccd1a3319b2a4858b02e3fffc3c0b8' + + 'f8b7df4ce2c536f5ce3c080ab57a01df71c4858f3a4db9eb4e4c203bd4426ea24b7b' + + 'd299b43a61b3813caf8ee47b5532f17793cc5e2b41a304a7f3f7298669c5a53f2d91' + + '38aecbc087d11dc353b30eb883689830f5b3cfb23c17150154cf527c0989ab8dbb37' + + 'acb4b40a30b9614f9c27f9c01b624dfa5d129d8248d2736024847465e160ea4f59f3' + + '598761fc35486122e229292d90f3bda2f32b45888fb68cdf865d26f5247d2e5d305e' + + 'd7279c39565dcfcc486a70d7cbe6501489e0f22192216cbcb9fe75bdf052403cbaf7' + + 'be8aaa9f934b319465ae8215b1d379069990e6a6b59b5ee8020477ec2385fddf0e1e' + + 'c739d71ffb5aa713e79a36e1554411ea9e3532f3b695c1d63cbc062602c8a1e8c11e' + + '99e7dd398c374523159922eeaf41fdd2777d7874997f43cc0942d2c8a5d4d8023e13' + + '0fab4db7f77fe08a29d0aae3249eb06f80ac4649f194ac32ae7e50b1eb5d5966544c' + + 'dd1ed8317d8e232d60e03ca13f30558f144cb66f0f9c8b379b71e2f8ef82fcf1c5f7' + + '7c3d27c5aa774c88c3b4a96af0ea6572cf0ba0aa8bc2bb3016725440971ed463d5b0' + + '6a4fe87fc599850838d253436a7ce76002910282010100f4dad7c2ae2463d90104ec' + + '0ba0565541ce24248fcd6ca6bf5bd14b75075121b32c6591d72775c3511f6f24071a' + + '691ef95b0202ed7e8de799d5b564eadbc072b3d7e527d46b0937dc88e9ed1c4a6106' + + '161a2f9653525fba921626b0e7ffa6c7dfd9568e382bc719f7f97a3b8e981431930d' + + '84f9cbfb9274605851e82d6a64bb634920cb861edf64b3b38051f21955897d6099f0' + + 'e05614ce181ac5e9a49e32de67c5d39065b6cdc93317e77de5823d8bccc3f34526b9' + + 'bb30f98c6b8927ea150d2b18706c6d0f1939377f2898eee360569d72233436268c55' + + '2a7735632385d0f041ab0847fff3f8b0a611b25c3ecb389e1fa9df7b0776d8a68453' + + '3e70a063f4841d0282010100e49ef9f3f35e2abd573d988bc57a216104278742dbe1' + + '0b46675c730a08e10502dc201793386fed6230ae7acf6d98bb7ddcba497f2a5227e4' + + 'a30cbc24476b34ebdfc8072606a71c9e1ad57eba5a98852c359c3d825ca3031b23b9' + + '8d70ecf6d26b4bf5217e86d72901f4dc245d16e8323e448d99763e01a7c5ca71bbc4' + + 'bafba18042d391678545cf9b75414cfb7d2be069ab061dfe1f6f90059ea6b48fa3cd' + + 'd497070b32ea52258f4b687c6145dcf6ca2d1928dc175c747072ccc68c306fbf351c' + + '0986ea5aa8f36c4bc563a2ad1fc261e0b84ce3aac76a810e4deae726c0c5e9ae96f0' + + '37fcf11b61a931317309da41fd0efdd95b8d2c4420f7dbc71f2dd4442e8302820101' + + '00e18ec7bb9b580272e1317b90aa3f5d82a5373e470a61d0a9ef173a7fb021d8fd89' + + '2477d8cf8cf8443ec4cf578bc8d2b3ba567c03f3d51d48e549989191a61304011a24' + + '3ad5ef43fa7055ae0ba5a9034651110d55ec482b42700d6c620b6bc42c3db6328524' + + '2ee18941d48c10ab9fce9b3c9506d81603b01920c33332c313d05b81fe27fe816a21' + + '06399137ebe1d29e395547fa516e7af3efd89a00c598c61b835505b3bb3f4f0acd7a' + + '73d1d21ecc3b8081f213fdbc92e866ba2845ccf32239633dbc32e5b446f4225f8d32' + + '74be18fd3144f7911d611d5d47255194e6205b7d37c12a7bc919223af880cce19526' + + 'f81d11e616eceacf5c7ce8e116600220921b310282010100813e223db7f21f2544c1' + + '6c906f85f882b8ef83b6d748a4b01b549730300ecd5f6d83b2f0263298372f20240b' + + '4980d35576c7d52ecf84fc4a73a68a61d402163bd619657928bfa61cf73c8454e34c' + + '5fd4bb45e53be214c177c13d6f694c7cc83da20624f63b523d3b7eea48a05b87ce87' + + '8707a99ebfb4fddc81f2c3dc967c1433c713859ac92bcb0eae3dc9404ee5d40ac885' + + '3fc55e8e1a14233948cfff2128326ce7f6d3a2b6db081d3c5b5d3c6a43a73516f53d' + + '3ba613bfc265e7f0a5eba9217d7d48d511b7f31beeadc1d42f251b6207ae67f22ea3' + + 'd5eb793ef787dfe8c28f5182e193dbd5c7e2f70d6664467f9188bd16f87b996fb657' + + '88664c09037bbbf30282010024799529bd73c16e62451e9109e7b16278767e663edc' + + '3acf49d33c0f186bd05f1d6b28beb6546a11d9c6d21be9e399fc80b52c91659c07d1' + + '1795424e6d918a0df1aec6031ade0ff178b036be6150d763313ecc87e2208d66fb20' + + '986c71ed3b8e1eb9c3879101567338fdd7baddcac424e376b1823c3b38bec69d8e12' + + '602bdac7962aae2cc641678ba7b12e1a9bf8d1389bd1cc2a59e0d44b50876acb0451' + + 'b55580f749862930b7397f1cea1af4b19f715af97820f8864f637b9badc9b9d8a620' + + '98b5069a7612b5f56a1925927610d71e5360239a5d000d05ce9c81937657f89b3187' + + '07279de2ab6010707aad3a9113065a0bdd6dd010fbbc12786aaa8f954fc0', 'hex'), + jwk: { + kty: 'RSA', + n: '2qr2TL2c2JmbsN0OLIRnaAB_ZKb1-Gh9H0qb4lrBuDaqkW_eFPwT-JIsvnNJvDT7B' + + 'LJ57tTMIj56ZMtv6efSSTWSk9MOoW2J1K_iEretZ2cegB_aRX7qQVjnoFsz9U02BK' + + 'fAIUT0o_K7b9G08d1rrAUohi_SVQhwObodg7BddMbKUmz70QNIS487LN44WUVnn9O' + + 'gE9atTYUARNukT0DuQb3J-K20ksTuVujXbSelohDmLobqlGoi5sY_548Qs9BtFmQ2' + + 'nGuEHNB2zdlZ5EvEqbUFVZ2QboG6jXdoos6qcwdgUvAhj1Hz10Ngic_RFqL7bNDoI' + + 'OzNp66hdA35uxbwuaygZ16ikxoPj7eTYud1hrkyQCgeGw2YhCiKIE6eos_U5dL7WH' + + 'RD5aSkkzsgXtnF8pVmStsuf0QcdAoC-eeCex0tSTgRw9AtGTz8Yr1tGQD9l_580zA' + + 'XnE6jmrwRRQ68EEA7vohGov3tnG8pGyg_zcxeADLtPlfTc1tEwmh3SGrioDClioYC' + + 'ipm1JvkweEgP9eMPpEC8SgRU1VNDSVe1SF4uNsH8vA7PHFKfg6juqJEc5ht-l10FY' + + 'ER-Qq6bZXsU2oNcfE5SLDeLTWmxiHmxK00M8ABMFIV5gUkPoMiWcl87O6XwzA2chs' + + 'IERp7Vb-Vn2O-EELiXzv7lPhc6fTGQ0Nc', + e: 'AQAB', + d: 'uXPRXBhcE5-DWabBRKQuhxgU8ype5gTISWefeYP7U96ZHqu_sBByZ5ihdgyU9pgAZ' + + 'GVx4Ep9rnVKnH2lNr2zrP9Qhyqy99nM0aMxmypIWLAuP__DwLj4t99M4sU29c48CA' + + 'q1egHfccSFjzpNuetOTCA71EJuokt70pm0OmGzgTyvjuR7VTLxd5PMXitBowSn8_c' + + 'phmnFpT8tkTiuy8CH0R3DU7MOuINomDD1s8-yPBcVAVTPUnwJiauNuzestLQKMLlh' + + 'T5wn-cAbYk36XRKdgkjSc2AkhHRl4WDqT1nzWYdh_DVIYSLiKSktkPO9ovMrRYiPt' + + 'ozfhl0m9SR9Ll0wXtcnnDlWXc_MSGpw18vmUBSJ4PIhkiFsvLn-db3wUkA8uve-iq' + + 'qfk0sxlGWughWx03kGmZDmprWbXugCBHfsI4X93w4exznXH_tapxPnmjbhVUQR6p4' + + '1MvO2lcHWPLwGJgLIoejBHpnn3TmMN0UjFZki7q9B_dJ3fXh0mX9DzAlC0sil1NgC' + + 'PhMPq02393_giinQquMknrBvgKxGSfGUrDKuflCx611ZZlRM3R7YMX2OIy1g4DyhP' + + 'zBVjxRMtm8PnIs3m3Hi-O-C_PHF93w9J8Wqd0yIw7SpavDqZXLPC6Cqi8K7MBZyVE' + + 'CXHtRj1bBqT-h_xZmFCDjSU0NqfOdgApE', + p: '9NrXwq4kY9kBBOwLoFZVQc4kJI_NbKa_W9FLdQdRIbMsZZHXJ3XDUR9vJAcaaR75W' + + 'wIC7X6N55nVtWTq28Bys9flJ9RrCTfciOntHEphBhYaL5ZTUl-6khYmsOf_psff2V' + + 'aOOCvHGff5ejuOmBQxkw2E-cv7knRgWFHoLWpku2NJIMuGHt9ks7OAUfIZVYl9YJn' + + 'w4FYUzhgaxemknjLeZ8XTkGW2zckzF-d95YI9i8zD80Umubsw-YxriSfqFQ0rGHBs' + + 'bQ8ZOTd_KJju42BWnXIjNDYmjFUqdzVjI4XQ8EGrCEf_8_iwphGyXD7LOJ4fqd97B' + + '3bYpoRTPnCgY_SEHQ', + q: '5J758_NeKr1XPZiLxXohYQQnh0Lb4QtGZ1xzCgjhBQLcIBeTOG_tYjCues9tmLt93' + + 'LpJfypSJ-SjDLwkR2s069_IByYGpxyeGtV-ulqYhSw1nD2CXKMDGyO5jXDs9tJrS_' + + 'UhfobXKQH03CRdFugyPkSNmXY-AafFynG7xLr7oYBC05FnhUXPm3VBTPt9K-BpqwY' + + 'd_h9vkAWeprSPo83UlwcLMupSJY9LaHxhRdz2yi0ZKNwXXHRwcszGjDBvvzUcCYbq' + + 'WqjzbEvFY6KtH8Jh4LhM46rHaoEOTernJsDF6a6W8Df88RthqTExcwnaQf0O_dlbj' + + 'SxEIPfbxx8t1EQugw', + dp: '4Y7Hu5tYAnLhMXuQqj9dgqU3PkcKYdCp7xc6f7Ah2P2JJHfYz4z4RD7Ez1eLyNKz' + + 'ulZ8A_PVHUjlSZiRkaYTBAEaJDrV70P6cFWuC6WpA0ZREQ1V7EgrQnANbGILa8Qs' + + 'PbYyhSQu4YlB1IwQq5_OmzyVBtgWA7AZIMMzMsMT0FuB_if-gWohBjmRN-vh0p45' + + 'VUf6UW568-_YmgDFmMYbg1UFs7s_TwrNenPR0h7MO4CB8hP9vJLoZrooRczzIjlj' + + 'Pbwy5bRG9CJfjTJ0vhj9MUT3kR1hHV1HJVGU5iBbfTfBKnvJGSI6-IDM4ZUm-B0R' + + '5hbs6s9cfOjhFmACIJIbMQ', + dq: 'gT4iPbfyHyVEwWyQb4X4grjvg7bXSKSwG1SXMDAOzV9tg7LwJjKYNy8gJAtJgNNV' + + 'dsfVLs-E_Epzpoph1AIWO9YZZXkov6Yc9zyEVONMX9S7ReU74hTBd8E9b2lMfMg9' + + 'ogYk9jtSPTt-6kigW4fOh4cHqZ6_tP3cgfLD3JZ8FDPHE4WaySvLDq49yUBO5dQK' + + 'yIU_xV6OGhQjOUjP_yEoMmzn9tOittsIHTxbXTxqQ6c1FvU9O6YTv8Jl5_Cl66kh' + + 'fX1I1RG38xvurcHULyUbYgeuZ_Iuo9XreT73h9_owo9RguGT29XH4vcNZmRGf5GI' + + 'vRb4e5lvtleIZkwJA3u78w', + qi: 'JHmVKb1zwW5iRR6RCeexYnh2fmY-3DrPSdM8Dxhr0F8dayi-tlRqEdnG0hvp45n8' + + 'gLUskWWcB9EXlUJObZGKDfGuxgMa3g_xeLA2vmFQ12MxPsyH4iCNZvsgmGxx7TuO' + + 'HrnDh5EBVnM4_de63crEJON2sYI8Ozi-xp2OEmAr2seWKq4sxkFni6exLhqb-NE4' + + 'm9HMKlng1EtQh2rLBFG1VYD3SYYpMLc5fxzqGvSxn3Fa-Xgg-IZPY3ubrcm52KYg' + + 'mLUGmnYStfVqGSWSdhDXHlNgI5pdAA0FzpyBk3ZX-JsxhwcnneKrYBBweq06kRMG' + + 'WgvdbdAQ-7wSeGqqj5VPwA' + } + }, +}; + +async function testImportSpki({ name, publicUsages }, size, hash, extractable) { + const key = await subtle.importKey( + 'spki', + keyData[size].spki, + { name, hash }, + extractable, + publicUsages); + + assert.strictEqual(key.type, 'public'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, publicUsages); + assert.strictEqual(key.algorithm.name, name); + assert.strictEqual(key.algorithm.modulusLength, size); + assert.deepStrictEqual(key.algorithm.publicExponent, + new Uint8Array([1, 0, 1])); + assert.strictEqual(key.algorithm.hash.name, hash); + + if (extractable) { + const spki = await subtle.exportKey('spki', key); + assert.strictEqual( + Buffer.from(spki).toString('hex'), + keyData[size].spki.toString('hex')); + } else { + await assert.rejects( + subtle.exportKey('spki', key), { + message: /key is not extractable/ + }); + } +} + +async function testImportPkcs8( + { name, privateUsages }, + size, + hash, + extractable) { + const key = await subtle.importKey( + 'pkcs8', + keyData[size].pkcs8, + { name, hash }, + extractable, + privateUsages); + + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, privateUsages); + assert.strictEqual(key.algorithm.name, name); + assert.strictEqual(key.algorithm.modulusLength, size); + assert.deepStrictEqual(key.algorithm.publicExponent, + new Uint8Array([1, 0, 1])); + assert.strictEqual(key.algorithm.hash.name, hash); + + if (extractable) { + const pkcs8 = await subtle.exportKey('pkcs8', key); + assert.strictEqual( + Buffer.from(pkcs8).toString('hex'), + keyData[size].pkcs8.toString('hex')); + } else { + await assert.rejects( + subtle.exportKey('pkcs8', key), { + message: /key is not extractable/ + }); + } +} + +async function testImportJwk( + { name, publicUsages, privateUsages }, + size, + hash, + extractable) { + + const jwk = keyData[size].jwk; + + const [ + publicKey, + privateKey, + ] = await Promise.all([ + subtle.importKey( + 'jwk', + { + kty: jwk.kty, + n: jwk.n, + e: jwk.e, + alg: `PS${hash.substring(4)}` + }, + { name, hash }, + extractable, + publicUsages), + subtle.importKey( + 'jwk', + { ...jwk, alg: `PS${hash.substring(4)}` }, + { name, hash }, + extractable, + privateUsages) + ]); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.extractable, extractable); + assert.strictEqual(privateKey.extractable, extractable); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(publicKey.algorithm.modulusLength, size); + assert.strictEqual(privateKey.algorithm.modulusLength, size); + assert.deepStrictEqual(publicKey.algorithm.publicExponent, + new Uint8Array([1, 0, 1])); + assert.deepStrictEqual(publicKey.algorithm.publicExponent, + privateKey.algorithm.publicExponent); + + if (extractable) { + const [ + pubJwk, + pvtJwk + ] = await Promise.all([ + subtle.exportKey('jwk', publicKey), + subtle.exportKey('jwk', privateKey) + ]); + + assert.strictEqual(pubJwk.kty, 'RSA'); + assert.strictEqual(pvtJwk.kty, 'RSA'); + assert.strictEqual(pubJwk.n, jwk.n); + assert.strictEqual(pvtJwk.n, jwk.n); + assert.strictEqual(pubJwk.e, jwk.e); + assert.strictEqual(pvtJwk.e, jwk.e); + assert.strictEqual(pvtJwk.d, jwk.d); + assert.strictEqual(pvtJwk.p, jwk.p); + assert.strictEqual(pvtJwk.q, jwk.q); + assert.strictEqual(pvtJwk.dp, jwk.dp); + assert.strictEqual(pvtJwk.dq, jwk.dq); + assert.strictEqual(pvtJwk.qi, jwk.qi); + assert.strictEqual(pubJwk.d, undefined); + assert.strictEqual(pubJwk.p, undefined); + assert.strictEqual(pubJwk.q, undefined); + assert.strictEqual(pubJwk.dp, undefined); + assert.strictEqual(pubJwk.dq, undefined); + assert.strictEqual(pubJwk.qi, undefined); + } else { + await assert.rejects( + subtle.exportKey('jwk', publicKey), { + message: /key is not extractable/ + }); + await assert.rejects( + subtle.exportKey('jwk', privateKey), { + message: /key is not extractable/ + }); + } +} + +// combinations to test +const testVectors = [ + { + name: 'RSA-OAEP', + privateUsages: ['decrypt', 'unwrapKey'], + publicUsages: ['encrypt', 'wrapKey'] + }, + { + name: 'RSA-PSS', + privateUsages: ['sign'], + publicUsages: ['verify'] + }, + { + name: 'RSASSA-PKCS1-V1_5', + privateUsages: ['sign'], + publicUsages: ['verify'] + } +]; + +(async function() { + const variations = []; + sizes.forEach((size) => { + hashes.forEach((hash) => { + [true, false].forEach((extractable) => { + testVectors.forEach((vector) => { + variations.push(testImportSpki(vector, size, hash, extractable)); + variations.push(testImportPkcs8(vector, size, hash, extractable)); + variations.push(testImportJwk(vector, size, hash, extractable)); + }); + }); + }); + }); + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js new file mode 100644 index 00000000000000..bc37f0ea6343a3 --- /dev/null +++ b/test/parallel/test-webcrypto-export-import.js @@ -0,0 +1,207 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle, getRandomValues } = require('crypto').webcrypto; + +{ + const keyData = getRandomValues(new Uint8Array(32)); + [1, null, undefined, {}, []].forEach((format) => { + assert.rejects( + subtle.importKey(format, keyData, {}, false, ['wrapKey']), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + assert.rejects( + subtle.importKey('not valid', keyData, {}, false, ['wrapKey']), { + code: 'ERR_INVALID_ARG_VALUE' + }); + [1, null, undefined, {}, []].forEach((keyData) => { + assert.rejects( + subtle.importKey('raw', keyData, {}, false, ['deriveBits']), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); +} + +// Import/Export HMAC Secret Key +{ + async function test() { + const keyData = getRandomValues(new Uint8Array(32)); + const key = await subtle.importKey( + 'raw', + keyData, { + name: 'HMAC', + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const raw = await subtle.exportKey('raw', key); + + assert.deepStrictEqual( + Buffer.from(keyData).toString('hex'), + Buffer.from(raw).toString('hex')); + + const jwk = await subtle.exportKey('jwk', key); + assert.deepStrictEqual(jwk.key_ops, ['sign', 'verify']); + assert(jwk.ext); + assert.strictEqual(jwk.kty, 'oct'); + + assert.deepStrictEqual( + Buffer.from(jwk.k, 'base64').toString('hex'), + Buffer.from(raw).toString('hex')); + } + + test().then(common.mustCall()); +} + +// Import/Export AES Secret Key +{ + async function test() { + const keyData = getRandomValues(new Uint8Array(32)); + const key = await subtle.importKey( + 'raw', + keyData, { + name: 'AES-CTR', + length: 256, + }, true, ['encrypt', 'decrypt']); + + const raw = await subtle.exportKey('raw', key); + + assert.deepStrictEqual( + Buffer.from(keyData).toString('hex'), + Buffer.from(raw).toString('hex')); + + const jwk = await subtle.exportKey('jwk', key); + assert.deepStrictEqual(jwk.key_ops, ['encrypt', 'decrypt']); + assert(jwk.ext); + assert.strictEqual(jwk.kty, 'oct'); + + assert.deepStrictEqual( + Buffer.from(jwk.k, 'base64').toString('hex'), + Buffer.from(raw).toString('hex')); + } + + test().then(common.mustCall()); +} + +// Import/Export RSA Key Pairs +{ + async function test() { + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-384' + }, true, ['sign', 'verify']); + + const [ + spki, + pkcs8, + publicJwk, + privateJwk + ] = await Promise.all([ + subtle.exportKey('spki', publicKey), + subtle.exportKey('pkcs8', privateKey), + subtle.exportKey('jwk', publicKey), + subtle.exportKey('jwk', privateKey) + ]); + + assert(spki); + assert(pkcs8); + assert(publicJwk); + assert(privateJwk); + + const [ + importedSpkiPublicKey, + importedPkcs8PrivateKey, + importedJwkPublicKey, + importedJwkPrivateKey + ] = await Promise.all([ + subtle.importKey('spki', spki, { + name: 'RSA-PSS', + hash: 'SHA-384', + }, true, ['verify']), + subtle.importKey('pkcs8', pkcs8, { + name: 'RSA-PSS', + hash: 'SHA-384', + }, true, ['sign']), + subtle.importKey('jwk', publicJwk, { + name: 'RSA-PSS', + hash: 'SHA-384', + }, true, ['verify']), + subtle.importKey('jwk', privateJwk, { + name: 'RSA-PSS', + hash: 'SHA-384', + }, true, ['sign']), + ]); + + assert(importedSpkiPublicKey); + assert(importedPkcs8PrivateKey); + assert(importedJwkPublicKey); + assert(importedJwkPrivateKey); + } + + test().then(common.mustCall()); +} + +// Import/Export EC Key Pairs +{ + async function test() { + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'ECDSA', + namedCurve: 'P-384' + }, true, ['sign', 'verify']); + + const [ + spki, + pkcs8, + publicJwk, + privateJwk + ] = await Promise.all([ + subtle.exportKey('spki', publicKey), + subtle.exportKey('pkcs8', privateKey), + subtle.exportKey('jwk', publicKey), + subtle.exportKey('jwk', privateKey) + ]); + + assert(spki); + assert(pkcs8); + assert(publicJwk); + assert(privateJwk); + + const [ + importedSpkiPublicKey, + importedPkcs8PrivateKey, + importedJwkPublicKey, + importedJwkPrivateKey + ] = await Promise.all([ + subtle.importKey('spki', spki, { + name: 'ECDSA', + namedCurve: 'P-384' + }, true, ['verify']), + subtle.importKey('pkcs8', pkcs8, { + name: 'ECDSA', + namedCurve: 'P-384' + }, true, ['sign']), + subtle.importKey('jwk', publicJwk, { + name: 'ECDSA', + namedCurve: 'P-384' + }, true, ['verify']), + subtle.importKey('jwk', privateJwk, { + name: 'ECDSA', + namedCurve: 'P-384' + }, true, ['sign']), + ]); + + assert(importedSpkiPublicKey); + assert(importedPkcs8PrivateKey); + assert(importedJwkPublicKey); + assert(importedJwkPrivateKey); + } + + test().then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js new file mode 100644 index 00000000000000..6d922be0fb89a8 --- /dev/null +++ b/test/parallel/test-webcrypto-keygen.js @@ -0,0 +1,647 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle, CryptoKey } = require('crypto').webcrypto; + +const allUsages = [ + 'encrypt', + 'decrypt', + 'sign', + 'verify', + 'deriveBits', + 'deriveKey', + 'wrapKey', + 'unwrapKey' +]; +const vectors = { + 'AES-CTR': { + algorithm: { length: 256 }, + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey' + ], + mandatoryUsages: [] + }, + 'AES-CBC': { + algorithm: { length: 256 }, + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey' + ], + mandatoryUsages: [] + }, + 'AES-GCM': { + algorithm: { length: 256 }, + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey' + ], + mandatoryUsages: [] + }, + 'AES-KW': { + algorithm: { length: 256 }, + usages: [ + 'wrapKey', + 'unwrapKey' + ], + mandatoryUsages: [] + }, + 'HMAC': { + algorithm: { length: 256, hash: 'SHA-256' }, + usages: [ + 'sign', + 'verify' + ], + mandatoryUsages: [] + }, + 'RSASSA-PKCS1-V1_5': { + algorithm: { + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + usages: [ + 'sign', + 'verify' + ], + mandatoryUsages: ['sign'] }, + 'RSA-PSS': { + algorithm: { + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + usages: [ + 'sign', + 'verify' + ], + mandatoryUsages: ['sign'] + }, + 'RSA-OAEP': { + algorithm: { + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey' + ], + mandatoryUsages: [ + 'decrypt', + 'unwrapKey' + ] + }, + 'ECDSA': { + algorithm: { namedCurve: 'P-521' }, + usages: [ + 'sign', + 'verify' + ], + mandatoryUsages: ['sign'] + }, + 'ECDH': { + algorithm: { namedCurve: 'P-521' }, + usages: [ + 'deriveKey', + 'deriveBits' + ], + mandatoryUsages: [ + 'deriveKey', + 'deriveBits' + ] + }, + 'NODE-DSA': { + algorithm: { modulusLength: 1024, hash: 'SHA-256' }, + usages: [ + 'sign', + 'verify' + ], + mandatoryUsages: [ + 'sign', + 'verify' + ] + } +}; + +// Test invalid algorithms +{ + async function test(algorithm) { + return assert.rejects( + // The extractable and usages values are invalid here also, + // but the unrecognized algorithm name should be caught first. + subtle.generateKey(algorithm, 7, ['zebra']), { + message: /Unrecognized name/ + }); + } + + const tests = [ + 'AES', + { name: 'AES' }, + { name: 'AES-CMAC' }, + { name: 'AES-CFB' }, + { name: 'HMAC', hash: 'MD5' }, + { + name: 'RSA', + hash: 'SHA-256', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]) + }, + { + name: 'RSA-PSS', + hash: 'SHA', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]) + }, + { + name: 'EC', + namedCurve: 'P521' + } + ].map(async (algorithm) => test(algorithm)); + + Promise.all(tests).then(common.mustCall()); +} + +// Test bad usages +{ + async function test(name) { + const invalidUsages = []; + allUsages.forEach((usage) => { + if (!vectors[name].usages.includes(usage)) + invalidUsages.push(usage); + }); + return assert.rejects( + subtle.generateKey( + { + name, ...vectors[name].algorithm + }, + true, + invalidUsages), + { message: /Unsupported key usage/ }); + } + + const tests = Object.keys(vectors).map(test); + + Promise.all(tests).then(common.mustCall()); +} + +// Test RSA key generation +{ + async function test( + name, + modulusLength, + publicExponent, + hash, + privateUsages, + publicUsages = privateUsages) { + let usages = privateUsages; + if (publicUsages !== privateUsages) + usages = usages.concat(publicUsages); + const { publicKey, privateKey } = await subtle.generateKey({ + name, + modulusLength, + publicExponent, + hash + }, true, usages); + + assert(publicKey); + assert(privateKey); + + assert(publicKey instanceof CryptoKey); + assert(privateKey instanceof CryptoKey); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.extractable, true); + assert.strictEqual(privateKey.extractable, true); + assert.deepStrictEqual(publicKey.usages, publicUsages); + assert.deepStrictEqual(privateKey.usages, privateUsages); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(publicKey.algorithm.modulusLength, modulusLength); + assert.deepStrictEqual(publicKey.algorithm.publicExponent, publicExponent); + assert.strictEqual(publicKey.algorithm.hash.name, hash); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm.modulusLength, modulusLength); + assert.deepStrictEqual(privateKey.algorithm.publicExponent, publicExponent); + assert.strictEqual(privateKey.algorithm.hash.name, hash); + + // Missing parameters + await assert.rejects( + subtle.generateKey({ name, publicExponent, hash }, true, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + + await assert.rejects( + subtle.generateKey({ name, modulusLength, hash }, true, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + + await assert.rejects( + subtle.generateKey({ name, modulusLength }, true, usages), { + code: 'ERR_MISSING_OPTION' + }); + + await Promise.all(['', true, {}].map((modulusLength) => { + return assert.rejects(subtle.generateKey({ + name, + modulusLength, + publicExponent, + hash + }, true, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + })); + + await Promise.all( + [ + '', + true, + {}, + 1, + [], + new Uint32Array(2) + ].map((publicExponent) => { + return assert.rejects( + subtle.generateKey( + { name, modulusLength, publicExponent, hash }, true, usages), + { code: 'ERR_INVALID_ARG_TYPE' }); + })); + + await Promise.all([true, {}, 1, []].map((hash) => { + return assert.rejects(subtle.generateKey({ + name, + modulusLength, + publicExponent, + hash + }, true, usages), { + message: /Unrecognized name/ + }); + })); + + await Promise.all(['', {}, 1, []].map((extractable) => { + return assert.rejects(subtle.generateKey({ + name, + modulusLength, + publicExponent, + hash + }, extractable, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + })); + + await Promise.all(['', {}, 1, false].map((usages) => { + return assert.rejects(subtle.generateKey({ + name, + modulusLength, + publicExponent, + hash + }, true, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + })); + } + + const kTests = [ + [ + 'RSASSA-PKCS1-V1_5', + 1024, + Buffer.from([1, 0, 1]), + 'SHA-256', + ['sign'], + ['verify'] + ], + [ + 'RSA-PSS', + 2048, + Buffer.from([1, 0, 1]), + 'SHA-512', + ['sign'], + ['verify'] + ], + [ + 'RSA-OAEP', + 1024, + Buffer.from([3]), + 'SHA-384', + ['decrypt', 'unwrapKey'], + ['encrypt', 'wrapKey'] + ] + ]; + + const tests = kTests.map((args) => test(...args)); + + Promise.all(tests).then(common.mustCall()); +} + +// Test EC Key Generation +{ + async function test( + name, + namedCurve, + privateUsages, + publicUsages = privateUsages) { + + let usages = privateUsages; + if (publicUsages !== privateUsages) + usages = usages.concat(publicUsages); + + const { publicKey, privateKey } = await subtle.generateKey({ + name, + namedCurve + }, true, usages); + + assert(publicKey); + assert(privateKey); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.extractable, true); + assert.strictEqual(privateKey.extractable, true); + assert.deepStrictEqual(publicKey.usages, publicUsages); + assert.deepStrictEqual(privateKey.usages, privateUsages); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(publicKey.algorithm.namedCurve, namedCurve); + assert.strictEqual(privateKey.algorithm.namedCurve, namedCurve); + + // Invalid parameters + [1, true, {}, [], undefined, null].forEach(async (namedCurve) => { + await assert.rejects( + subtle.generateKey({ name, namedCurve }, true, privateUsages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + } + + const kTests = [ + [ + 'ECDSA', + 'P-384', + ['sign'], + ['verify'] + ], + [ + 'ECDSA', + 'P-521', + ['sign'], + ['verify'] + ], + [ + 'ECDH', + 'P-384', + ['deriveKey', 'deriveBits'], + [] + ], + [ + 'ECDH', + 'P-521', + ['deriveKey', 'deriveBits'], + [] + ] + ]; + + const tests = kTests.map((args) => test(...args)); + + // Test bad parameters + + Promise.all(tests).then(common.mustCall()); +} + +// Test AES Key Generation +{ + async function test(name, length, usages) { + const key = await subtle.generateKey({ + name, + length + }, true, usages); + + assert(key); + + assert.strictEqual(key.type, 'secret'); + assert.strictEqual(key.extractable, true); + assert.deepStrictEqual(key.usages, usages); + assert.strictEqual(key.algorithm.name, name); + assert.strictEqual(key.algorithm.length, length); + + // Invalid parameters + [1, 100, 257].forEach(async (length) => { + await assert.rejects( + subtle.generateKey({ name, length }, true, usages), { + code: 'ERR_INVALID_ARG_VALUE' + }); + }); + + ['', {}, [], false, null, undefined].forEach(async (length) => { + await assert.rejects( + subtle.generateKey({ name, length }, true, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + } + + const kTests = [ + [ 'AES-CTR', 128, ['encrypt', 'decrypt', 'wrapKey']], + [ 'AES-CTR', 256, ['encrypt', 'decrypt', 'unwrapKey']], + [ 'AES-CBC', 128, ['encrypt', 'decrypt']], + [ 'AES-CBC', 256, ['encrypt', 'decrypt']], + [ 'AES-GCM', 128, ['encrypt', 'decrypt']], + [ 'AES-GCM', 256, ['encrypt', 'decrypt']], + [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']], + [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']], + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test HMAC Key Generation +{ + async function test(length, hash, usages) { + const key = await subtle.generateKey({ + name: 'HMAC', + length, + hash + }, true, usages); + + if (length === undefined) { + switch (hash) { + case 'SHA-1': length = 160; break; + case 'SHA-256': length = 256; break; + case 'SHA-384': length = 384; break; + case 'SHA-512': length = 512; break; + } + } + + assert(key); + + assert.strictEqual(key.type, 'secret'); + assert.strictEqual(key.extractable, true); + assert.deepStrictEqual(key.usages, usages); + assert.strictEqual(key.algorithm.name, 'HMAC'); + assert.strictEqual(key.algorithm.length, length); + assert.strictEqual(key.algorithm.hash.name, hash); + + ['', {}, [], false, null].forEach(async (length) => { + await assert.rejects( + subtle.generateKey({ name: 'HMAC', length, hash }, true, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, {}, [], false, null].forEach(async (hash) => { + await assert.rejects( + subtle.generateKey({ name: 'HMAC', length, hash }, true, usages), { + message: /Unrecognized name/ + }); + }); + } + + const kTests = [ + [ undefined, 'SHA-1', ['sign', 'verify']], + [ undefined, 'SHA-256', ['sign', 'verify']], + [ undefined, 'SHA-384', ['sign', 'verify']], + [ undefined, 'SHA-512', ['sign', 'verify']], + [ 128, 'SHA-256', ['sign', 'verify']], + [ 1024, 'SHA-512', ['sign', 'verify']], + ]; + + const tests = Promise.all(kTests.map((args) => test(...args))); + + tests.then(common.mustCall()); +} + +// Test NODE-DSA key generation +{ + async function test( + name, + modulusLength, + hash, + privateUsages, + publicUsages = privateUsages) { + let usages = privateUsages; + if (publicUsages !== privateUsages) + usages = usages.concat(publicUsages); + const { publicKey, privateKey } = await subtle.generateKey({ + name, + modulusLength, + hash + }, true, usages); + + assert(publicKey); + assert(privateKey); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.extractable, true); + assert.strictEqual(privateKey.extractable, true); + assert.deepStrictEqual(publicKey.usages, publicUsages); + assert.deepStrictEqual(privateKey.usages, privateUsages); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(publicKey.algorithm.modulusLength, modulusLength); + assert.strictEqual(publicKey.algorithm.hash.name, hash); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm.modulusLength, modulusLength); + assert.strictEqual(privateKey.algorithm.hash.name, hash); + + // Missing parameters + await assert.rejects( + subtle.generateKey({ name, hash }, true, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + + await assert.rejects( + subtle.generateKey({ name, modulusLength }, true, usages), { + code: 'ERR_MISSING_OPTION' + }); + + await Promise.all(['', true, {}].map((modulusLength) => { + return assert.rejects(subtle.generateKey({ + name, + modulusLength, + hash + }, true, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + })); + + await Promise.all([true, {}, 1, []].map((hash) => { + return assert.rejects(subtle.generateKey({ + name, + modulusLength, + hash + }, true, usages), { + message: /Unrecognized name/ + }); + })); + + await Promise.all(['', {}, 1, []].map((extractable) => { + return assert.rejects(subtle.generateKey({ + name, + modulusLength, + hash + }, extractable, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + })); + + await Promise.all(['', {}, 1, false].map((usages) => { + return assert.rejects(subtle.generateKey({ + name, + modulusLength, + hash + }, true, usages), { + code: 'ERR_INVALID_ARG_TYPE' + }); + })); + } + + const kTests = [ + [ + 'NODE-DSA', + 1024, + 'SHA-256', + ['sign'], + ['verify'] + ] + ]; + + const tests = kTests.map((args) => test(...args)); + + Promise.all(tests).then(common.mustCall()); +} + +// Test NODE-DH key generation +(async function() { + const { publicKey, privateKey } = + await subtle.generateKey({ + name: 'NODE-DH', + group: 'modp15' + }, true, ['deriveKey']); + assert(publicKey); + assert(privateKey); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.algorithm.name, 'NODE-DH'); + assert.strictEqual(privateKey.algorithm.name, 'NODE-DH'); + assert.strictEqual(publicKey.algorithm.group, 'modp15'); + assert.strictEqual(privateKey.algorithm.group, 'modp15'); +})().then(common.mustCall()); + +// End user code cannot create CryptoKey directly +assert.throws(() => new CryptoKey(), { + code: 'ERR_OPERATION_FAILED' +}); diff --git a/test/parallel/test-webcrypto-random.js b/test/parallel/test-webcrypto-random.js new file mode 100644 index 00000000000000..fd933915c5e48e --- /dev/null +++ b/test/parallel/test-webcrypto-random.js @@ -0,0 +1,66 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { Buffer } = require('buffer'); +const assert = require('assert'); +const { getRandomValues } = require('crypto').webcrypto; + +[undefined, null, '', 1, {}, []].forEach((i) => { + assert.throws(() => getRandomValues(i), { code: 17 }); +}); + +{ + const buf = new Uint8Array(0); + getRandomValues(buf); +} + +{ + const buf = new Uint8Array(new Array(10).fill(0)); + const before = Buffer.from(buf).toString('hex'); + getRandomValues(buf); + const after = Buffer.from(buf).toString('hex'); + assert.notStrictEqual(before, after); +} + +{ + const buf = new Uint16Array(new Array(10).fill(0)); + const before = Buffer.from(buf).toString('hex'); + getRandomValues(buf); + const after = Buffer.from(buf).toString('hex'); + assert.notStrictEqual(before, after); +} + +{ + const buf = new Uint32Array(new Array(10).fill(0)); + const before = Buffer.from(buf).toString('hex'); + getRandomValues(buf); + const after = Buffer.from(buf).toString('hex'); + assert.notStrictEqual(before, after); +} + +{ + const buf = new Uint16Array(new Array(10).fill(0)); + const before = Buffer.from(buf).toString('hex'); + getRandomValues(new DataView(buf.buffer)); + const after = Buffer.from(buf).toString('hex'); + assert.notStrictEqual(before, after); +} + +{ + let kData; + try { + kData = Buffer.alloc(65536 + 1); + } catch { + // Ignore if error here. + } + + if (kData !== undefined) { + assert.throws(() => getRandomValues(kData), { + code: 22 + }); + } +} diff --git a/test/parallel/test-webcrypto-sign-verify-ecdsa.js b/test/parallel/test-webcrypto-sign-verify-ecdsa.js new file mode 100644 index 00000000000000..051640267922dd --- /dev/null +++ b/test/parallel/test-webcrypto-sign-verify-ecdsa.js @@ -0,0 +1,226 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const vectors = require('../fixtures/crypto/ecdsa')(); + +async function testVerify({ + name, + hash, + namedCurve, + publicKeyBuffer, + privateKeyBuffer, + signature, + plaintext }) { + const [ + publicKey, + noVerifyPublicKey, + privateKey, + hmacKey, + rsaKeys + ] = await Promise.all([ + subtle.importKey( + 'spki', + publicKeyBuffer, + { name, namedCurve }, + false, + ['verify']), + subtle.importKey( + 'spki', + publicKeyBuffer, + { name, namedCurve }, + false, + [ /* No usages */ ]), + subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name, namedCurve }, + false, + ['sign']), + subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign']), + subtle.generateKey( + { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + false, + ['sign']) + ]); + + assert(await subtle.verify({ name, hash }, publicKey, signature, plaintext)); + + // Test verification with altered buffers + const copy = Buffer.from(plaintext); + const sigcopy = Buffer.from(signature); + const p = subtle.verify({ name, hash }, publicKey, sigcopy, copy); + copy[0] = 255 - copy[0]; + sigcopy[0] = 255 - sigcopy[0]; + assert(await p); + + // Test failure when using wrong key + await assert.rejects( + subtle.verify({ name, hash }, privateKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + await assert.rejects( + subtle.verify({ name, hash }, noVerifyPublicKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.verify({ name, hash }, hmacKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + await assert.rejects( + subtle.verify({ name, hash }, rsaKeys.publicKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + // Test failure when signature is altered + { + const copy = Buffer.from(signature); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify( + { name, hash }, + publicKey, + copy, + plaintext))); + assert(!(await subtle.verify( + { name, hash }, + publicKey, + copy.slice(1), + plaintext))); + } + + // Test failure when data is altered + { + const copy = Buffer.from(plaintext); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify({ name, hash }, publicKey, signature, copy))); + } + + // Test failure when wrong hash is used + { + const otherhash = hash === 'SHA-1' ? 'SHA-256' : 'SHA-1'; + assert(!(await subtle.verify({ + name, + hash: otherhash + }, publicKey, signature, copy))); + } + + await assert.rejects( + subtle.verify({ name, hash: 'sha256' }, publicKey, signature, copy), { + message: /Unrecognized name/ + }); +} + +async function testSign({ + name, + hash, + namedCurve, + publicKeyBuffer, + privateKeyBuffer, + signature, + plaintext }) { + const [ + publicKey, + noSignPrivateKey, + privateKey, + hmacKey, + rsaKeys, + ] = await Promise.all([ + subtle.importKey( + 'spki', + publicKeyBuffer, + { name, namedCurve }, + false, + ['verify']), + subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name, namedCurve }, + false, + [ /* No usages */ ]), + subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name, namedCurve }, + false, + ['sign']), + subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign']), + subtle.generateKey( + { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + false, + ['sign']) + ]); + + { + const sig = await subtle.sign({ name, hash }, privateKey, plaintext); + assert.strictEqual(sig.byteLength, signature.byteLength); + assert(await subtle.verify({ name, hash }, publicKey, sig, plaintext)); + } + + { + const copy = Buffer.from(plaintext); + const p = subtle.sign({ name, hash }, privateKey, copy); + copy[0] = 255 - copy[0]; + const sig = await p; + assert(await subtle.verify({ name, hash }, publicKey, sig, plaintext)); + } + + // Test failure when using wrong key + await assert.rejects( + subtle.sign({ name, hash }, publicKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + // Test failure when no sign usage + await assert.rejects( + subtle.sign({ name, hash }, noSignPrivateKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.sign({ name, hash }, hmacKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + await assert.rejects( + subtle.sign({ name, hash }, rsaKeys.privateKey, plaintext), { + message: /Unable to use this key to sign/ + }); +} + +(async function() { + const variations = []; + + vectors.forEach((vector) => { + variations.push(testVerify(vector)); + variations.push(testSign(vector)); + }); + + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify-hmac.js b/test/parallel/test-webcrypto-sign-verify-hmac.js new file mode 100644 index 00000000000000..d4e53e860485d4 --- /dev/null +++ b/test/parallel/test-webcrypto-sign-verify-hmac.js @@ -0,0 +1,179 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const vectors = require('../fixtures/crypto/hmac')(); + +async function testVerify({ + hash, + keyBuffer, + signature, + plaintext }) { + const name = 'HMAC'; + const [ + key, + noVerifyKey, + rsaKeys + ] = await Promise.all([ + subtle.importKey( + 'raw', + keyBuffer, + { name, hash }, + false, + ['verify']), + subtle.importKey( + 'raw', + keyBuffer, + { name, hash }, + false, + [ /* No usages */ ]), + subtle.generateKey( + { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + false, + ['sign']) + ]); + + assert(await subtle.verify({ name, hash }, key, signature, plaintext)); + + // Test verification with altered buffers + const copy = Buffer.from(plaintext); + const sigcopy = Buffer.from(signature); + const p = subtle.verify({ name, hash }, key, sigcopy, copy); + copy[0] = 255 - copy[0]; + sigcopy[0] = 255 - sigcopy[0]; + assert(await p); + + // Test failure when using wrong key + await assert.rejects( + subtle.verify({ name, hash }, noVerifyKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.verify({ name, hash }, rsaKeys.publicKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + // Test failure when signature is altered + { + const copy = Buffer.from(signature); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify( + { name, hash }, + key, + copy, + plaintext))); + assert(!(await subtle.verify( + { name, hash }, + key, + copy.slice(1), + plaintext))); + } + + // Test failure when data is altered + { + const copy = Buffer.from(plaintext); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify({ name, hash }, key, signature, copy))); + } + + // Test failure when wrong hash is used + { + const otherhash = hash === 'SHA-1' ? 'SHA-256' : 'SHA-1'; + assert(!(await subtle.verify({ + name, + hash: otherhash + }, key, signature, copy))); + } + + await assert.rejects( + subtle.verify({ name, hash: 'sha256' }, key, signature, copy), { + message: /Unrecognized name/ + }); +} + +async function testSign({ + hash, + keyBuffer, + signature, + plaintext }) { + const name = 'HMAC'; + const [ + key, + noSignKey, + rsaKeys, + ] = await Promise.all([ + subtle.importKey( + 'raw', + keyBuffer, + { name, hash }, + false, + ['verify', 'sign']), + subtle.importKey( + 'raw', + keyBuffer, + { name, hash }, + false, + [ 'verify' ]), + subtle.generateKey( + { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + false, + ['sign']) + ]); + + { + const sig = await subtle.sign({ name, hash }, key, plaintext); + assert.strictEqual( + Buffer.from(sig).toString('hex'), + signature.toString('hex')); + assert(await subtle.verify({ name, hash }, key, sig, plaintext)); + } + + { + const copy = Buffer.from(plaintext); + const p = subtle.sign({ name, hash }, key, copy); + copy[0] = 255 - copy[0]; + const sig = await p; + assert(await subtle.verify({ name, hash }, key, sig, plaintext)); + } + + // Test failure when no sign usage + await assert.rejects( + subtle.sign({ name, hash }, noSignKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.sign({ name, hash }, rsaKeys.privateKey, plaintext), { + message: /Unable to use this key to sign/ + }); +} + +(async function() { + const variations = []; + + vectors.forEach((vector) => { + variations.push(testVerify(vector)); + variations.push(testSign(vector)); + }); + + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify-node-dsa.js b/test/parallel/test-webcrypto-sign-verify-node-dsa.js new file mode 100644 index 00000000000000..4c1424007213f0 --- /dev/null +++ b/test/parallel/test-webcrypto-sign-verify-node-dsa.js @@ -0,0 +1,220 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const dsa = require('../fixtures/crypto/dsa'); + +async function testVerify({ + algorithm, + hash, + publicKeyBuffer, + privateKeyBuffer, + signature, + plaintext }) { + const [ + publicKey, + noVerifyPublicKey, + privateKey, + hmacKey, + wrongKeys + ] = await Promise.all([ + subtle.importKey( + 'spki', + publicKeyBuffer, + { name: algorithm.name, hash }, + false, + ['verify']), + subtle.importKey( + 'spki', + publicKeyBuffer, + { name: algorithm.name, hash }, + false, + [ /* No usages */ ]), + subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name: algorithm.name, hash }, + false, + ['sign']), + subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign']), + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-521', + hash: 'SHA-256', + }, + false, + ['sign']) + ]); + + assert(await subtle.verify(algorithm, publicKey, signature, plaintext)); + + // Test verification with altered buffers + const copy = Buffer.from(plaintext); + const sigcopy = Buffer.from(signature); + const p = subtle.verify(algorithm, publicKey, sigcopy, copy); + copy[0] = 255 - copy[0]; + sigcopy[0] = 255 - sigcopy[0]; + assert(await p); + + // Test failure when using wrong key + await assert.rejects( + subtle.verify(algorithm, privateKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + await assert.rejects( + subtle.verify(algorithm, noVerifyPublicKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.verify(algorithm, hmacKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + await assert.rejects( + subtle.verify(algorithm, wrongKeys.publicKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + // Test failure when signature is altered + { + const copy = Buffer.from(signature); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify(algorithm, publicKey, copy, plaintext))); + assert(!(await subtle.verify( + algorithm, + publicKey, + copy.slice(1), + plaintext))); + } + + // Test failure when data is altered + { + const copy = Buffer.from(plaintext); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify(algorithm, publicKey, signature, copy))); + } + + // Test failure when wrong hash is used + { + const otherhash = hash === 'SHA-1' ? 'SHA-256' : 'SHA-1'; + assert(!(await subtle.verify({ + ...algorithm, + hash: otherhash + }, publicKey, signature, copy))); + } + + await assert.rejects( + subtle.verify( + { ...algorithm, hash: 'sha256' }, + publicKey, + signature, + copy), + { message: /Unrecognized name/ }); +} + +async function testSign({ + algorithm, + hash, + publicKeyBuffer, + privateKeyBuffer, + signature, + plaintext }) { + const [ + publicKey, + noSignPrivateKey, + privateKey, + hmacKey, + wrongKeys, + ] = await Promise.all([ + subtle.importKey( + 'spki', + publicKeyBuffer, + { name: algorithm.name, hash }, + false, + ['verify']), + subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name: algorithm.name, hash }, + false, + [ /* No usages */ ]), + subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name: algorithm.name, hash }, + false, + ['sign']), + subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign']), + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-521', + hash: 'SHA-256', + }, + false, + ['sign']) + ]); + + { + const sig = await subtle.sign(algorithm, privateKey, plaintext); + assert(await subtle.verify(algorithm, publicKey, sig, plaintext)); + } + + { + const copy = Buffer.from(plaintext); + const p = subtle.sign(algorithm, privateKey, copy); + copy[0] = 255 - copy[0]; + const sig = await p; + assert(await subtle.verify(algorithm, publicKey, sig, plaintext)); + } + + // Test failure when using wrong key + await assert.rejects( + subtle.sign(algorithm, publicKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + // Test failure when no sign usage + await assert.rejects( + subtle.sign(algorithm, noSignPrivateKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.sign(algorithm, hmacKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + await assert.rejects( + subtle.sign(algorithm, wrongKeys.privateKey, plaintext), { + message: /Unable to use this key to sign/ + }); +} + +(async function() { + const variations = []; + + dsa().forEach((vector) => { + variations.push(testVerify(vector)); + variations.push(testSign(vector)); + }); + + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js new file mode 100644 index 00000000000000..35bd363c5558a6 --- /dev/null +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -0,0 +1,226 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const rsa_pkcs = require('../fixtures/crypto/rsa_pkcs'); +const rsa_pss = require('../fixtures/crypto/rsa_pss'); + +async function testVerify({ + algorithm, + hash, + publicKeyBuffer, + privateKeyBuffer, + signature, + plaintext }) { + const [ + publicKey, + noVerifyPublicKey, + privateKey, + hmacKey, + rsaKeys + ] = await Promise.all([ + subtle.importKey( + 'spki', + publicKeyBuffer, + { name: algorithm.name, hash }, + false, + ['verify']), + subtle.importKey( + 'spki', + publicKeyBuffer, + { name: algorithm.name, hash }, + false, + [ /* No usages */ ]), + subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name: algorithm.name, hash }, + false, + ['sign']), + subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign']), + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-521', + hash: 'SHA-256', + }, + false, + ['sign']) + ]); + + assert(await subtle.verify(algorithm, publicKey, signature, plaintext)); + + // Test verification with altered buffers + const copy = Buffer.from(plaintext); + const sigcopy = Buffer.from(signature); + const p = subtle.verify(algorithm, publicKey, sigcopy, copy); + copy[0] = 255 - copy[0]; + sigcopy[0] = 255 - sigcopy[0]; + assert(await p); + + // Test failure when using wrong key + await assert.rejects( + subtle.verify(algorithm, privateKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + await assert.rejects( + subtle.verify(algorithm, noVerifyPublicKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.verify(algorithm, hmacKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + await assert.rejects( + subtle.verify(algorithm, rsaKeys.publicKey, signature, plaintext), { + message: /Unable to use this key to verify/ + }); + + // Test failure when signature is altered + { + const copy = Buffer.from(signature); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify(algorithm, publicKey, copy, plaintext))); + assert(!(await subtle.verify( + algorithm, + publicKey, + copy.slice(1), + plaintext))); + } + + // Test failure when data is altered + { + const copy = Buffer.from(plaintext); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify(algorithm, publicKey, signature, copy))); + } + + // Test failure when wrong hash is used + { + const otherhash = hash === 'SHA-1' ? 'SHA-256' : 'SHA-1'; + assert(!(await subtle.verify({ + ...algorithm, + hash: otherhash + }, publicKey, signature, copy))); + } + + await assert.rejects( + subtle.verify( + { ...algorithm, hash: 'sha256' }, + publicKey, + signature, + copy), + { message: /Unrecognized name/ }); +} + +async function testSign({ + algorithm, + hash, + publicKeyBuffer, + privateKeyBuffer, + signature, + plaintext }) { + const [ + publicKey, + noSignPrivateKey, + privateKey, + hmacKey, + rsaKeys, + ] = await Promise.all([ + subtle.importKey( + 'spki', + publicKeyBuffer, + { name: algorithm.name, hash }, + false, + ['verify']), + subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name: algorithm.name, hash }, + false, + [ /* No usages */ ]), + subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name: algorithm.name, hash }, + false, + ['sign']), + subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign']), + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-521', + hash: 'SHA-256', + }, + false, + ['sign']) + ]); + + { + const sig = await subtle.sign(algorithm, privateKey, plaintext); + assert.strictEqual(sig.byteLength, signature.byteLength); + assert(await subtle.verify(algorithm, publicKey, sig, plaintext)); + } + + { + const copy = Buffer.from(plaintext); + const p = subtle.sign(algorithm, privateKey, copy); + copy[0] = 255 - copy[0]; + const sig = await p; + assert(await subtle.verify(algorithm, publicKey, sig, plaintext)); + } + + // Test failure when using wrong key + await assert.rejects( + subtle.sign(algorithm, publicKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + // Test failure when no sign usage + await assert.rejects( + subtle.sign(algorithm, noSignPrivateKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.sign(algorithm, hmacKey, plaintext), { + message: /Unable to use this key to sign/ + }); + + await assert.rejects( + subtle.sign(algorithm, rsaKeys.privateKey, plaintext), { + message: /Unable to use this key to sign/ + }); +} + +(async function() { + const variations = []; + + rsa_pkcs().forEach((vector) => { + variations.push(testVerify(vector)); + variations.push(testSign(vector)); + }); + rsa_pss().forEach((vector) => { + variations.push(testVerify(vector)); + variations.push(testSign(vector)); + }); + + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify.js b/test/parallel/test-webcrypto-sign-verify.js new file mode 100644 index 00000000000000..93fcdc17815f3e --- /dev/null +++ b/test/parallel/test-webcrypto-sign-verify.js @@ -0,0 +1,106 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +// This is only a partial test. The WebCrypto Web Platform Tests +// will provide much greater coverage. + +// Test Sign/Verify RSASSA-PKCS1-V1_5 +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSASSA-PKCS1-V1_5', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'RSASSA-PKCS1-V1_5' + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'RSASSA-PKCS1-V1_5' + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify RSA-PSS +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSA-PSS', + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'RSA-PSS', + saltLength: 256, + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'RSA-PSS', + saltLength: 256, + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify ECDSA +{ + async function test(data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'ECDSA', + namedCurve: 'P-384', + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'ECDSA', + hash: 'SHA-384', + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name: 'ECDSA', + hash: 'SHA-384', + }, publicKey, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} + +// Test Sign/Verify HMAC +{ + async function test(data) { + const ec = new TextEncoder(); + + const key = await subtle.generateKey({ + name: 'HMAC', + length: 256, + hash: 'SHA-256' + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name: 'HMAC', + }, key, ec.encode(data)); + + assert(await subtle.verify({ + name: 'HMAC', + }, key, signature, ec.encode(data))); + } + + test('hello world').then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js new file mode 100644 index 00000000000000..cadeeca8ec3ee5 --- /dev/null +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -0,0 +1,276 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = require('crypto').webcrypto; + +const kWrappingData = { + 'RSA-OAEP': { + generate: { + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + wrap: { label: new Uint8Array(8) }, + pair: true + }, + 'AES-CTR': { + generate: { length: 128 }, + wrap: { counter: new Uint8Array(16), length: 64 }, + pair: false + }, + 'AES-CBC': { + generate: { length: 128 }, + wrap: { iv: new Uint8Array(16) }, + pair: false + }, + 'AES-GCM': { + generate: { length: 128 }, + wrap: { + iv: new Uint8Array(16), + additionalData: new Uint8Array(16), + tagLength: 64 + }, + pair: false + }, + 'AES-KW': { + generate: { length: 128 }, + wrap: { }, + pair: false + } +}; + +function generateWrappingKeys() { + return Promise.all(Object.keys(kWrappingData).map(async (name) => { + const keys = await subtle.generateKey( + { name, ...kWrappingData[name].generate }, + true, + ['wrapKey', 'unwrapKey']); + if (kWrappingData[name].pair) { + kWrappingData[name].wrappingKey = keys.publicKey; + kWrappingData[name].unwrappingKey = keys.privateKey; + } else { + kWrappingData[name].wrappingKey = keys; + kWrappingData[name].unwrappingKey = keys; + } + })); +} + +async function generateKeysToWrap() { + const parameters = [ + { + algorithm: { + name: 'RSASSA-PKCS1-V1_5', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'RSA-OAEP', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256' + }, + privateUsages: ['decrypt'], + publicUsages: ['encrypt'], + pair: true, + }, + { + algorithm: { + name: 'ECDSA', + namedCurve: 'P-384' + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'ECDH', + namedCurve: 'P-384' + }, + privateUsages: ['deriveBits'], + publicUsages: [], + pair: true, + }, + { + algorithm: { + name: 'AES-CTR', + length: 128 + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + { + algorithm: { + name: 'AES-CBC', + length: 128 + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + { + algorithm: { + name: 'AES-GCM', length: 128 + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + { + algorithm: { + name: 'AES-KW', + length: 128 + }, + usages: ['wrapKey', 'unwrapKey'], + pair: false, + }, + { + algorithm: { + name: 'HMAC', + length: 128, + hash: 'SHA-256' + }, + usages: ['sign', 'verify'], + pair: false, + } + ]; + + const allkeys = await Promise.all(parameters.map(async (params) => { + const usages = 'usages' in params ? + params.usages : + params.publicUsages.concat(params.privateUsages); + + const keys = await subtle.generateKey(params.algorithm, true, usages); + + if (params.pair) { + return [ + { + algorithm: params.algorithm, + usages: params.publicUsages, + key: keys.publicKey, + }, + { + algorithm: params.algorithm, + usages: params.privateUsages, + key: keys.privateKey, + } + ]; + } + + return [{ + algorithm: params.algorithm, + usages: params.usages, + key: keys }]; + })); + + return allkeys.flat(); +} + +function getFormats(key) { + switch (key.key.type) { + case 'secret': return ['raw', 'jwk']; + case 'public': return ['spki', 'jwk']; + case 'private': return ['pkcs8', 'jwk']; + } +} + +// If the wrapping algorithm is AES-KW, the exported key +// material length must be a multiple of 8. +// If the wrapping algorithm is RSA-OAEP, the exported key +// material maximum length is a factor of the modulusLength +async function wrappingIsPossible(name, exported) { + if ('byteLength' in exported) { + switch (name) { + case 'AES-KW': + return exported.byteLength % 8 === 0; + case 'RSA-OAEP': + return exported.byteLength <= 446; + } + } else if ('kty' in exported) { + switch (name) { + case 'AES-KW': + return JSON.stringify(exported).length % 8 === 0; + case 'RSA-OAEP': + return JSON.stringify(exported).length <= 478; + } + } + return true; +} + +async function testWrap(wrappingKey, unwrappingKey, key, wrap, format) { + const exported = await subtle.exportKey(format, key.key); + if (!(await wrappingIsPossible(wrappingKey.algorithm.name, exported))) + return; + + const wrapped = + await subtle.wrapKey( + format, + key.key, + wrappingKey, + { name: wrappingKey.algorithm.name, ...wrap }); + const unwrapped = + await subtle.unwrapKey( + format, + wrapped, + unwrappingKey, + { name: wrappingKey.algorithm.name, ...wrap }, + key.algorithm, + true, + key.usages); + assert(unwrapped.extractable); + + const exportedAgain = await subtle.exportKey(format, unwrapped); + assert.deepStrictEqual(exported, exportedAgain); +} + +async function testWrapping(name, keys, ecdhPeerKey) { + const variations = []; + + const { + wrappingKey, + unwrappingKey, + wrap + } = kWrappingData[name]; + + keys.forEach((key) => { + getFormats(key).forEach((format) => { + variations.push(testWrap(wrappingKey, unwrappingKey, key, wrap, format)); + }); + }); + + return Promise.all(variations); +} + +(async function() { + await generateWrappingKeys(); + const keys = await generateKeysToWrap(); + const ecdhPeerKey = await subtle.generateKey({ + name: 'ECDH', + namedCurve: 'P-384' + }, true, ['deriveBits']); + const variations = []; + Object.keys(kWrappingData).forEach((name) => { + return testWrapping(name, keys, ecdhPeerKey); + }); + await Promise.all(variations); +})().then(common.mustCall()); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 50a097febb8053..22fde0916544d6 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -56,6 +56,14 @@ const { getSystemErrorName } = require('util'); if (!common.isMainThread) delete providers.INSPECTORJSBINDING; delete providers.KEYPAIRGENREQUEST; + delete providers.KEYGENREQUEST; + delete providers.KEYEXPORTREQUEST; + delete providers.CIPHERREQUEST; + delete providers.DERIVEBITSREQUEST; + delete providers.SCRYPTREQUEST; + delete providers.SIGNREQUEST; + delete providers.VERIFYREQUEST; + delete providers.HASHREQUEST; delete providers.HTTPCLIENTREQUEST; delete providers.HTTPINCOMINGMESSAGE; delete providers.ELDHISTOGRAM; @@ -129,17 +137,17 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check // so need to check it from the callback. const mc = common.mustCall(function pb() { - testInitialized(this, 'AsyncWrap'); + testInitialized(this, 'PBKDF2Job'); }); crypto.pbkdf2('password', 'salt', 1, 20, 'sha256', mc); crypto.randomBytes(1, common.mustCall(function rb() { - testInitialized(this, 'AsyncWrap'); + testInitialized(this, 'RandomBytesJob'); })); if (typeof internalBinding('crypto').scrypt === 'function') { crypto.scrypt('password', 'salt', 8, common.mustCall(function() { - testInitialized(this, 'AsyncWrap'); + testInitialized(this, 'ScryptJob'); })); } } diff --git a/test/sequential/test-crypto-timing-safe-equal.js b/test/sequential/test-crypto-timing-safe-equal.js index 769b5f7d8810c3..a8bd3abf4cf1ae 100644 --- a/test/sequential/test-crypto-timing-safe-equal.js +++ b/test/sequential/test-crypto-timing-safe-equal.js @@ -46,9 +46,6 @@ assert.throws( { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: - 'The "buf1" argument must be an instance of Buffer, TypedArray, or ' + - 'DataView.' } ); @@ -57,8 +54,5 @@ assert.throws( { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: - 'The "buf2" argument must be an instance of Buffer, TypedArray, or ' + - 'DataView.' } ); diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 4e46d2c7c6cd21..8a2fc18206b412 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -71,6 +71,44 @@ const customTypesMap = { 'Verify': 'crypto.html#crypto_class_verify', 'crypto.constants': 'crypto.html#crypto_crypto_constants_1', + 'CryptoKey': 'webcrypto.html#webcrypto_class_cryptokey', + 'CryptoKeyPair': 'webcrypto.html#webcrypto_class_cryptokeypair', + 'Crypto': 'webcrypto.html#webcrypto_class_crypto', + 'SubtleCrypto': 'webcrypto.html#webcrypto_class_subtlecrypto', + 'RsaOaepParams': 'webcrypto.html#webcrypto_class_rsaoaepparams', + 'AesCtrParams': 'webcrypto.html#webcrypto_class_aesctrparams', + 'AesCbcParams': 'webcrypto.html#webcrypto_class_aescbcparams', + 'AesGcmParams': 'webcrypto.html#webcrypto_class_aesgcmparams', + 'AesKwParams': 'webcrypto.html#webcrypto_class_aeskwparams', + 'EcdhKeyDeriveParams': 'webcrypto.html#webcrypto_class_ecdhkeyderiveparams', + 'HkdfParams': 'webcrypto.html#webcrypto_class_hkdfparams', + 'Pbkdf2Params': 'webcrypto.html#webcrypto_class_pbkdf2params', + 'HmacKeyGenParams': 'webcrypto.html#webcrypto_class_hmackeygenparams', + 'AesKeyGenParams': 'webcrypto.html#webcrypto_class_aeskeygenparams', + 'RsaHashedKeyGenParams': + 'webcrypto.html#webcrypto_class_rsahashedkeygenparams', + 'EcKeyGenParams': 'webcrypto.html#webcrypto_class_eckeygenparams', + 'RsaHashedImportParams': + 'webcrypto.html#webcrypto_class_rsahashedimportparams', + 'EcKeyImportParams': 'webcrypto.html#webcrypto_class_eckeyimportparams', + 'HmacImportParams': 'webcrypto.html#webcrypto_class_hmacimportparams', + 'AesImportParams': 'webcrypto.html#webcrypto_class_aesimportparams', + 'Pbkdf2ImportParams': 'webcrypto.html#webcrypto_class.pbkdf2importparams', + 'HmacParams': 'webcrypto.html#webcrypto_class_hmacparams', + 'EcdsaParams': 'webcrypto.html#webcrypto_class_ecdsaparams', + 'RsaPssParams': 'webcrypto.html#webcrypto_class_rsapssparams', + 'RsaSignParams': 'webcrypto.html#webcrypto_class_rsasignparams', + 'NodeDhImportParams': 'webcrypto.html#webcrypto_class_nodedhimportparams', + 'NodeDhKeyGenParams': 'webcrypto.html#webcrypto_class_nodedhkeygenparams', + 'NodeDhDeriveBitsParams': + 'webcrypto.html#webcrypto_class_nodedhderivebitsparams', + 'NodeDsaImportParams': 'webcrypto.html#webcrypto_class_nodedsaimportparams', + 'NodeDsaKeyGenParams': 'webcrypto.html#webcrypto_class_nodedsakeygenparams', + 'NodeDsaSignParams': 'webcrypto.html#webcrypto_class_nodedsasignparams', + 'NodeScryptImportParams': + 'webcrypto.html#webcrypto_class_nodescryptimportparams', + 'NodeScryptParams': 'webcrypto.html#webcrypto_class_nodescryptparams', + 'dgram.Socket': 'dgram.html#dgram_class_dgram_socket', 'Domain': 'domain.html#domain_class_domain',