diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3ce84f6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[**.{js,json}] +indent_style = space +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..09a8422 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +!.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..25c4dcc --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,268 @@ +'use strict'; + +// This is a modified version of the Node.js core ESLint configuration. + +module.exports = { + root: true, + env: { + node: true, + es6: true + }, + parser: 'babel-eslint', + parserOptions: { sourceType: 'script' }, + rules: { + // ESLint built-in rules + // http://eslint.org/docs/rules + 'accessor-pairs': 'error', + 'array-callback-return': 'error', + 'arrow-parens': ['error', 'always'], + 'arrow-spacing': ['error', { before: true, after: true }], + 'block-scoped-var': 'error', + 'block-spacing': 'error', + 'brace-style': ['error', '1tbs', { allowSingleLine: true }], + 'capitalized-comments': ['error', 'always', { + line: { + // Ignore all lines that have less characters than 20 and all lines that + // start with something that looks like a variable name or code. + // eslint-disable-next-line max-len + ignorePattern: '.{0,20}$|[a-z]+ ?[0-9A-Z_.(/=:[#-]|std|http|ssh|ftp|(let|var|const) [a-z_A-Z0-9]+ =|[b-z] |[a-z]*[0-9].* ', + ignoreInlineComments: true, + ignoreConsecutiveComments: true + }, + block: { + ignorePattern: '.*' + } + }], + 'comma-dangle': ['error', 'never'], + 'comma-spacing': 'error', + 'comma-style': 'error', + 'computed-property-spacing': 'error', + 'constructor-super': 'error', + 'dot-location': ['error', 'property'], + 'dot-notation': 'error', + 'eol-last': 'error', + 'eqeqeq': ['error', 'smart'], + 'for-direction': 'error', + 'func-call-spacing': 'error', + 'func-name-matching': 'error', + 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], + 'getter-return': 'error', + 'indent': ['error', 2, { + ArrayExpression: 'first', + CallExpression: { arguments: 'first' }, + FunctionDeclaration: { parameters: 'first' }, + FunctionExpression: { parameters: 'first' }, + MemberExpression: 'off', + ObjectExpression: 'first', + SwitchCase: 1 + }], + 'key-spacing': ['error', { mode: 'strict' }], + 'keyword-spacing': 'error', + 'linebreak-style': ['error', 'unix'], + 'max-len': ['error', { + code: 80, + ignorePattern: '^// Flags:', + ignoreRegExpLiterals: true, + ignoreUrls: true, + tabWidth: 2 + }], + 'new-parens': 'error', + 'no-async-promise-executor': 'error', + 'no-class-assign': 'error', + 'no-confusing-arrow': 'error', + 'no-const-assign': 'error', + 'no-control-regex': 'error', + 'no-debugger': 'error', + 'no-delete-var': 'error', + 'no-dupe-args': 'error', + 'no-dupe-class-members': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty-character-class': 'error', + 'no-ex-assign': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-parens': ['error', 'functions'], + 'no-extra-semi': 'error', + 'no-fallthrough': 'error', + 'no-func-assign': 'error', + 'no-global-assign': 'error', + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-lonely-if': 'error', + 'no-misleading-character-class': 'error', + 'no-mixed-requires': 'error', + 'no-mixed-spaces-and-tabs': 'error', + 'no-multi-spaces': ['error', { ignoreEOLComments: true }], + 'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 0, maxBOF: 0 }], + 'no-new-require': 'error', + 'no-new-symbol': 'error', + 'no-obj-calls': 'error', + 'no-octal': 'error', + 'no-path-concat': 'error', + 'no-proto': 'error', + 'no-redeclare': ['error', { 'builtinGlobals': false }], + 'no-restricted-modules': ['error', 'sys'], + /* eslint-disable max-len */ + 'no-restricted-properties': [ + 'error', + { + object: 'assert', + property: 'deepEqual', + message: 'Use `assert.deepStrictEqual()`.' + }, + { + object: 'assert', + property: 'notDeepEqual', + message: 'Use `assert.notDeepStrictEqual()`.' + }, + { + object: 'assert', + property: 'equal', + message: 'Use `assert.strictEqual()` rather than `assert.equal()`.' + }, + { + object: 'assert', + property: 'notEqual', + message: 'Use `assert.notStrictEqual()` rather than `assert.notEqual()`.' + }, + { + property: '__defineGetter__', + message: '__defineGetter__ is deprecated.' + }, + { + property: '__defineSetter__', + message: '__defineSetter__ is deprecated.' + } + ], + 'no-restricted-syntax': [ + 'error', + { + selector: "CallExpression[callee.property.name='deepStrictEqual'][arguments.2.type='Literal']", + message: 'Do not use a literal for the third argument of assert.deepStrictEqual().' + }, + { + selector: "CallExpression[callee.property.name='doesNotThrow']", + message: 'Do not use `assert.doesNotThrow()`. Write the code without the wrapper and add a comment instead.' + }, + { + selector: "CallExpression[callee.property.name='doesNotReject']", + message: 'Do not use `assert.doesNotReject()`. Write the code without the wrapper and add a comment instead.' + }, + { + selector: "CallExpression[callee.property.name='rejects'][arguments.length<2]", + message: '`assert.rejects()` must be invoked with at least two arguments.' + }, + { + selector: "CallExpression[callee.property.name='strictEqual'][arguments.2.type='Literal']", + message: 'Do not use a literal for the third argument of assert.strictEqual().' + }, + { + selector: "CallExpression[callee.property.name='throws'][arguments.1.type='Literal']:not([arguments.1.regex])", + message: 'Use an object as second argument of `assert.throws()`.' + }, + { + selector: "CallExpression[callee.property.name='throws'][arguments.length<2]", + message: '`assert.throws()` must be invoked with at least two arguments.' + }, + { + selector: "CallExpression[callee.name='setTimeout'][arguments.length<2]", + message: '`setTimeout()` must be invoked with at least two arguments.' + }, + { + selector: "CallExpression[callee.name='setInterval'][arguments.length<2]", + message: '`setInterval()` must be invoked with at least two arguments.' + }, + { + selector: 'ThrowStatement > CallExpression[callee.name=/Error$/]', + message: 'Use `new` keyword when throwing an `Error`.' + }, + { + selector: "CallExpression[callee.property.name='notDeepStrictEqual'][arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])", + message: 'The first argument should be the `actual`, not the `expected` value.' + }, + { + selector: "CallExpression[callee.property.name='notStrictEqual'][arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])", + message: 'The first argument should be the `actual`, not the `expected` value.' + }, + { + selector: "CallExpression[callee.property.name='deepStrictEqual'][arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])", + message: 'The first argument should be the `actual`, not the `expected` value.' + }, + { + selector: "CallExpression[callee.property.name='strictEqual'][arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])", + message: 'The first argument should be the `actual`, not the `expected` value.' + } + ], + /* eslint-enable max-len */ + 'no-return-await': 'error', + 'no-self-assign': 'error', + 'no-self-compare': 'error', + 'no-shadow-restricted-names': 'error', + 'no-tabs': 'error', + 'no-template-curly-in-string': 'error', + 'no-this-before-super': 'error', + 'no-throw-literal': 'error', + 'no-trailing-spaces': 'error', + 'no-undef': ['error', { typeof: true }], + 'no-undef-init': 'error', + 'no-unexpected-multiline': 'error', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'error', + 'no-unsafe-negation': 'error', + 'no-unused-labels': 'error', + 'no-unused-vars': ['error', { args: 'none', caughtErrors: 'all' }], + 'no-use-before-define': ['error', { + classes: true, + functions: false, + variables: false + }], + 'no-useless-call': 'error', + 'no-useless-catch': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-useless-escape': 'error', + 'no-useless-return': 'error', + 'no-void': 'error', + 'no-whitespace-before-property': 'error', + 'no-with': 'error', + 'object-curly-spacing': ['error', 'always'], + 'one-var': ['error', { initialized: 'never' }], + 'one-var-declaration-per-line': 'error', + 'operator-linebreak': ['error', 'after'], + 'prefer-const': ['error', { ignoreReadBeforeAssign: true }], + 'quotes': ['error', 'single', { avoidEscape: true }], + 'quote-props': ['error', 'consistent'], + 'rest-spread-spacing': 'error', + 'semi': 'error', + 'semi-spacing': 'error', + 'space-before-blocks': ['error', 'always'], + 'space-before-function-paren': ['error', { + anonymous: 'never', + named: 'never', + asyncArrow: 'always' + }], + 'space-in-parens': ['error', 'never'], + 'space-infix-ops': 'error', + 'space-unary-ops': 'error', + 'spaced-comment': ['error', 'always', { + 'block': { 'balanced': true }, + 'exceptions': ['-'] + }], + 'strict': ['error', 'global'], + 'symbol-description': 'error', + 'template-curly-spacing': 'error', + 'unicode-bom': 'error', + 'use-isnan': 'error', + 'valid-typeof': 'error' + }, + globals: { + Atomics: 'readable', + BigInt: 'readable', + BigInt64Array: 'readable', + BigUint64Array: 'readable', + TextEncoder: 'readable', + TextDecoder: 'readable', + queueMicrotask: 'readable' + } +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..851b757 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - "12" + +script: + - npm run lint + - npm test diff --git a/README.md b/README.md new file mode 100644 index 0000000..4732522 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# WebCrypto Prototype for Node.js + +This is a partial and experimental WebCrypto implementation for the Node.js +platform. + +## Asynchonicity + +The WebCrypto specification requires almost all operations to be completed +asynchronously, however, Node.js implements very few operations asynchronously. +Usually, this is not a problem, since most cryptographic functions are +incredibly fast compared to the overhead that comes with asynchronicity, +and because Node.js implements most cryptographic features through efficient +streaming interfaces. WebCrypto has no streaming interfaces but only one-shot +APIs. Encrypting, hashing, signing or verifying large amounts of data is thus +difficult in WebCrypto without underlying asynchronous APIs. + +## Development + +### Structure + +The main export of this package is implemented in `lib/index.js` and represents +the `Crypto` interface as defined in section 10 of the +[WebCrypto specification][]. It contains two members: + +- The `subtle` attribute is implemented in `lib/subtle.js`, including all + methods described in section 14.3 of the WebCrypto specification. These + methods usually delegate work to one or more cryptographic operations + that are listed in section 18.2.2 and implemented in `lib/algorithms/`. +- The `getRandomValues` function is implemented in `lib/random.js`. + +### Tests + +The `test` directory contains a small number of unit tests. All of these tests +are required to pass after each commit. You can run unit tests using `npm test`. + +It is our intention to add Web Platform Tests (WPT) at some point. When this +happens, not all WPTs are required to pass, but if a test passes, it must not be +broken by a later commit. + +### Linting + +This repository uses ESLint. Use `npm run lint` to check the code. + +[WebCrypto specification]: https://www.w3.org/TR/WebCryptoAPI/ diff --git a/lib/algorithms.js b/lib/algorithms.js new file mode 100644 index 0000000..9a052de --- /dev/null +++ b/lib/algorithms.js @@ -0,0 +1,73 @@ +'use strict'; + +const { AES_CTR, AES_CBC, AES_GCM, AES_KW } = require('./algorithms/aes'); +const { HKDF } = require('./algorithms/hkdf'); +const { PBKDF2 } = require('./algorithms/pbkdf2'); +const { SHA_1, SHA_256, SHA_384, SHA_512 } = require('./algorithms/sha'); +const { NotSupportedError } = require('./errors'); + +const algorithms = [ + AES_CTR, + AES_CBC, + AES_GCM, + AES_KW, + + HKDF, + + PBKDF2, + + SHA_1, + SHA_256, + SHA_384, + SHA_512 +]; + +function objectFromArray(array, fn) { + const obj = {}; + for (const val of array) + fn(obj, val); + return obj; +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#dfn-supportedAlgorithms +const supportedAlgorithms = objectFromArray([ + // This corresponds to section 18.2.2 of the WebCrypto spec. + 'encrypt', + 'decrypt', + 'sign', + 'verify', + 'deriveBits', + 'wrapKey', + 'unwrapKey', + 'digest', + 'generateKey', + 'importKey', + 'exportKey', + 'get key length', + + // The following APIs are for internal use only. + 'get hash function' +], (opsByName, op) => { + opsByName[op] = objectFromArray(algorithms, (algsByName, alg) => { + if (typeof alg[op] === 'function') + algsByName[alg.name.toLowerCase()] = alg; + }); +}); + +function getAlgorithm(alg, op) { + if (typeof alg !== 'string') { + if (typeof alg !== 'object') + throw new SyntaxError(); + const { name } = alg; + if (typeof name !== 'string') + throw new SyntaxError(); + return getAlgorithm(alg.name, op); + } + + const impl = supportedAlgorithms[op][alg.toLowerCase()]; + if (impl === undefined) + throw new NotSupportedError(); + return impl; +} + +module.exports.getAlgorithm = getAlgorithm; diff --git a/lib/algorithms/aes.js b/lib/algorithms/aes.js new file mode 100644 index 0000000..668467b --- /dev/null +++ b/lib/algorithms/aes.js @@ -0,0 +1,234 @@ +'use strict'; + +const { + createCipheriv, + createDecipheriv, + createSecretKey, + randomBytes: randomBytesCallback +} = require('crypto'); +const { promisify } = require('util'); + +const { NotSupportedError, OperationError } = require('../errors'); +const { kKeyMaterial, CryptoKey } = require('../key'); +const { limitUsages, toBuffer } = require('../util'); + +const randomBytes = promisify(randomBytesCallback); + +const aesBase = { + async generateKey(algorithm, extractable, usages) { + limitUsages(usages, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']); + + const { length } = algorithm; + if (length !== 128 && length !== 192 && length !== 256) + throw new OperationError(); + + const key = createSecretKey(await randomBytes(length >> 3)); + return new CryptoKey('secret', { name: this.name, length }, extractable, + usages, key); + }, + + importKey(keyFormat, keyData, params, extractable, keyUsages) { + if (keyFormat !== 'raw') + throw new NotSupportedError(); + + limitUsages(keyUsages, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']); + + const buf = toBuffer(keyData); + if (buf.length !== 16 && buf.length !== 24 && buf.length !== 32) + throw new OperationError(); + + return new CryptoKey('secret', { name: this.name, length: buf.length << 3 }, + extractable, keyUsages, + createSecretKey(toBuffer(keyData))); + }, + + exportKey(format, key) { + if (format !== 'raw') + throw new NotSupportedError(); + return key[kKeyMaterial].export(); + }, + + 'get key length'(algorithm) { + const { length } = algorithm; + if (length !== 128 && length !== 192 && length !== 256) + throw new OperationError(); + return length; + } +}; + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#aes-ctr +module.exports.AES_CTR = { + name: 'AES-CTR', + ...aesBase, + + _doCipher(iv, length, data, fn) { + if (length === 128) { + // Fast path for the default 128-bit length. + return fn(data, iv); + } else { + // WebCrypto has a non-standard feature which allows to specify the number + // of bits that are used as the counter. This feature is not available in + // Node.js or OpenSSL and thus needs to be simulated: We calculate when + // an overflow would occur that would violate the given "length" restraint + // and restart encryption at those points with a different IV. + + let nBlocksBeforeOverflow = 1; + for (let i = 0; i < length; i++) { + if ((iv[15 - Math.floor(i / 8)] & (1 << (i % 8))) === 0) { + nBlocksBeforeOverflow += 2 ** i; + + if (nBlocksBeforeOverflow >= data.length / 16) + return fn(data, iv); + } + } + + const overflowIV = Buffer.from(iv); + if (length >= 8) + overflowIV.fill(0, 16 - Math.floor(length / 8), 16); + overflowIV[15 - Math.floor(length / 8)] &= (0xff << (length % 8)) & 0xff; + + let result = fn(data.slice(0, nBlocksBeforeOverflow * 16), iv); + const blocksPerCycle = 2 ** length; + const nBlocks = Math.ceil(data.length / 16); + for (let i = nBlocksBeforeOverflow; i < nBlocks; i += blocksPerCycle) { + result = Buffer.concat([ + result, + fn(data.slice(16 * i, 16 * (i + blocksPerCycle)), overflowIV) + ]); + } + return result; + } + }, + + encrypt(params, key, data) { + const { counter, length } = params; + + const iv = toBuffer(counter); + if (iv.length !== 16) + throw new OperationError(); + + if (length === 0 || length > 128) + throw new OperationError(); + + const secretKey = key[kKeyMaterial]; + const cipher = `aes-${secretKey.symmetricKeySize << 3}-ctr`; + + return this._doCipher(iv, length, data, (data, iv) => { + return createCipheriv(cipher, secretKey, iv).update(data); + }); + }, + + decrypt(params, key, data) { + const { counter, length } = params; + + const iv = toBuffer(counter); + if (iv.length !== 16) + throw new OperationError(); + + if (length === 0 || length > 128) + throw new OperationError(); + + const secretKey = key[kKeyMaterial]; + const cipher = `aes-${secretKey.symmetricKeySize << 3}-ctr`; + + return this._doCipher(iv, length, data, (data, iv) => { + return createDecipheriv(cipher, secretKey, iv).update(data); + }); + } +}; + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#aes-cbc +module.exports.AES_CBC = { + name: 'AES-CBC', + ...aesBase, + + encrypt(params, key, data) { + let { iv } = params; + + iv = toBuffer(iv); + if (iv.length !== 16) + throw new OperationError(); + + const secretKey = key[kKeyMaterial]; + const cipher = `aes-${secretKey.symmetricKeySize << 3}-cbc`; + + const c = createCipheriv(cipher, secretKey, iv); + return Buffer.concat([c.update(data), c.final()]); + }, + + decrypt(params, key, data) { + let { iv } = params; + + iv = toBuffer(iv); + if (iv.length !== 16) + throw new OperationError(); + + const secretKey = key[kKeyMaterial]; + const cipher = `aes-${secretKey.symmetricKeySize << 3}-cbc`; + + const c = createDecipheriv(cipher, secretKey, iv); + return Buffer.concat([c.update(data), c.final()]); + } +}; + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#aes-gcm +module.exports.AES_GCM = { + name: 'AES-GCM', + ...aesBase, + + encrypt(params, key, data) { + const { iv, tagLength, additionalData } = params; + + const ivBuf = toBuffer(iv); + const secretKey = key[kKeyMaterial]; + const cipher = `aes-${secretKey.symmetricKeySize << 3}-gcm`; + + const authTagLength = tagLength === undefined ? 16 : tagLength >> 3; + const c = createCipheriv(cipher, secretKey, ivBuf, { authTagLength }); + if (additionalData !== undefined) + c.setAAD(additionalData); + return Buffer.concat([c.update(data), c.final(), c.getAuthTag()]); + }, + + decrypt(params, key, data) { + const { iv, tagLength, additionalData } = params; + + const ivBuf = toBuffer(iv); + const secretKey = key[kKeyMaterial]; + const cipher = `aes-${secretKey.symmetricKeySize << 3}-gcm`; + + const authTagLength = tagLength === undefined ? 16 : tagLength >> 3; + const c = createDecipheriv(cipher, secretKey, ivBuf, { authTagLength }); + if (additionalData !== undefined) + c.setAAD(additionalData); + c.setAuthTag(data.slice(data.byteLength - authTagLength, data.length)); + return Buffer.concat([ + c.update(data.slice(0, data.byteLength - authTagLength)), + c.final() + ]); + } +}; + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#aes-kw +module.exports.AES_KW = { + name: 'AES-KW', + ...aesBase, + + defaultIV: Buffer.from('A6A6A6A6A6A6A6A6', 'hex'), + + async wrapKey(params, key, data) { + const secretKey = key[kKeyMaterial]; + const cipher = `aes${secretKey.symmetricKeySize << 3}-wrap`; + + const c = createCipheriv(cipher, secretKey, this.defaultIV); + return Buffer.concat([c.update(data), c.final()]); + }, + + async unwrapKey(params, key, data) { + const secretKey = key[kKeyMaterial]; + const cipher = `aes${secretKey.symmetricKeySize << 3}-wrap`; + + const c = createDecipheriv(cipher, secretKey, this.defaultIV); + return Buffer.concat([c.update(data), c.final()]); + } +}; diff --git a/lib/algorithms/hkdf.js b/lib/algorithms/hkdf.js new file mode 100644 index 0000000..5c8d619 --- /dev/null +++ b/lib/algorithms/hkdf.js @@ -0,0 +1,57 @@ +'use strict'; + +const { createHmac } = require('crypto'); + +const { OperationError, NotSupportedError } = require('../errors'); +const { kKeyMaterial, CryptoKey } = require('../key'); +const { + limitUsages, + opensslHashFunctionName, + toBuffer +} = require('../util'); + +function hmac(hash, key, data) { + return createHmac(hash, key).update(data).digest(); +} + +module.exports.HKDF = { + name: 'HKDF', + + deriveBits(params, key, length) { + if (length === null) + throw new OperationError(); + length >>= 3; + + const hashFn = opensslHashFunctionName(params.hash); + + const keyDerivationKey = key[kKeyMaterial]; + const hmacKey = toBuffer(params.salt); + const pseudoRandomKey = hmac(hashFn, hmacKey, keyDerivationKey); + const hashLen = pseudoRandomKey.length; + const N = Math.ceil(length / hashLen); + + const blocks = new Array(N); + let t = Buffer.alloc(0); + const info = toBuffer(params.info); + for (let i = 0; i < N; i++) { + const data = Buffer.concat([t, info, Buffer.from([i + 1])], + t.length + info.length + 1); + t = blocks[i] = hmac(hashFn, pseudoRandomKey, data); + } + const all = Buffer.concat(blocks, N * hashLen); + return all.slice(0, length); + }, + + importKey(keyFormat, keyData, params, extractable, keyUsages) { + if (keyFormat !== 'raw') + throw new NotSupportedError(); + + limitUsages(keyUsages, ['deriveKey', 'deriveBits']); + + if (extractable !== false) + throw new SyntaxError(); + + return new CryptoKey('secret', { name: 'HKDF' }, extractable, keyUsages, + Buffer.from(keyData)); + } +}; diff --git a/lib/algorithms/pbkdf2.js b/lib/algorithms/pbkdf2.js new file mode 100644 index 0000000..d7d5663 --- /dev/null +++ b/lib/algorithms/pbkdf2.js @@ -0,0 +1,43 @@ +'use strict'; + +const { pbkdf2 } = require('crypto'); + +const { OperationError, NotSupportedError } = require('../errors'); +const { kKeyMaterial, CryptoKey } = require('../key'); +const { limitUsages, opensslHashFunctionName } = require('../util'); + +module.exports.PBKDF2 = { + name: 'PBKDF2', + + deriveBits(params, key, length) { + const { hash, salt, iterations } = params; + + if (length === null || length % 8 !== 0) + throw new OperationError(); + length >>= 3; + + const hashFn = opensslHashFunctionName(hash); + + const keyDerivationKey = key[kKeyMaterial]; + return new Promise((resolve, reject) => { + pbkdf2(keyDerivationKey, salt, iterations, length, hashFn, (err, key) => { + if (err) + return reject(err); + resolve(key); + }); + }); + }, + + importKey(keyFormat, keyData, params, extractable, keyUsages) { + if (keyFormat !== 'raw') + throw new NotSupportedError(); + + limitUsages(keyUsages, ['deriveKey', 'deriveBits']); + + if (extractable !== false) + throw new SyntaxError(); + + return new CryptoKey('secret', { name: 'PBKDF2' }, extractable, keyUsages, + Buffer.from(keyData)); + } +}; diff --git a/lib/algorithms/sha.js b/lib/algorithms/sha.js new file mode 100644 index 0000000..b7b0978 --- /dev/null +++ b/lib/algorithms/sha.js @@ -0,0 +1,22 @@ +'use strict'; + +const { createHash } = require('crypto'); + +function implement(name, opensslName) { + return { + name, + + digest(params, data) { + return createHash(opensslName).update(data).digest(); + }, + + 'get hash function'() { + return opensslName; + } + }; +} + +module.exports.SHA_1 = implement('SHA-1', 'sha1'); +module.exports.SHA_256 = implement('SHA-256', 'sha256'); +module.exports.SHA_384 = implement('SHA-384', 'sha384'); +module.exports.SHA_512 = implement('SHA-512', 'sha512'); diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..51b604f --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,20 @@ +'use strict'; + +class NotSupportedError extends Error {} +NotSupportedError.prototype.name = 'NotSupportedError'; + +class InvalidAccessError extends Error {} +InvalidAccessError.prototype.name = 'InvalidAccessError'; + +class OperationError extends Error {} +OperationError.prototype.name = 'OperationError'; + +class QuotaExceededError extends Error {} +QuotaExceededError.prototype.name = 'QuotaExceededError'; + +module.exports = { + NotSupportedError, + InvalidAccessError, + OperationError, + QuotaExceededError +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..8d0fa6a --- /dev/null +++ b/lib/index.js @@ -0,0 +1,10 @@ +'use strict'; + +const { getRandomValues } = require('./random'); +const subtle = require('./subtle'); + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#crypto-interface +module.exports = { + getRandomValues, + subtle +}; diff --git a/lib/key.js b/lib/key.js new file mode 100644 index 0000000..4a2b3db --- /dev/null +++ b/lib/key.js @@ -0,0 +1,52 @@ +'use strict'; + +const { InvalidAccessError } = require('./errors'); + +const kType = Symbol('kType'); +const kAlgorithm = Symbol('kAlgorithm'); +const kExtractable = Symbol('kExtractable'); +const kUsages = Symbol('kUsages'); + +const kKeyMaterial = Symbol('kKeyMaterial'); + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#cryptokey-interface +class CryptoKey { + constructor(type, algorithm, extractable, usages, keyMaterial) { + this[kType] = type; + this[kAlgorithm] = algorithm; + this[kExtractable] = extractable; + this[kUsages] = new Set(usages); + this[kKeyMaterial] = keyMaterial; + } + + get type() { + return this[kType]; + } + + get extractable() { + return this[kExtractable]; + } + + get algorithm() { + return this[kAlgorithm]; + } + + get usages() { + return [...this[kUsages]]; + } +} + +function requireKeyUsage(key, usage) { + if (!key[kUsages].has(usage)) + throw new InvalidAccessError(); +} + +module.exports = { + kType, + kAlgorithm, + kExtractable, + kUsages, + kKeyMaterial, + CryptoKey, + requireKeyUsage +}; diff --git a/lib/random.js b/lib/random.js new file mode 100644 index 0000000..5bc1f15 --- /dev/null +++ b/lib/random.js @@ -0,0 +1,14 @@ +'use strict'; + +const { randomFillSync } = require('crypto'); + +const { QuotaExceededError } = require('./errors'); + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#dfn-Crypto-method-getRandomValues +module.exports.getRandomValues = (array) => { + if (array.byteLength > 65536) + throw new QuotaExceededError(); + + randomFillSync(array); + return array; +}; diff --git a/lib/subtle.js b/lib/subtle.js new file mode 100644 index 0000000..8e82837 --- /dev/null +++ b/lib/subtle.js @@ -0,0 +1,165 @@ +'use strict'; + +const { getAlgorithm } = require('./algorithms'); +const { InvalidAccessError } = require('./errors'); +const { kAlgorithm, kExtractable, requireKeyUsage } = require('./key'); +const { toBuffer } = require('./util'); + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-encrypt +async function encrypt(algorithm, key, data) { + const alg = getAlgorithm(algorithm, 'encrypt'); + if (key.algorithm.name !== alg.name) + throw new InvalidAccessError(); + + requireKeyUsage(key, 'encrypt'); + const buffer = toBuffer(data); + return alg.encrypt(algorithm, key, buffer); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-decrypt +async function decrypt(algorithm, key, data) { + const alg = getAlgorithm(algorithm, 'decrypt'); + if (key.algorithm.name !== alg.name) + throw new InvalidAccessError(); + + requireKeyUsage(key, 'decrypt'); + const buffer = toBuffer(data); + return alg.decrypt(algorithm, key, buffer); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-sign +async function sign(algorithm, key, data) { + const alg = getAlgorithm(algorithm, 'sign'); + if (alg.name !== key[kAlgorithm].name) + throw new InvalidAccessError(); + + requireKeyUsage(key, 'sign'); + return alg.sign(algorithm, key, data); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-verify +async function verify(algorithm, key, signature, data) { + const alg = getAlgorithm(algorithm, 'verify'); + if (alg.name !== key[kAlgorithm].name) + throw new InvalidAccessError(); + + requireKeyUsage(key, 'verify'); + return alg.verify(algorithm, key, signature, data); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-digest +async function digest(algorithm, data) { + const buffer = toBuffer(data); + const alg = getAlgorithm(algorithm, 'digest'); + return alg.digest(algorithm, buffer); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-generateKey +async function generateKey(algorithm, extractable, keyUsages) { + const alg = getAlgorithm(algorithm, 'generateKey'); + return alg.generateKey(algorithm, extractable, keyUsages); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-deriveKey +async function deriveKey(algorithm, baseKey, derivedKeyType, extractable, + keyUsages) { + const alg = getAlgorithm(algorithm, 'deriveBits'); + const keyAlg = getAlgorithm(derivedKeyType, 'get key length'); + const length = await keyAlg['get key length'](derivedKeyType); + requireKeyUsage(baseKey, 'deriveKey'); + const bits = await alg.deriveBits(algorithm, baseKey, length, extractable, + keyUsages); + return keyAlg.importKey('raw', bits, derivedKeyType, extractable, keyUsages); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-deriveBits +async function deriveBits(algorithm, key, length) { + const alg = getAlgorithm(algorithm, 'deriveBits'); + requireKeyUsage(key, 'deriveBits'); + return alg.deriveBits(algorithm, key, length); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-importKey +async function importKey(keyFormat, keyData, algorithm, extractable, + keyUsages) { + const alg = getAlgorithm(algorithm, 'importKey'); + return alg.importKey(keyFormat, keyData, algorithm, extractable, keyUsages); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-exportKey +async function exportKey(format, key) { + const alg = getAlgorithm(key.algorithm, 'exportKey'); + if (!key.extractable) + throw new InvalidAccessError(); + return alg.exportKey(format, key); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-wrapKey +async function wrapKey(format, key, wrappingKey, wrapAlgorithm) { + let wrapFn, alg; + try { + alg = getAlgorithm(wrapAlgorithm, wrapFn = 'wrapKey'); + } catch { + alg = getAlgorithm(wrapAlgorithm, wrapFn = 'encrypt'); + } + + if (wrappingKey[kAlgorithm].name !== alg.name) + throw new InvalidAccessError(); + + requireKeyUsage(wrappingKey, 'wrapKey'); + + const exportAlg = getAlgorithm(key.algorithm, 'exportKey'); + if (!key[kExtractable]) + throw new InvalidAccessError(); + + let bytes = exportAlg.exportKey(format, key); + if (format === 'jwk') + bytes = Buffer.from(JSON.stringify(bytes), 'utf8'); + + return alg[wrapFn](wrapAlgorithm, wrappingKey, bytes); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-unwrapKey +async function unwrapKey(format, wrappedKey, unwrappingKey, + unwrapAlgorithm, unwrappedKeyAlgorithm, + extractable, keyUsages) { + let unwrapFn, alg; + try { + alg = getAlgorithm(unwrapAlgorithm, unwrapFn = 'unwrapKey'); + } catch { + alg = getAlgorithm(unwrapAlgorithm, unwrapFn = 'decrypt'); + } + + const importAlg = getAlgorithm(unwrappingKey.algorithm, 'importKey'); + + if (unwrappingKey[kAlgorithm].name !== alg.name) + throw new InvalidAccessError(); + + requireKeyUsage(unwrappingKey, 'unwrapKey'); + + let key = await alg[unwrapFn](unwrapAlgorithm, unwrappingKey, wrappedKey); + if (format === 'jwk') + key = JSON.parse(key.toString('utf8')); + + return importAlg.importKey(format, key, unwrappedKeyAlgorithm, extractable, + keyUsages); +} + +// Spec: https://www.w3.org/TR/WebCryptoAPI/#subtlecrypto-interface +module.exports = { + encrypt, + decrypt, + sign, + verify, + digest, + + generateKey, + deriveKey, + deriveBits, + + importKey, + exportKey, + + wrapKey, + unwrapKey +}; diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..d088910 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,23 @@ +'use strict'; + +const algorithms = require('./algorithms'); + +module.exports.toBuffer = (source) => { + if (ArrayBuffer.isView(source)) { + return Buffer.from(source.buffer, source.byteOffset, source.byteLength); + } else { + return Buffer.from(source); + } +}; + +module.exports.opensslHashFunctionName = (algorithm) => { + const op = 'get hash function'; + return algorithms.getAlgorithm(algorithm, op)[op](); +}; + +module.exports.limitUsages = (usages, allowed) => { + for (const usage of usages) { + if (!allowed.includes(usage)) + throw new SyntaxError(); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..1b943c0 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "main": "./lib/index.js", + "devDependencies": { + "babel-eslint": "^10.0.2", + "eslint": "^6.1.0", + "mocha": "^6.2.0" + }, + "scripts": { + "test": "mocha --recursive ./test/", + "lint": "eslint ." + }, + "license": "MIT", + "private": true, + "author": "Tobias Nießen ", + "bugs": "https://github.com/nodejs/webcrypto/issues", + "repository": { + "type": "git", + "url": "https://github.com/nodejs/webcrypto.git" + }, + "engines": { + "node": "^12.x" + } +} diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 0000000..7eeefc3 --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/test/algorithms/aes.js b/test/algorithms/aes.js new file mode 100644 index 0000000..810eb92 --- /dev/null +++ b/test/algorithms/aes.js @@ -0,0 +1,254 @@ +'use strict'; + +const assert = require('assert'); +const { randomBytes } = require('crypto'); + +const { subtle } = require('../../'); + +function twice(buf) { + return Buffer.concat([buf, buf], buf.length * 2); +} + +function testGenImportExport(name) { + return async () => { + const key1 = await subtle.generateKey({ name, length: 192 }, true, + ['encrypt', 'decrypt']); + assert.strictEqual(key1.algorithm.name, name); + const key2 = await subtle.generateKey({ name, length: 192 }, true, + ['encrypt', 'decrypt']); + assert.strictEqual(key2.algorithm.name, name); + const key3 = await subtle.generateKey({ name, length: 256 }, true, + ['encrypt', 'decrypt']); + assert.strictEqual(key3.algorithm.name, name); + + const expKey1 = await subtle.exportKey('raw', key1); + assert(Buffer.isBuffer(expKey1)); + assert.strictEqual(expKey1.length, 24); + const expKey2 = await subtle.exportKey('raw', key2); + assert(Buffer.isBuffer(expKey2)); + assert.strictEqual(expKey2.length, 24); + const expKey3 = await subtle.exportKey('raw', key3); + assert(Buffer.isBuffer(expKey3)); + assert.strictEqual(expKey3.length, 32); + + assert.notDeepStrictEqual(expKey1, expKey2); + + const impKey1 = await subtle.importKey('raw', expKey1, name, true, + ['encrypt', 'decrypt']); + const impKey2 = await subtle.importKey('raw', expKey2, name, true, + ['encrypt', 'decrypt']); + const impKey3 = await subtle.importKey('raw', expKey3, name, true, + ['encrypt', 'decrypt']); + + assert.deepStrictEqual(await subtle.exportKey('raw', impKey1), expKey1); + assert.deepStrictEqual(await subtle.exportKey('raw', impKey2), expKey2); + assert.deepStrictEqual(await subtle.exportKey('raw', impKey3), expKey3); + }; +} + +describe('AES-CTR', () => { + it('should generate, import and export keys', + testGenImportExport('AES-CTR')); + + it('should encrypt and decrypt', async () => { + const keyData = Buffer.from('36adfe538cc234279e4cbb29e1f27af5', 'hex'); + const counter = Buffer.from('2159e5bd415791990e52b5c825572994', 'hex'); + const length = 128; + + const key = await subtle.importKey('raw', keyData, 'AES-CTR', false, + ['encrypt', 'decrypt']); + + const plaintext = Buffer.from('Hello WebCrypto!', 'utf8'); + const ciphertext = await subtle.encrypt({ + name: 'AES-CTR', + counter, + length + }, key, plaintext); + assert.strictEqual(ciphertext.toString('hex'), + '0bdbe0f2de637f43b9d86f8bb0ba5f05'); + + const deciphered = await subtle.decrypt({ + name: 'AES-CTR', + counter, + length + }, key, ciphertext); + assert.deepStrictEqual(deciphered, plaintext); + }); + + it('should handle the "length" parameter', async () => { + const blockSize = 16; + const key = await subtle.generateKey({ name: 'AES-CTR', length: 192 }, + false, ['encrypt', 'decrypt']); + + // In this case, only the last bit of the IV will be flipped between blocks, + // meaning that every second block will be XOR'd with the same bit stream. + const plaintext = twice(randomBytes(2 * blockSize)); + let counter = randomBytes(blockSize); + let length = 1; + let ciphertext = await subtle.encrypt({ name: 'AES-CTR', counter, length }, + key, plaintext); + assert.strictEqual(ciphertext.length, 4 * blockSize); + const encryptedFirstHalf = ciphertext.slice(0, 2 * blockSize); + const encryptedSecondHalf = ciphertext.slice(2 * blockSize); + assert.deepStrictEqual(encryptedFirstHalf, encryptedSecondHalf); + + let decrypted = await subtle.decrypt({ name: 'AES-CTR', counter, length }, + key, ciphertext); + assert.deepStrictEqual(decrypted, plaintext); + + // This is slightly more tricky: We allow incrementing the last 127 bits, + // which will not lead to any repetitions that we could test for. However, + // we can pick an IV that will cause an overflow, which would usually cause + // the MSB to be flipped, but since the MSB is not within the last 127 bits, + // this cannot happen. We just need to verify that the second block was + // encrypted using the correct IV (with an unmodified MSB). + counter = Buffer.from('7fffffffffffffffffffffffffffffff', 'hex'); + length = 127; + ciphertext = await subtle.encrypt({ name: 'AES-CTR', counter, length }, + key, plaintext); + const expectedIV = Buffer.from('00000000000000000000000000000000', 'hex'); + const expectedSecondBlock = await subtle.encrypt({ + name: 'AES-CTR', + counter: expectedIV, + length: 128 + }, key, plaintext.slice(blockSize, 2 * blockSize)); + assert.deepStrictEqual(ciphertext.slice(blockSize, 2 * blockSize), + expectedSecondBlock); + + decrypted = await subtle.decrypt({ name: 'AES-CTR', counter, length }, key, + ciphertext); + assert.deepStrictEqual(decrypted, plaintext); + }); +}); + +describe('AES-CBC', () => { + it('should generate, import and export keys', + testGenImportExport('AES-CBC')); + + it('should encrypt and decrypt', async () => { + const keyData = Buffer.from('36adfe538cc234279e4cbb29e1f27af5', 'hex'); + const iv = Buffer.from('2159e5bd415791990e52b5c825572994', 'hex'); + + const key = await subtle.importKey('raw', keyData, 'AES-CBC', false, + ['encrypt', 'decrypt']); + + const plaintext = Buffer.from('Hello WebCrypto!', 'utf8'); + const ciphertext = await subtle.encrypt({ + name: 'AES-CBC', + iv + }, key, plaintext); + assert.strictEqual(ciphertext.toString('hex'), + '8bb6173879b0f7a8899397e0fde3a3c88c69e86b18' + + 'eb74f8629be60287c89552'); + + const deciphered = await subtle.decrypt({ + name: 'AES-CBC', + iv + }, key, ciphertext); + assert.deepStrictEqual(deciphered, plaintext); + }); +}); + +describe('AES-GCM', () => { + it('should generate, import and export keys', + testGenImportExport('AES-GCM')); + + it('should encrypt and decrypt', async () => { + const keyData = Buffer.from('36adfe538cc234279e4cbb29e1f27af5', 'hex'); + const iv = Buffer.from('2159e5bd415791990e52b5c825572994', 'hex'); + + const key = await subtle.importKey('raw', keyData, 'AES-GCM', false, + ['encrypt', 'decrypt']); + + const plaintext = Buffer.from('Hello WebCrypto!', 'utf8'); + const ciphertext = await subtle.encrypt({ + name: 'AES-GCM', + iv + }, key, plaintext); + assert.strictEqual(ciphertext.toString('hex'), + '7080337fe4a1f8d8d96fa061ccfdb8cda6dacbf3f2' + + '7ef1dc85190feddc4befdd'); + + const deciphered = await subtle.decrypt({ + name: 'AES-GCM', + iv + }, key, ciphertext); + assert.deepStrictEqual(deciphered, plaintext); + }); + + it('should handle the "tagLength" parameter', async () => { + const keyData = Buffer.from('36adfe538cc234279e4cbb29e1f27af5', 'hex'); + const iv = Buffer.from('2159e5bd415791990e52b5c825572994', 'hex'); + + const key = await subtle.importKey('raw', keyData, 'AES-GCM', false, + ['encrypt', 'decrypt']); + + const plaintext = Buffer.from('Hello WebCrypto!', 'utf8'); + const ciphertext = await subtle.encrypt({ + name: 'AES-GCM', + iv, + tagLength: 112 + }, key, plaintext); + assert.strictEqual(ciphertext.toString('hex'), + '7080337fe4a1f8d8d96fa061ccfdb8cda6dacbf3f2' + + '7ef1dc85190feddc4b'); + + const deciphered = await subtle.decrypt({ + name: 'AES-GCM', + iv, + tagLength: 112 + }, key, ciphertext); + assert.deepStrictEqual(deciphered, plaintext); + }); + + it('should support all IV lengths', async () => { + const keyData = Buffer.from('36adfe538cc234279e4cbb29e1f27af5', 'hex'); + const iv = twice(Buffer.from('2159e5bd415791990e52b5c825572994', 'hex')); + + const key = await subtle.importKey('raw', keyData, 'AES-GCM', false, + ['encrypt', 'decrypt']); + + const plaintext = Buffer.from('Hello WebCrypto!', 'utf8'); + const ciphertext = await subtle.encrypt({ + name: 'AES-GCM', + iv, + tagLength: 112 + }, key, plaintext); + assert.strictEqual(ciphertext.toString('hex'), + '2f136ce56f36acf081476d227c0fb89ed4e0fcd07b' + + '3b8de9d412f99a2c2d'); + + const deciphered = await subtle.decrypt({ + name: 'AES-GCM', + iv, + tagLength: 112 + }, key, ciphertext); + assert.deepStrictEqual(deciphered, plaintext); + }); +}); + +describe('AES-KW', () => { + it('should generate, import and export keys', + testGenImportExport('AES-KW')); + + it('should wrap and unwrap keys', async () => { + const wrappingKey = await subtle.generateKey({ + name: 'AES-KW', + length: 192 + }, false, ['wrapKey', 'unwrapKey']); + const keyToWrap = await subtle.generateKey({ + name: 'AES-CBC', + length: 256 + }, true, ['encrypt', 'decrypt']); + const wrappedKey = await subtle.wrapKey('raw', keyToWrap, wrappingKey, + 'AES-KW'); + assert(Buffer.isBuffer(wrappedKey)); + assert.strictEqual(wrappedKey.length, (256 + 64) / 8); + + const unwrappedKey = await subtle.unwrapKey('raw', wrappedKey, wrappingKey, + 'AES-KW', 'AES-CBC', true, + ['encrypt', 'decrypt']); + assert.deepStrictEqual(await subtle.exportKey('raw', unwrappedKey), + await subtle.exportKey('raw', keyToWrap)); + }); +}); diff --git a/test/algorithms/hkdf.js b/test/algorithms/hkdf.js new file mode 100644 index 0000000..5245519 --- /dev/null +++ b/test/algorithms/hkdf.js @@ -0,0 +1,57 @@ +'use strict'; + +const assert = require('assert'); + +const { subtle } = require('../../'); + +describe('HKDF', () => { + it('should import keys', async () => { + const keyBuffer = Buffer.from('passphrase', 'utf8'); + const key = await subtle.importKey('raw', keyBuffer, 'HKDF', false, + ['deriveBits']); + assert.strictEqual(key.algorithm.name, 'HKDF'); + assert.strictEqual(key.type, 'secret'); + assert.strictEqual(key.extractable, false); + assert.deepStrictEqual(key.usages, ['deriveBits']); + }); + + it('should produce correct outputs', async () => { + const keyBuffer = Buffer.from('passphrase', 'utf8'); + const key = await subtle.importKey('raw', keyBuffer, 'HKDF', false, + ['deriveBits']); + + const bits = await subtle.deriveBits({ + name: 'HKDF', + hash: 'SHA-384', + salt: Buffer.from('b19a9d6d7f7d2e9e', 'hex'), + info: Buffer.alloc(0) + }, key, 128); + + assert(Buffer.isBuffer(bits)); + assert.deepStrictEqual(bits.toString('hex'), + '4b52236af0e6516384e531e618c95b96'); + }); + + it('should produce correct keys', async () => { + const keyBuffer = Buffer.from('passphrase', 'utf8'); + const key = await subtle.importKey('raw', keyBuffer, 'HKDF', false, + ['deriveKey']); + + const derivedKey = await subtle.deriveKey({ + name: 'HKDF', + hash: 'SHA-1', + salt: Buffer.from('b19a9d6d7f7d2e9e', 'hex'), + info: Buffer.alloc(0) + }, key, { + name: 'AES-CTR', + length: 256 + }, true, ['encrypt', 'decrypt']); + + assert.strictEqual(derivedKey.type, 'secret'); + + const bits = await subtle.exportKey('raw', derivedKey); + assert.strictEqual(bits.toString('hex'), + '70d38acbfd289f4869069d254e7addff' + + 'c1eec6cf90dc0f8f1598b97828f23b3f'); + }); +}); diff --git a/test/algorithms/pbkdf2.js b/test/algorithms/pbkdf2.js new file mode 100644 index 0000000..a61515e --- /dev/null +++ b/test/algorithms/pbkdf2.js @@ -0,0 +1,58 @@ +'use strict'; + +const assert = require('assert'); + +const { subtle } = require('../../'); + +describe('PBKDF2', () => { + it('should import keys', async () => { + const keyBuffer = Buffer.from('passphrase', 'utf8'); + const key = await subtle.importKey('raw', keyBuffer, 'PBKDF2', false, + ['deriveBits']); + assert.strictEqual(key.algorithm.name, 'PBKDF2'); + assert.strictEqual(key.type, 'secret'); + assert.strictEqual(key.extractable, false); + assert.deepStrictEqual(key.usages, ['deriveBits']); + }); + + it('should produce correct outputs', async () => { + const keyBuffer = Buffer.from('passphrase', 'utf8'); + const key = await subtle.importKey('raw', keyBuffer, 'PBKDF2', false, + ['deriveBits']); + + const bits = await subtle.deriveBits({ + name: 'PBKDF2', + iterations: 1000, + hash: 'SHA-512', + salt: Buffer.from('Hello world', 'utf8') + }, key, 256); + + assert(Buffer.isBuffer(bits)); + assert.strictEqual(bits.toString('hex'), + '5552743c1053eeb1c91c1b33c806efd6' + + '2585e90c932bcdad4814a572537bdef5'); + }); + + it('should produce correct keys', async () => { + const keyBuffer = Buffer.from('passphrase', 'utf8'); + const key = await subtle.importKey('raw', keyBuffer, 'PBKDF2', false, + ['deriveKey']); + + const derivedKey = await subtle.deriveKey({ + name: 'PBKDF2', + iterations: 1000, + hash: 'SHA-512', + salt: Buffer.from('Hello world', 'utf8') + }, key, { + name: 'AES-CTR', + length: 256 + }, true, ['encrypt', 'decrypt']); + + assert.strictEqual(derivedKey.type, 'secret'); + + const bits = await subtle.exportKey('raw', derivedKey); + assert.strictEqual(bits.toString('hex'), + '5552743c1053eeb1c91c1b33c806efd6' + + '2585e90c932bcdad4814a572537bdef5'); + }); +}); diff --git a/test/algorithms/sha.js b/test/algorithms/sha.js new file mode 100644 index 0000000..3709fd8 --- /dev/null +++ b/test/algorithms/sha.js @@ -0,0 +1,47 @@ +'use strict'; + +const assert = require('assert'); + +const { subtle } = require('../../'); + +describe('SHA', () => { + + it('should support SHA-1', async () => { + const data = Buffer.from('Hello world', 'utf8'); + const digest = await subtle.digest('SHA-1', data); + assert(Buffer.isBuffer(digest)); + assert.strictEqual(digest.toString('hex'), + '7b502c3a1f48c8609ae212cdfb639dee39673f5e'); + }); + + it('should support SHA-256', async () => { + const data = Buffer.from('Hello world', 'utf8'); + const digest = await subtle.digest('SHA-256', data); + assert(Buffer.isBuffer(digest)); + assert.strictEqual(digest.toString('hex'), + '64ec88ca00b268e5ba1a35678a1b5316' + + 'd212f4f366b2477232534a8aeca37f3c'); + }); + + it('should support SHA-384', async () => { + const data = Buffer.from('Hello world', 'utf8'); + const digest = await subtle.digest('SHA-384', data); + assert(Buffer.isBuffer(digest)); + assert.strictEqual(digest.toString('hex'), + '9203b0c4439fd1e6ae5878866337b7c5' + + '32acd6d9260150c80318e8ab8c27ce33' + + '0189f8df94fb890df1d298ff360627e1'); + }); + + it('should support SHA-512', async () => { + const data = Buffer.from('Hello world', 'utf8'); + const digest = await subtle.digest('SHA-512', data); + assert(Buffer.isBuffer(digest)); + assert.strictEqual(digest.toString('hex'), + 'b7f783baed8297f0db917462184ff4f0' + + '8e69c2d5e5f79a942600f9725f58ce1f' + + '29c18139bf80b06c0fff2bdd34738452' + + 'ecf40c488c22a7e3d80cdf6f9c1c0d47'); + }); + +}); diff --git a/test/default-export.js b/test/default-export.js new file mode 100644 index 0000000..77df9ba --- /dev/null +++ b/test/default-export.js @@ -0,0 +1,19 @@ +'use strict'; + +const assert = require('assert'); + +const crypto = require('../'); + +describe('Default export', () => { + it('should have getRandomBytes', () => { + assert.strictEqual(typeof crypto.getRandomValues, 'function'); + }); + + it('should have subtle', () => { + assert.strictEqual(typeof crypto.subtle, 'object'); + }); + + it('should not have any other properties', () => { + assert.strictEqual(Object.keys(crypto).length, 2); + }); +}); diff --git a/test/random.js b/test/random.js new file mode 100644 index 0000000..5dd9a24 --- /dev/null +++ b/test/random.js @@ -0,0 +1,28 @@ +'use strict'; + +const assert = require('assert'); + +const { getRandomValues } = require('../'); + +describe('crypto.getRandomBytes', () => { + it('should exist', () => { + assert.strictEqual(typeof getRandomValues, 'function'); + }); + + it('should return the input parameter', () => { + const buf = Buffer.alloc(1024); + assert.strictEqual(getRandomValues(buf), buf); + }); + + it('should overwrite the buffer', () => { + const zero = Buffer.alloc(1024, 0); + const buf = getRandomValues(Buffer.alloc(1024, 0)); + assert(!buf.equals(zero)); + }); + + it('should produce a different output each time', () => { + const buf1 = getRandomValues(Buffer.alloc(1024)); + const buf2 = getRandomValues(Buffer.alloc(1024)); + assert(!buf1.equals(buf2)); + }); +}); diff --git a/test/subtle.js b/test/subtle.js new file mode 100644 index 0000000..36eea10 --- /dev/null +++ b/test/subtle.js @@ -0,0 +1,23 @@ +'use strict'; + +const assert = require('assert'); + +const { subtle } = require('../'); + +describe('crypto.subtle', () => { + const fns = [ + 'encrypt', 'decrypt', 'sign', 'verify', 'digest', + 'generateKey', 'deriveKey', 'deriveBits', + 'importKey', 'exportKey', + 'wrapKey', 'unwrapKey' + ]; + + it('should have all SubtleCrypto functions', () => { + for (const key of fns) + assert.strictEqual(typeof subtle[key], 'function'); + }); + + it('should not have any other properties', () => { + assert.strictEqual(Object.keys(subtle).length, fns.length); + }); +});