diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e717f5e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..20c9288 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,14 @@ +{ + "env": { + "node": true + }, + "globals": { + "window": true + }, + "rules": { + "no-shadow": [0], + "no-underscore-dangle": [0], + "strict": [2, "global"], + "yoda": [0] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4041fb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +env/ +node_modules +coverage/ +dist/ +old/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..1440973 --- /dev/null +++ b/.npmignore @@ -0,0 +1,16 @@ +# self + revision control +.npmignore +.gitignore +.git +.hg +.hgtags + +# build environment +.editorconfig +.eslintrc +gulpfile.js + +runas +env/ +coverage/ +test/ diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..ac82602 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,5 @@ +# node-jose Contributors +# Listed alphabetically by surname + +Matthew A. Miller +Ian W. Remmel diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e9a1e2e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ + +# [0.3.0] (2015-09-11) + +Initial public release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c12cc00 --- /dev/null +++ b/README.md @@ -0,0 +1,454 @@ +# node-jose # + +A JavaScript implementation of the JSON Object Signing and Encryption (JOSE) for current web browsers and node.js-based servers. This library implements (wherever possible) all algorithms, formats, and options in [JWS](https://tools.ietf.org/html/rfc7515 "Jones, M., J. Bradley and N. Sakimura, 'JSON Web Signature (JWS)' RFC 7515, May 2015"), [JWE](https://tools.ietf.org/html/rfc7516 "Jones, M. and J. Hildebrand 'JSON Web Encryption (JWE)', RFC 7516, May 2015"), [JWK](https://tools.ietf/html/rfc7517 "Jones, M., 'JSON Web Key (JWK)', RFC 7517, May 2015"), and [JWA](https://tools.ietf/html/rfc7518 "Jones, M., 'JSON Web Algorithms (JWA)', RFC 7518, May 2015") and uses native cryptographic support ([WebCrypto API](http://www.w3.org/TR/WebCryptoAPI/) or node.js' "[crypto](https://nodejs.org/api/crypto.html)" module) where feasible. + + + + + +- [Installing](#installing) +- [Basics](#basics) +- [Keys and Key Stores](#keys-and-key-stores) + - [Obtaining a KeyStore](#obtaining-a-keystore) + - [Exporting a KeyStore](#exporting-a-keystore) + - [Retrieving Keys](#retrieving-keys) + - [Searching for Keys](#searching-for-keys) + - [Managing Keys](#managing-keys) + - [Importing and Exporting a Single Key](#importing-and-exporting-a-single-key) +- [Signatures](#signatures) + - [Signing Content](#signing-content) + - [Verifying a JWS](#verifying-a-jws) +- [Encryption](#encryption) + - [Encrypting Content](#encrypting-content) + - [Decrypting a JWE](#decrypting-a-jwe) +- [Useful Utilities](#useful-utilities) + - [Converting to Buffer](#converting-to-buffer) + - [URI-Safe Base64](#uri-safe-base64) + - [Random Bytes](#random-bytes) + + + +## Installing ## + +To install the latest from the repository: + +``` + npm install git+ssh://git@github.com:cisco/node-jose.git +``` + +Or to install a specific release from the repository: + +``` + npm install git+ssh://git@github.com:cisco/node-jose.git#0.3.0 +``` + +## Basics ## + +Require the library as normal: + +``` +var jose = require('node-jose'); +``` + +This library uses [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) for nearly every operation. + +The content to be signed/encrypted or returned from being verified/decrypted are [Buffer](https://nodejs.org/api/buffer.html) objects. + +## Keys and Key Stores ## + +The `jose.JWK` namespace deals with JWK and JWK-sets. + +* `jose.JWK.Key` is a logical representation of a JWK, and is the "raw" entry point for various cryptographic operations (e.g., sign, verify, encrypt, decrypt). +* `jose.JWK.KeyStore` represents a collection of Keys. + +Creating a JWE or JWS ultimately require one or more explicit Key objects. + +Processing a JWE or JWS relies on a KeyStore. + +### Obtaining a KeyStore ### +To create an empty keystore: +``` +keystore = jose.JWK.createKeyStore(); +``` + +To import a JWK-set as a keystore: +``` +// {input} is a String or JSON object representing the JWK-set +jose.JWK.asKeyStore(input). + then(function(result) { + // {result} is a jose.JWK.KeyStore + keystore = result; + }); +``` + +### Exporting a KeyStore ### + +To export the public keys of a keystore as a JWK-set: +``` +output = keystore.toJSON(); +``` + +To export **all** the keys of a keystore: +``` +output = keystore.toJSON(true); +``` + +### Retrieving Keys ### + +To retrieve a key from a keystore: +``` +// by 'kid' +key = keystore.get(kid); +``` + +This retrieves the first key that matches the given {kid}. If multiple keys have the same {kid}, you can further narrow what to retrieve: + +``` +// ... and by 'kty' +key = keystore.get(kid, { kty: 'RSA' }); + +// ... and by 'use' +key = keystore.get(kid, { use: 'enc' }); + +// ... and by 'alg' +key = keystore.get(kid, { use: 'RSA-OAEP' }); + +// ... and by 'kty' and 'use' +key = keystore.get(kid, { kty: 'RSA', use: 'enc' }); + +// same as above, but with a single {props} argument +key = keystore.get({ kid: kid, kty: 'RSA', use: 'enc' }); +``` + +### Searching for Keys ### + +To retrieve all the keys from a keystore: +``` +everything = keystore.all(); +``` + +`all()` can be filtered much like `get()`: +``` +// filter by 'kid' +everything = keystore.all({ kid: kid }); + +// filter by 'kty' +everything = keystore.all({ kty: 'RSA' }); + +// filter by 'use' +everything = keystore.all({ use: 'enc' }); + +// filter by 'alg' +everything = keystore.all({ alg: 'RSA-OAEP' }); + +// filter by 'kid' + 'kty' + 'alg' +everything = keystore.all({ kid: kid, kty: 'RSA', alg: 'RSA-OAEP' }); +``` + +### Managing Keys ### + +To import an existing Key (as a JSON object or Key instance): +``` +// input is either a: +// * jose.JWK.Key to copy from; or +// * JSON object representing a JWK; or +// * String serialization of a JWK +keystore.add(input). + then(function(result) { + // {result} is a jose.JWK.Key + key = result; + }); +``` + +To generate a new Key: +``` +// first argument is the key type (kty) +// second is the key size (in bits) or named curve ('crv') for "EC" +keystore.generate("oct", 256). + then(function(result) { + // {result} is a jose.JWK.Key + key = result; + }); + +// ... with properties +var props = { + kid: 'gBdaS-G8RLax2qgObTD94w', + alg: 'A256GCM', + use: 'enc' +}; +keystore.generate("oct", 256, props). + then(function(result) { + // {result} is a jose.JWK.Key + key = result; + }); +``` + +To remove a Key from its Keystore: +``` +kestyore.remove(key); +// NOTE: key.keystore does not change!! +``` + +### Importing and Exporting a Single Key ### + +To import a single Key (as a JSON Object, or String serialized JSON Object): +``` +jose.JWK.asKey(input). + then(function(result) { + // {result} is a jose.JWK.Key + // {result.keystore} is a unique jose.JWK.KeyStore + }); +``` + +To export the public portion of a Key as a JWK: +``` +var output = key.toJSON(); +``` + +To export the public **and** private portions of a Key: +``` +var output = key.toJSON(true); +``` + +## Signatures ## + +### Signing Content ### + +At its simplest, to create a JWS: +``` +// {input} is a Buffer +jose.JWS.createSign(key). + update(input). + final(). + then(function(result) { + // {result} is a JSON object -- JWS using the JSON General Serialization + }); +``` + +The JWS is signed using the preferred algorithm appropriate for the given Key. The preferred algorithm is the first item returned by `key.algorithms("sign")`. + +To create a JWS using another serialization format: +``` +jose.JWS.createSign({ format: 'flattened' }, key). + update(input). + final(). + then(function(result) { + // {result} is a JSON object -- JWS using the JSON Flattened Serialization + }); + +jose.JWS.createSign({ format: 'compact' }, key). + update(input). + final(). + then(function(result) { + // {result} is a String -- JWS using the Compact Serialization + }); +``` + +To create a JWS using a specific algorithm: +``` +jose.JWS.createSign({ alg: 'PS256' }, key). + update(input). + final(). + then(function(result) { + // .... + }); +``` + +To create a JWS for a specified content type: +``` +jose.JWS.createSign({ fields: { cty: 'jwk+json' } }, key). + update(input). + final(). + then(function(result) { + // .... + }); +``` + +To create a JWS from String content: +``` +jose.JWS.createSign(key). + update(input, "utf8"). + final(). + then(function(result) { + // .... + }); +``` + +To create a JWS with multiple signatures: +``` +// {keys} is an Array of jose.JWK.Key instances +jose.JWS.createSign(keys). + update(input). + final(). + then(function(result) { + // .... + }); +``` + +### Verifying a JWS ### + +To verify a JWS, and retrieve the payload: +``` +jose.JWS.createVerify(keystore). + verify(input). + then(function(result) { + // {result} is a Object with: + // * header: the combined 'protected' and 'unprotected' header members + // * payload: Buffer of the signed content + // * signature: Buffer of the verified signature + }); +``` + +To verify using an implied Key: +``` +// {key} can be: +// * jose.JWK.Key +// * JSON object representing a JWK +jose.JWS.createVerify(key). + verify(input). + then(function(result) { + // ... + }); +``` + +## Encryption ## + +### Encrypting Content ### + +At its simplest, to create a JWE: +``` +// {input} is a Buffer +jose.JWE.createEncrypt(key). + update(input). + final(). + then(function(result) { + // {result} is a JSON Object -- JWE using the JSON General Serialization + }); +``` + +How the JWE content is encrypted depends on the provided Key. + +* If the Key only supports content encryption algorithms, then the preferred algorithm is used to encrypt the content and the key encryption algorithm (i.e., the "alg" member) is set to "dir". The preferred algorithm is the first item returned by `key.algorithms("encrypt")`. +* If the Key supports key management algorithms, then the JWE content is encrypted using "A128CBC-HS256" by default, and the Content Encryption Key is encrypted using the preferred algorithms for the given Key. The preferred algorithm is the first item returned by `key.algorithms("wrap")`. + + +To create a JWE using a different serialization format: +``` +jose.JWE.createEncrypt({ format: 'compact' }, key). + update(input). + final(). + then(function(result) { + // {result} is a String -- JWE using the Compact Serialization + }); + +jose.JWE.createEncrypt({ format: 'flattened' }, key). + update(input). + final(). + then(function(result) { + // {result} is a JSON Object -- JWE using the JSON Flattened Serialization + }); +``` + +To create a JWE and compressing the content before encrypting: +``` +jose.JWE.createEncrypt({ zip: true }, key). + update(input). + final(). + then(function(result) { + // .... + }); +``` + +To create a JWE for a specific content type: +``` +jose.JWE.createEncrypt({ fields: { cty : 'jwk+json' } }, key). + update(input). + final(). + then(function(result) { + // .... + }); +``` + +To create a JWE with multiple recipients: +``` +// {keys} is an Array of jose.JWK.Key instances +jose.JWE.createEncrypt(keys). + update(input). + final(). + then(function(result) { + // .... + }); +``` + +### Decrypting a JWE ### + +To decrypt a JWE, and retrieve the plaintext: +``` +jose.JWE.createDecrypt(keystore). + verify(input). + then(function(result) { + // {result} is a Object with: + // * header: the combined 'protected' and 'unprotected' header members + // * key: Key used to decrypt + // * plaintext: Buffer of the decrypted content + }); +``` + +To decrypt a JWE using an implied key: +``` +jose.JWE.createDecrypt(key). + verify(input). + then(function(result) { + // .... + }); +``` + +## Useful Utilities ## + +### Converting to Buffer ### + +To convert a [Typed Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays), [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), or Array of Numbers to a Buffer: +``` +buff = jose.util.asBuffer(input); +``` + +### URI-Safe Base64 ### + +To convert from a Buffer to a base64uri-encoded String: +``` +var output = jose.util.base64url.encode(input); +``` + +To convert a String to a base64uri-encoded String: +``` +// explicit encoding +output = jose.util.base64url.encode(input, "utf8"); + +// implied "binary" encoding +output = jose.util.base64url.encode(input); +``` + +To convert a base64uri-encoded String to a Buffer: +``` +var output = jose.util.base64url.decode(input); +``` + +To convert a base64uri-encoded String to a String: +``` +output = jose.util.base64url.decode(input, "utf8"); +``` + +### Random Bytes ### + +To generate a Buffer of octets, regardless of platform: + +``` +// argument is size (in bytes) +var rnd = jose.util.randomBytes(32); +``` + +This function uses: + +* `crypto.randomBytes()` on node.js +* `crypto.getRandomValues()` on modern browsers +* A PRNG based on AES and SHA-1 for older platforms diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..45eef8a --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,269 @@ +/** + * gulpfile.js - Gulp-based build + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var ARGV = require("yargs"). + usage("$0 [options] task [task ...]"). + option("browsers", { + type: "string", + describe: "browsers to run tests in", + default: "" + }). + option("sauce", { + type: "boolean", + describe: "use SauceLabs for tests/reporting", + default: false + }). + help("help"). + argv; + +var browserify = require("browserify"), + clone = require("lodash.clone"), + gulp = require("gulp"), + karma = require("karma"), + merge = require("lodash.merge"), + mocha = require("gulp-mocha"), + istanbul = require("gulp-istanbul"), + del = require("del"), + runSequence = require("run-sequence"); + +// ### 'CONSTANTS' ### +var SOURCES = ["./lib/**/*.js", "!(./lib/old/**/*.js)"], + TESTS = "./test/**/*-test.js"; + +// ### HELPERS ### +var MOCHA_CONFIG = { + timeout: 600000 +}; + +// ### LINT TASKS ### +function doEslint() { + var eslint = require("gulp-eslint"); + + return gulp.src([ + "lib/**/*.js", + "test/**/*.js", + "gulpfile.js" + ]) + .pipe(eslint()) + .pipe(eslint.format()); +} + +gulp.task("eslint", function() { + return doEslint(); +}); + +gulp.task("test:lint", function() { + var eslint = require("gulp-eslint"); + return doEslint() + .pipe(eslint.failOnError()); +}); + +// ### CLEAN TASKS ### +gulp.task("clean:coverage:nodejs", function() { + del("coverage/nodejs"); +}); +gulp.task("clean:coverage:browser", function() { + del("coverage/browser"); +}); +gulp.task("clean:coverage", function() { + del("coverage"); +}); + +gulp.task("clean:dist", function() { + del("dist"); +}); + +// ### NODEJS TASKS ### +function doTestsNodejs() { + return gulp.src(TESTS). + pipe(mocha(MOCHA_CONFIG)); +} + +gulp.task("test:nodejs:single", function() { + return doTestsNodejs(); +}); + +gulp.task("cover:nodejs", function() { + return gulp.src(SOURCES). + pipe(istanbul()). + pipe(istanbul.hookRequire()). + on("finish", function() { + doTestsNodejs(). + pipe(istanbul.writeReports({ + dir: "./coverage/nodejs", + reporters: ["html", "text-summary"] + })); + }); +}); + +gulp.task("test:nodejs", function(cb) { + runSequence("test:lint", + "test:nodejs:single", + cb); +}); + +// ### BROWSER TASKS ### +function doBrowserify(suffix, steps) { + var source = require("vinyl-source-stream"), + buffer = require("vinyl-buffer"), + sourcemaps = require("gulp-sourcemaps"); + + var pkg = require("./package.json"); + + suffix = suffix || ".js"; + steps = steps || []; + + var stream = browserify({ + entries: require("path").resolve(pkg.main), + standalone: "jose" + }).bundle(). + pipe(source(pkg.name + suffix)). + pipe(buffer()); + + steps.forEach(function(s) { + stream = stream.pipe(s); + }); + + return stream.pipe(sourcemaps.init({ loadMaps: true })). + pipe(sourcemaps.write("./")). + pipe(gulp.dest("./dist")); +} + +gulp.task("bundle", function() { + return doBrowserify(); +}); + +gulp.task("minify", function() { + var uglify = require("gulp-uglify"); + + return doBrowserify(".min.js", [ + uglify() + ]); +}); + +var KARMA_CONFIG = { + frameworks: ["mocha", "browserify"], + basePath: ".", + browserDisconnectTolerance: 1, + browserDisconnectTimeout: 600000, + browserNoActivityTimeout: 600000, + client: { + mocha: MOCHA_CONFIG + }, + preprocessors: { + "test/**/*-test.js": ["browserify"] + }, + reporters: ["mocha"], + browserify: { + debug: true + }, + customLaunchers: { + "SL_Chrome": { + base: "SauceLabs", + browserName: "chrome" + }, + "SL_Firefox": { + base: "SauceLabs", + browserName: "firefox" + }, + "SL_Safari": { + base: "SauceLabs", + platform: "OS X 10.9", + browserName: "safari", + version: "7" + }, + "SL_IE": { + base: "SauceLabs", + browserName: "internet explorer", + version: "10" + } + }, + captureTimeout: 600000, + sauceLabs: { + testName: "node-jose", + commandTimeout: 300 + }, + files: [TESTS] +}; +var KARMA_BROWSERS = { + local: ["Chrome", "Firefox"], + saucelabs: ["SL_Chrome", "SL_Firefox", "SL_IE", "SL_Safari"] +}; +// allow for IE on windows +if (/^win/.test(process.platform)) { + KARMA_BROWSERS.local.push("IE"); +} +// allow for Safari on Mac OS X +if (/^darwin/.test(process.platform)) { + KARMA_BROWSERS.local.push("Safari"); +} + +gulp.task("test:browser:single", function(done) { + var browsers = ARGV.browsers.split(/\s*,\s*/g). + filter(function (v) { return v; }); + + var config = merge({}, KARMA_CONFIG, { + singleRun: true + }); + if (ARGV.sauce) { + config = merge(config, { + reporters: ["mocha", "saucelabs"], + browsers: KARMA_BROWSERS.saucelabs + }); + } else { + config.browsers = KARMA_BROWSERS.local; + } + if (browsers.length) { + config.browsers = config.browsers.filter(function(b) { + b = b.replace("SL_", ""); + return -1 !== browsers.indexOf(b); + }); + } + + karma.server.start(config, done); +}); + +gulp.task("test:browser:watch", function(done) { + var config = clone(KARMA_CONFIG); + + karma.server.start(config, done); +}); + +gulp.task("test:browser", function(cb) { + runSequence("test:lint", + "test:browser:single", + cb); +}); + +// ### MAIN TASKS ### +gulp.task("test", function(cb) { + runSequence("test:lint", + "test:browser:single", + "test:nodejs:single", + cb); +}); +gulp.task("coverage", function(cb) { + runSequence("test:lint", + "cover:nodejs", + cb); +}); +gulp.task("clean", ["clean:coverage", "clean:dist"]); +gulp.task("dist", function(cb) { + runSequence("clean:dist", + "test:lint", + "test:browser", + ["bundle", "minify"], + cb); +}); + +// ### MAIN WATCHERS ### +gulp.task("watch:test", ["test"], function() { + return gulp.watch([SOURCES, TESTS], ["test:nodejs", "test:browser"]); +}); + +// ### DEFAULT ### +gulp.task("default", ["test"]); diff --git a/lib/algorithms/aes-cbc-hmac-sha2.js b/lib/algorithms/aes-cbc-hmac-sha2.js new file mode 100644 index 0000000..6db495a --- /dev/null +++ b/lib/algorithms/aes-cbc-hmac-sha2.js @@ -0,0 +1,360 @@ +/*! + * algorithms/aes-cbc-hmac-sha2.js - AES-CBC-HMAC-SHA2 Composited Encryption + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var helpers = require("./helpers.js"), + HMAC = require("./hmac.js"), + forge = require("../deps/forge.js"), + DataBuffer = require("../util/databuffer.js"); + +function cbcHmacEncryptFN(size) { + function commonChecks(key, iv) { + if ((size << 1) !== (key.length << 3)) { + throw new Error("invalid key size"); + } + if (16 !== iv.length) { + throw new Error("invalid iv"); + } + } + + function doHmacTag(key, iv, cdata, adata) { + var promise; + // construct MAC input + var mdata = Buffer.concat([ + adata, + iv, + cdata, + helpers.int64ToBuffer(adata.length * 8) + ]); + promise = HMAC["HS" + (size * 2)].sign(key, mdata, { + loose: true + }); + promise = promise.then(function(result) { + // TODO: move slice to hmac.js + var tag = result.mac.slice(0, size / 8); + return { + data: cdata, + tag: tag + }; + }); + return promise; + } + + // ### 'fallback' implementation -- uses forge + var fallback = function(key, pdata, props) { + props = props || {}; + + var iv = props.iv || new Buffer(0), + adata = props.aad || props.adata || new Buffer(0); + + try { + commonChecks(key, iv, adata); + } catch (err) { + return Promise.reject(err); + } + + var promise = Promise.resolve(); + + // STEP 1 -- Encrypt + promise = promise.then(function() { + var encKey = key.slice(size / 8); + + var cipher = forge.cipher.createCipher("AES-CBC", new DataBuffer(encKey)); + cipher.start({ + iv: new DataBuffer(iv) + }); + + // TODO: chunk data + cipher.update(new DataBuffer(pdata)); + if (!cipher.finish()) { + return Promise.reject(new Error("encryption failed")); + } + + var cdata = cipher.output.native(); + return cdata; + }); + + // STEP 2 -- MAC + promise = promise.then(function(cdata) { + var macKey = key.slice(0, size / 8); + return doHmacTag(macKey, iv, cdata, adata); + }); + + return promise; + }; + + // ### WebCryptoAPI implementation + // TODO: cache CryptoKey sooner + var webcrypto = function(key, pdata, props) { + props = props || {}; + + var iv = props.iv || new Buffer(0), + adata = props.aad || props.adata || new Buffer(0); + + try { + commonChecks(key, iv, adata); + } catch (err) { + return Promise.reject(err); + } + + var promise = Promise.resolve(); + + // STEP 1 -- Encrypt + promise = promise.then(function() { + var alg = { + name: "AES-CBC" + }; + var encKey = key.slice(size / 8); + return helpers.subtleCrypto.importKey("raw", encKey, alg, true, ["encrypt"]); + }); + promise = promise.then(function(key) { + var alg = { + name: "AES-CBC", + iv: iv + }; + return helpers.subtleCrypto.encrypt(alg, key, pdata); + }); + promise = promise.then(function(cdata) { + // wrap in *augmented* Uint8Array -- Buffer without copies + cdata = new Uint8Array(cdata); + cdata = Buffer._augment(cdata); + return cdata; + }); + + // STEP 2 -- MAC + promise = promise.then(function(cdata) { + var macKey = key.slice(0, size / 8); + return doHmacTag(macKey, iv, cdata, adata); + }); + return promise; + }; + + // ### NodeJS implementation + var nodejs = function(key, pdata, props) { + props = props || {}; + + var iv = props.iv || new Buffer(0), + adata = props.aad || props.adata || new Buffer(0); + + try { + commonChecks(key, iv, adata); + } catch (err) { + return Promise.reject(err); + } + + var promise = Promise.resolve(pdata); + + // STEP 1 -- Encrypt + promise = promise.then(function(pdata) { + var encKey = key.slice(size / 8), + name = "AES-" + size + "-CBC"; + var cipher = helpers.nodeCrypto.createCipheriv(name, encKey, iv); + var cdata = Buffer.concat([ + cipher.update(pdata), + cipher.final() + ]); + return cdata; + }); + + // STEP 2 -- MAC + promise = promise.then(function(cdata) { + var macKey = key.slice(0, size / 8); + return doHmacTag(macKey, iv, cdata, adata); + }); + + return promise; + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} + +function cbcHmacDecryptFN(size) { + function commonChecks(key, iv, tag) { + if ((size << 1) !== (key.length << 3)) { + throw new Error("invalid key size"); + } + if (16 !== iv.length) { + throw new Error("invalid iv"); + } + if ((size >>> 3) !== tag.length) { + throw new Error("invalid tag length"); + } + } + + function doHmacTag(key, iv, cdata, adata, tag) { + var promise; + // construct MAC input + var mdata = Buffer.concat([ + adata, + iv, + cdata, + helpers.int64ToBuffer(adata.length * 8) + ]); + promise = HMAC["HS" + (size * 2)].verify(key, mdata, tag, { + loose: true + }); + promise = promise.then(function() { + // success -- return ciphertext + return cdata; + }, function() { + // failure -- invalid tag error + throw new Error("mac check failed"); + }); + return promise; + } + + // ### 'fallback' implementation -- uses forge + var fallback = function(key, cdata, props) { + props = props || {}; + + var iv = props.iv || new Buffer(0), + adata = props.aad || props.adata || new Buffer(0), + tag = props.tag || props.mac || new Buffer(0); + + // validate inputs + try { + commonChecks(key, iv, tag); + } catch (err) { + return Promise.reject(err); + } + + var promise = Promise.resolve(); + + // STEP 1 -- MAC + promise = promise.then(function() { + var macKey = key.slice(0, size / 8); + return doHmacTag(macKey, iv, cdata, adata, tag); + }); + + // STEP 2 -- Decrypt + promise = promise.then(function() { + var encKey = key.slice(size / 8); + + var cipher = forge.cipher.createDecipher("AES-CBC", new DataBuffer(encKey)); + cipher.start({ + iv: new DataBuffer(iv) + }); + + // TODO: chunk data + cipher.update(new DataBuffer(cdata)); + if (!cipher.finish()) { + return Promise.reject(new Error("encryption failed")); + } + + var pdata = cipher.output.native(); + return pdata; + }); + + return promise; + }; + + // ### WebCryptoAPI implementation + // TODO: cache CryptoKey sooner + var webcrypto = function(key, cdata, props) { + props = props || {}; + + var iv = props.iv || new Buffer(0), + adata = props.aad || props.adata || new Buffer(0), + tag = props.tag || props.mac || new Buffer(0); + + // validate inputs + try { + commonChecks(key, iv, tag); + } catch (err) { + return Promise.reject(err); + } + + var promise = Promise.resolve(); + + // STEP 1 -- MAC + promise = promise.then(function() { + var macKey = key.slice(0, size / 8); + return doHmacTag(macKey, iv, cdata, adata, tag); + }); + + // STEP 2 -- Decrypt + promise = promise.then(function() { + var alg = { + name: "AES-CBC" + }; + var encKey = key.slice(size / 8); + return helpers.subtleCrypto.importKey("raw", encKey, alg, true, ["decrypt"]); + }); + promise = promise.then(function(key) { + var alg = { + name: "AES-CBC", + iv: iv + }; + return helpers.subtleCrypto.decrypt(alg, key, cdata); + }); + promise = promise.then(function(pdata) { + // wrap in *augmented* Uint8Array -- Buffer without copies + pdata = new Uint8Array(pdata); + pdata = Buffer._augment(pdata); + return pdata; + }); + + return promise; + }; + + // ### NodeJS implementation + var nodejs = function(key, cdata, props) { + props = props || {}; + + var iv = props.iv || new Buffer(0), + adata = props.aad || props.adata || new Buffer(0), + tag = props.tag || props.mac || new Buffer(0); + + // validate inputs + try { + commonChecks(key, iv, tag); + } catch (err) { + return Promise.reject(err); + } + + var promise = Promise.resolve(); + + // STEP 1 -- MAC + promise = promise.then(function() { + var macKey = key.slice(0, size / 8); + return doHmacTag(macKey, iv, cdata, adata, tag); + }); + + // SETP 2 -- Decrypt + promise = promise.then(function(cdata) { + var encKey = key.slice(size / 8), + name = "AES-" + size + "-CBC"; + var cipher = helpers.nodeCrypto.createDecipheriv(name, encKey, iv); + var pdata = Buffer.concat([ + cipher.update(cdata), + cipher.final() + ]); + return pdata; + }); + + return promise; + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} + +// ### Public API +// * [name].encrypt +// * [name].decrypt +var aesCbcHmacSha2 = {}; +[ + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" +].forEach(function(alg) { + var size = parseInt(/A(\d+)CBC-HS(\d+)?/g.exec(alg)[1]); + aesCbcHmacSha2[alg] = { + encrypt: cbcHmacEncryptFN(size), + decrypt: cbcHmacDecryptFN(size) + }; +}); + +module.exports = aesCbcHmacSha2; diff --git a/lib/algorithms/aes-gcm.js b/lib/algorithms/aes-gcm.js new file mode 100644 index 0000000..852bace --- /dev/null +++ b/lib/algorithms/aes-gcm.js @@ -0,0 +1,347 @@ +/*! + * algorithms/aes-gcm.js - AES-GCM Encryption and Key-Wrapping + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var helpers = require("./helpers.js"), + CONSTANTS = require("./constants.js"), + GCM = require("../deps/ciphermodes/gcm"); + +function gcmEncryptFN(size) { + function commonChecks(key, iv) { + if (size !== (key.length << 3)) { + throw new Error("invalid key size"); + } + if (12 !== iv.length) { + throw new Error("invalid iv"); + } + } + + // ### 'fallback' implementation -- uses forge + var fallback = function(key, pdata, props) { + var iv = props.iv || new Buffer(0), + adata = props.aad || props.adata || new Buffer(0), + cipher, + cdata; + + // validate inputs + try { + commonChecks(key, iv, adata); + } catch (err) { + return Promise.reject(err); + } + + // setup cipher + cipher = GCM.createCipher({ + key: key, + iv: iv, + additionalData: adata + }); + // ciphertext is the same length as plaintext + cdata = new Buffer(pdata.length); + + var promise = new Promise(function(resolve, reject) { + var amt = CONSTANTS.CHUNK_SIZE, + clen = 0, + poff = 0; + + (function doChunk() { + var plen = Math.min(amt, pdata.length - poff); + clen += cipher.update(pdata, + poff, + plen, + cdata, + clen); + poff += plen; + if (pdata.length > poff) { + setTimeout(doChunk, 0); + return; + } + + // finish it + clen += cipher.finish(cdata, clen); + if (clen !== pdata.length) { + reject(new Error("encryption failed")); + return; + } + + // resolve with output + var tag = cipher.tag; + resolve({ + data: cdata, + tag: tag + }); + })(); + }); + + return promise; + }; + + // ### WebCryptoAPI implementation + // TODO: cache CryptoKey sooner + var webcrypto = function(key, pdata, props) { + var iv = props.iv || new Buffer(0), + adata = props.aad || props.adata || new Buffer(0); + + try { + commonChecks(key, iv, adata); + } catch (err) { + return Promise.reject(err); + } + + var alg = { + name: "AES-GCM" + }; + var promise; + promise = helpers.subtleCrypto.importKey("raw", key, alg, true, ["encrypt"]); + promise = promise.then(function(key) { + alg.iv = iv; + alg.tagLength = 128; + if (adata.length) { + alg.additionalData = adata; + } + + return helpers.subtleCrypto.encrypt(alg, key, pdata); + }); + promise = promise.then(function(result) { + var tagStart = result.byteLength - 16; + + // wrap in *augmented* Uint8Array -- Buffer without copies + var tag = result.slice(tagStart); + tag = new Uint8Array(tag); + tag = Buffer._augment(tag); + + // wrap in *augmented* Uint8Array -- Buffer without copies + var cdata = result.slice(0, tagStart); + cdata = new Uint8Array(cdata); + cdata = Buffer._augment(cdata); + + return { + data: cdata, + tag: tag + }; + }); + + return promise; + }; + + // ### NodeJS implementation + var nodejs = function(key, pdata, props) { + var iv = props.iv || new Buffer(0), + adata = props.aad || props.adata || new Buffer(0); + + try { + commonChecks(key, iv, adata); + } catch (err) { + return Promise.reject(err); + } + + var alg = "aes-" + (key.length * 8) + "-gcm"; + var cipher; + try { + cipher = helpers.nodeCrypto.createCipheriv(alg, key, iv); + } catch (err) { + throw new Error("unsupported algorithm: " + alg); + } + if ("function" !== typeof cipher.setAAD) { + throw new Error("unsupported algorithm: " + alg); + } + if (adata.length) { + cipher.setAAD(adata); + } + + var cdata = Buffer.concat([ + cipher.update(pdata), + cipher.final() + ]); + var tag = cipher.getAuthTag(); + + return { + data: cdata, + tag: tag + }; + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} +function gcmDecryptFN(size) { + function commonChecks(key, iv, tag) { + if (size !== (key.length << 3)) { + throw new Error("invalid key size"); + } + if (12 !== iv.length) { + throw new Error("invalid iv"); + } + if (16 !== tag.length) { + throw new Error("invalid tag length"); + } + } + + // ### fallback implementation -- uses forge + var fallback = function(key, cdata, props) { + var adata = props.aad || props.adata || new Buffer(0), + iv = props.iv || new Buffer(0), + tag = props.tag || props.mac || new Buffer(0), + cipher, + pdata; + + // validate inputs + try { + commonChecks(key, iv, tag); + } catch (err) { + return Promise.reject(err); + } + + // setup cipher + cipher = GCM.createDecipher({ + key: key, + iv: iv, + additionalData: adata, + tag: tag + }); + // plaintext is the same length as ciphertext + pdata = new Buffer(cdata.length); + + var promise = new Promise(function(resolve, reject) { + var amt = CONSTANTS.CHUNK_SIZE, + plen = 0, + coff = 0; + + (function doChunk() { + var clen = Math.min(amt, cdata.length - coff); + plen += cipher.update(cdata, + coff, + clen, + pdata, + plen); + coff += clen; + if (cdata.length > coff) { + setTimeout(doChunk, 0); + return; + } + + try { + plen += cipher.finish(pdata, plen); + } catch (err) { + reject(new Error("decryption failed")); + return; + } + + if (plen !== cdata.length) { + reject(new Error("decryption failed")); + return; + } + + // resolve with output + resolve(pdata); + })(); + }); + + return promise; + }; + + // ### WebCryptoAPI implementation + // TODO: cache CryptoKey sooner + var webcrypto = function(key, cdata, props) { + var adata = props.aad || props.adata || new Buffer(0), + iv = props.iv || new Buffer(0), + tag = props.tag || props.mac || new Buffer(0); + + // validate inputs + try { + commonChecks(key, iv, tag); + } catch (err) { + return Promise.reject(err); + } + + var alg = { + name: "AES-GCM" + }; + var promise; + promise = helpers.subtleCrypto.importKey("raw", key, alg, true, ["decrypt"]); + promise = promise.then(function(key) { + alg.iv = iv; + alg.tagLength = 128; + if (adata.length) { + alg.additionalData = adata; + } + + // concatenate cdata and tag + cdata = Buffer.concat([cdata, tag], cdata.length + tag.length); + + return helpers.subtleCrypto.decrypt(alg, key, cdata); + }); + promise = promise.then(function(pdata) { + // wrap *augmented* Uint8Array -- Buffer without copies + pdata = new Uint8Array(pdata); + pdata = Buffer._augment(pdata); + return pdata; + }); + + return promise; + }; + + var nodejs = function(key, cdata, props) { + var adata = props.aad || props.adata || new Buffer(0), + iv = props.iv || new Buffer(0), + tag = props.tag || props.mac || new Buffer(0); + + // validate inputs + try { + commonChecks(key, iv, tag); + } catch (err) { + return Promise.reject(err); + } + + var alg = "aes-" + (key.length * 8) + "-gcm"; + var cipher; + try { + cipher = helpers.nodeCrypto.createDecipheriv(alg, key, iv); + } catch(err) { + throw new Error("unsupported algorithm: " + alg); + } + if ("function" !== typeof cipher.setAAD) { + throw new Error("unsupported algorithm: " + alg); + } + cipher.setAuthTag(tag); + if (adata.length) { + cipher.setAAD(adata); + } + + try { + var pdata = Buffer.concat([ + cipher.update(cdata), + cipher.final() + ]); + + return pdata; + } catch (err) { + throw new Error("decryption failed"); + } + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} + +// ### Public API +// * [name].encrypt +// * [name].decrypt +var aesGcm = {}; +[ + "A128GCM", + "A192GCM", + "A256GCM", + "A128GCMKW", + "A192GCMKW", + "A256GCMKW" +].forEach(function(alg) { + var size = parseInt(/A(\d+)GCM(?:KW)?/g.exec(alg)[1]); + aesGcm[alg] = { + encrypt: gcmEncryptFN(size), + decrypt: gcmDecryptFN(size) + }; +}); + +module.exports = aesGcm; diff --git a/lib/algorithms/aes-kw.js b/lib/algorithms/aes-kw.js new file mode 100644 index 0000000..d251155 --- /dev/null +++ b/lib/algorithms/aes-kw.js @@ -0,0 +1,226 @@ +/*! + * algorithms/aes-kw.js - AES-KW Key-Wrapping + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var helpers = require("./helpers.js"), + forge = require("../deps/forge.js"), + DataBuffer = require("../util/databuffer.js"); + +var A0 = new Buffer("a6a6a6a6a6a6a6a6", "hex"); + +// ### helpers +function xor(a, b) { + var len = Math.max(a.length, b.length); + var result = new Buffer(len); + for (var idx = 0; len > idx; idx++) { + result[idx] = (a[idx] || 0) ^ (b[idx] || 0); + } + return result; +} + +function split(input, size) { + var output = []; + for (var idx = 0; input.length > idx; idx += size) { + output.push(input.slice(idx, idx + size)); + } + return output; +} + +function longToBigEndian(input) { + var hi = Math.floor(input / 4294967296), + lo = input % 4294967296; + var output = new Buffer(8); + output[0] = 0xff & (hi >>> 24); + output[1] = 0xff & (hi >>> 16); + output[2] = 0xff & (hi >>> 8); + output[3] = 0xff & (hi >>> 0); + output[4] = 0xff & (lo >>> 24); + output[5] = 0xff & (lo >>> 16); + output[6] = 0xff & (lo >>> 8); + output[7] = 0xff & (lo >>> 0); + return output; +} + +function kwEncryptFN(size) { + function commonChecks(key, data) { + if (size !== (key.length << 3)) { + throw new Error("invalid key size"); + } + if (0 < data.length && 0 !== (data.length % 8)) { + throw new Error("invalid data length"); + } + } + + // ### 'fallback' implementation -- uses forge + var fallback = function(key, pdata) { + try { + commonChecks(key, pdata); + } catch (err) { + return Promise.reject(err); + } + + // setup cipher + var cipher = forge.cipher.createCipher("AES", new DataBuffer(key)); + + // split input into chunks + var R = split(pdata, 8); + var A, + B, + count; + A = A0; + for (var jdx = 0; 6 > jdx; jdx++) { + for (var idx = 0; R.length > idx; idx++) { + count = (R.length * jdx) + idx + 1; + B = Buffer.concat([A, R[idx]]); + cipher.start(); + cipher.update(new DataBuffer(B)); + cipher.finish(); + B = cipher.output.native(); + + A = xor(B.slice(0, 8), + longToBigEndian(count)); + R[idx] = B.slice(8, 16); + } + } + R = [A].concat(R); + var cdata = Buffer.concat(R); + return Promise.resolve({ + data: cdata + }); + }; + // ### WebCryptoAPI implementation + var webcrypto = function(key, pdata) { + try { + commonChecks(key, pdata); + } catch (err) { + return Promise.reject(err); + } + + var alg = { + name: "AES-KW" + }; + var promise = [ + helpers.subtleCrypto.importKey("raw", pdata, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]), + helpers.subtleCrypto.importKey("raw", key, alg, true, ["wrapKey"]) + ]; + promise = Promise.all(promise); + promise = promise.then(function(keys) { + return helpers.subtleCrypto.wrapKey("raw", + keys[0], // key + keys[1], // wrappingKey + alg); + }); + promise = promise.then(function(result) { + // wrap in *augmented* Uint8Array -- Buffer without copies + result = new Uint8Array(result); + result = Buffer._augment(result); + + return { + data: result + }; + }); + return promise; + }; + + return helpers.setupFallback(null, webcrypto, fallback); +} +function kwDecryptFN(size) { + function commonChecks(key, data) { + if (size !== (key.length << 3)) { + throw new Error("invalid key size"); + } + if (0 < (data.length - 8) && 0 !== (data.length % 8)) { + throw new Error("invalid data length"); + } + } + + // ### 'fallback' implementation -- uses forge + var fallback = function(key, cdata) { + try { + commonChecks(key, cdata); + } catch (err) { + return Promise.reject(err); + } + + // setup cipher + var cipher = forge.cipher.createDecipher("AES", new DataBuffer(key)); + + // prepare inputs + var R = split(cdata, 8), + A, + B, + count; + A = R[0]; + R = R.slice(1); + for (var jdx = 5; 0 <= jdx; --jdx) { + for (var idx = R.length - 1; 0 <= idx; --idx) { + count = (R.length * jdx) + idx + 1; + B = xor(A, + longToBigEndian(count)); + B = Buffer.concat([B, R[idx]]); + cipher.start(); + cipher.update(new DataBuffer(B)); + cipher.finish(); + B = cipher.output.native(); + + A = B.slice(0, 8); + R[idx] = B.slice(8, 16); + } + } + if (A.toString() !== A0.toString()) { + return Promise.reject(new Error("decryption failed")); + } + var pdata = Buffer.concat(R); + return Promise.resolve(pdata); + }; + // ### WebCryptoAPI implementation + var webcrypto = function(key, cdata) { + try { + commonChecks(key, cdata); + } catch (err) { + return Promise.reject(err); + } + + var alg = { + name: "AES-KW" + }; + var promise = helpers.subtleCrypto.importKey("raw", key, alg, true, ["unwrapKey"]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.unwrapKey("raw", cdata, key, alg, {name: "HMAC", hash: "SHA-256"}, true, ["sign"]); + }); + promise = promise.then(function(result) { + // unwrapped CryptoKey -- extract raw + return helpers.subtleCrypto.exportKey("raw", result); + }); + promise = promise.then(function(result) { + // wrap in *augmented* Uint8Array -- Buffer without copies + result = new Uint8Array(result); + result = Buffer._augment(result); + return result; + }); + return promise; + }; + + return helpers.setupFallback(null, webcrypto, fallback); +} + +// ### Public API +// * [name].encrypt +// * [name].decrypt +var aesKw = {}; +[ + "A128KW", + "A192KW", + "A256KW" +].forEach(function(alg) { + var size = parseInt(/A(\d+)KW/g.exec(alg)[1]); + aesKw[alg] = { + encrypt: kwEncryptFN(size), + decrypt: kwDecryptFN(size) + }; +}); + +module.exports = aesKw; diff --git a/lib/algorithms/concat.js b/lib/algorithms/concat.js new file mode 100644 index 0000000..4e6a605 --- /dev/null +++ b/lib/algorithms/concat.js @@ -0,0 +1,71 @@ +/*! + * algorithms/concat.js - Concat Key Derivation + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var CONSTANTS = require("./constants.js"), + sha = require("./sha.js"); + +function concatDeriveFn(name) { + name = name.replace("CONCAT-", ""); + + // NOTE: no nodejs/webcrypto/fallback model, since ConcatKDF is + // implemented using the SHA algorithms + + var fn = function(key, props) { + props = props || {}; + + var keyLen = props.length, + hashLen = CONSTANTS.HASHLENGTH[name]; + if (!keyLen) { + return Promise.reject("invalid key length"); + } + + // setup otherInfo + if (!props.otherInfo) { + return Promise.reject(new Error("invalid otherInfo")); + } + var otherInfo = props.otherInfo; + + var op = sha[name].digest; + var N = Math.ceil(keyLen / hashLen), + idx = 0, + okm = []; + function step() { + if (N === idx++) { + return Buffer.concat(okm).slice(0, keyLen); + } + + var T = new Buffer(4 + key.length + otherInfo.length); + T.writeUInt32BE(idx, 0); + key.copy(T, 4); + otherInfo.copy(T, 4 + key.length); + return op(T).then(function(result) { + okm.push(result); + return step(); + }); + } + + return step(); + }; + + return fn; +} + +// Public API +// * [name].derive +var concat = {}; +[ + "CONCAT-SHA-1", + "CONCAT-SHA-256", + "CONCAT-SHA-384", + "CONCAT-SHA-512" +].forEach(function(name) { + concat[name] = { + derive: concatDeriveFn(name) + }; +}); + +module.exports = concat; diff --git a/lib/algorithms/constants.js b/lib/algorithms/constants.js new file mode 100644 index 0000000..dc788bd --- /dev/null +++ b/lib/algorithms/constants.js @@ -0,0 +1,46 @@ +/*! + * algorithms/constants.js - Constants used in Cryptographic Algorithms + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ + "use strict"; + +module.exports = { + CHUNK_SIZE: 1024, + HASHLENGTH: { + "SHA-1": 160, + "SHA-256": 256, + "SHA-384": 384, + "SHA-512": 512 + }, + ENCLENGTH: { + "AES-128-CBC": 128, + "AES-192-CBC": 192, + "AES-256-CBC": 256, + "AES-128-KW": 128, + "AES-192-KW": 192, + "AES-256-KW": 256 + }, + KEYLENGTH: { + "A128CBC-HS256": 256, + "A192CBC-HS384": 384, + "A256CBC-HS512": 512, + "A128GCM": 128, + "A192GCM": 192, + "A256GCM": 256, + "A128KW": 128, + "A192KW": 192, + "A256KW": 256, + "ECDH-ES+A128KW": 128, + "ECDH-ES+A192KW": 192, + "ECDH-EC+A256KW": 256 + }, + NONCELENGTH: { + "A128CBC-HS256": 128, + "A192CBC-HS384": 128, + "A256CBC-HS512": 128, + "A128GCM": 96, + "A192GCM": 96, + "A256GCM": 96 + } +}; diff --git a/lib/algorithms/dir.js b/lib/algorithms/dir.js new file mode 100644 index 0000000..ad05928 --- /dev/null +++ b/lib/algorithms/dir.js @@ -0,0 +1,33 @@ +/*! + * algorithms/dir.js - Direct key mode + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +function dirEncryptFN(key) { + // NOTE: pdata unused + // NOTE: props unused + return Promise.resolve({ + data: key, + once: true, + direct: true + }); +} +function dirDecryptFN(key) { + // NOTE: pdata unused + // NOTE: props unused + return Promise.resolve(key); +} + +// ### Public API +// * [name].encrypt +// * [name].decrypt +var direct = { + dir: { + encrypt: dirEncryptFN, + decrypt: dirDecryptFN + } +}; + +module.exports = direct; diff --git a/lib/algorithms/ec-util.js b/lib/algorithms/ec-util.js new file mode 100644 index 0000000..ec0c83e --- /dev/null +++ b/lib/algorithms/ec-util.js @@ -0,0 +1,87 @@ +/*! + * algorithms/ec-util.js - Elliptic Curve Utility Functions + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var clone = require("lodash.clone"), + ecc = require("../deps/ecc"), + forge = require("../deps/forge.js"), + util = require("../util"); + +var EC_KEYSIZES = { + "P-256": 256, + "P-384": 384, + "P-521": 521 +}; + +function convertToForge(key, isPublic) { + var parts = isPublic ? + ["x", "y"] : + ["d"]; + parts = parts.map(function(f) { + return new forge.jsbn.BigInteger(key[f].toString("hex"), 16); + }); + // prefix with curve + parts = [key.crv].concat(parts); + var fn = isPublic ? + ecc.asPublicKey : + ecc.asPrivateKey; + return fn.apply(ecc, parts); +} + +function convertToJWK(key, isPublic) { + var result = clone(key); + var parts = isPublic ? + ["x", "y"] : + ["x", "y", "d"]; + parts.forEach(function(f) { + result[f] = util.base64url.encode(result[f]); + }); + + // remove potentially troublesome properties + delete result.key_ops; + delete result.use; + delete result.alg; + + if (isPublic) { + delete result.d; + } + + return result; +} + +function convertToObj(key, isPublic) { + var result = clone(key); + var parts = isPublic ? + ["x", "y"] : + ["d"]; + parts.forEach(function(f) { + // assume string if base64url-encoded + result[f] = util.asBuffer(result[f], "base64url"); + }); + + return result; +} + +var UNCOMPRESSED = new Buffer([0x04]); +function convertToBuffer(key, isPublic) { + key = convertToObj(key, isPublic); + var result = isPublic ? + Buffer.concat([UNCOMPRESSED, key.x, key.y]) : + key.d; + return result; +} + +function curveSize(crv) { + return EC_KEYSIZES[crv || ""] || NaN; +} + +module.exports = { + convertToForge: convertToForge, + convertToJWK: convertToJWK, + convertToObj: convertToObj, + convertToBuffer: convertToBuffer, + curveSize: curveSize +}; diff --git a/lib/algorithms/ecdh.js b/lib/algorithms/ecdh.js new file mode 100644 index 0000000..f4cd971 --- /dev/null +++ b/lib/algorithms/ecdh.js @@ -0,0 +1,441 @@ +/*! + * algorithms/ecdh.js - Elliptic Curve Diffie-Hellman algorithms + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var clone = require("lodash.clone"), + merge = require("../util/merge"), + omit = require("lodash.omit"), + pick = require("lodash.pick"), + util = require("../util"), + ecUtil = require("./ec-util.js"), + hkdf = require("./hkdf.js"), + concat = require("./concat.js"), + aesKw = require("./aes-kw.js"), + helpers = require("./helpers.js"), + CONSTANTS = require("./constants.js"); + +function idealHash(curve) { + switch (curve) { + case "P-256": + return "SHA-256"; + case "P-384": + return "SHA-384"; + case "P-521": + return "SHA-512"; + default: + throw new Error("unsupported curve: " + curve); + } +} + +// ### Exported +var ecdh = module.exports = {}; + +// ### Derivation algorithms +// ### "raw" ECDH +function ecdhDeriveFn() { + var alg = { + name: "ECDH" + }; + + // ### fallback implementation -- uses ecc + forge + var fallback = function(key, props) { + props = props || {}; + var keyLen = props.length || 0; + // assume {key} is privateKey + var privKey = ecUtil.convertToForge(key, false); + // assume {props.public} is publicKey + if (!props.public) { + return Promise.reject(new Error("invalid EC public key")); + } + var pubKey = ecUtil.convertToForge(props.public, true); + var secret = privKey.computeSecret(pubKey); + if (keyLen) { + // truncate to requested key length + if (secret.length < keyLen) { + return Promise.reject(new Error("key length too large: " + keyLen)); + } + secret = secret.slice(0, keyLen); + } + return Promise.resolve(secret); + }; + + // ### WebCryptoAPI implementation + // TODO: cache CryptoKey sooner + var webcrypto = function(key, props) { + key = key || {}; + props = props || {}; + + var keyLen = props.length || 0, + algParams = merge(clone(alg), { + namedCurve: key.crv + }); + + // assume {key} is privateKey + if (!keyLen) { + // calculate key length from private key size + keyLen = key.d.length; + } + var privKey = ecUtil.convertToJWK(key, false); + privKey = helpers.subtleCrypto.importKey("jwk", + privKey, + algParams, + false, + [ "deriveBits" ]); + + // assume {props.public} is publicKey + if (!props.public) { + return Promise.reject(new Error("invalid EC public key")); + } + var pubKey = ecUtil.convertToJWK(props.public, true); + pubKey = helpers.subtleCrypto.importKey("jwk", + pubKey, + algParams, + false, + []); + + var promise = Promise.all([privKey, pubKey]); + promise = promise.then(function(keypair) { + var privKey = keypair[0], + pubKey = keypair[1]; + + var algParams = merge(clone(alg), { + public: pubKey + }); + return helpers.subtleCrypto.deriveBits(algParams, privKey, keyLen * 8); + }); + promise = promise.then(function(result) { + result = new Uint8Array(result); + Buffer._augment(result); + return result; + }); + return promise; + }; + + var nodejs = function(key, props) { + if ("function" !== typeof helpers.nodeCrypto.createECDH) { + throw new Error("unsupported algorithm: ECDH"); + } + + props = props || {}; + var keyLen = props.length || 0; + var curve; + switch (key.crv) { + case "P-256": + curve = "prime256v1"; + break; + case "P-384": + curve = "secp384r1"; + break; + case "P-521": + curve = "secp521r1"; + break; + default: + return Promise.reject(new Error("invalid curve: " + curve)); + } + + // assume {key} is privateKey + var privKey = ecUtil.convertToBuffer(key, false); + + // assume {props.public} is publicKey + var pubKey = ecUtil.convertToBuffer(props.public, true); + + var ecdh = helpers.nodeCrypto.createECDH(curve); + // dummy call so computeSecret doesn't fail + ecdh.generateKeys(); + ecdh.setPrivateKey(privKey); + var secret = ecdh.computeSecret(pubKey); + if (keyLen) { + if (secret.length < keyLen) { + return Promise.reject(new Error("key length too large: " + keyLen)); + } + secret = secret.slice(0, keyLen); + } + return Promise.resolve(secret); + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} + +function ecdhConcatDeriveFn() { + // NOTE: no nodejs/webcrypto/fallback model, since this algorithm is + // implemented using other primitives + + var fn = function(key, props) { + props = props || {}; + + var hash; + try { + hash = props.hash || idealHash(key.crv); + if (!hash) { + throw new Error("invalid hash: " + hash); + } + hash.toUpperCase(); + } catch (ex) { + return Promise.reject(ex); + } + + var params = ["public"]; + // derive shared secret + // NOTE: whitelist items from {props} for ECDH + var promise = ecdh.ECDH.derive(key, pick(props, params)); + // expand + promise = promise.then(function(shared) { + // NOTE: blacklist items from {props} for ECDH + return concat["CONCAT-" + hash].derive(shared, omit(props, params)); + }); + return promise; + }; + + return fn; +} + +function ecdhHkdfDeriveFn() { + // NOTE: no nodejs/webcrypto/fallback model, since this algorithm is + // implemented using other primitives + + var fn = function(key, props) { + props = props || {}; + + var hash; + try { + hash = props.hash || idealHash(key.crv); + if (!hash) { + throw new Error("invalid hash: " + hash); + } + hash.toUpperCase(); + } catch (ex) { + return Promise.reject(ex); + } + + var params = ["public"]; + // derive shared secret + // NOTE: whitelist items from {props} for ECDH + var promise = ecdh.ECDH.derive(key, pick(props, params)); + // extract-and-expand + promise = promise.then(function(shared) { + // NOTE: blacklist items from {props} for ECDH + return hkdf["HKDF-" + hash].derive(shared, omit(props, params)); + }); + return promise; + }; + + return fn; +} + +// ### Wrap/Unwrap algorithms +function doEcdhesCommonDerive(privKey, pubKey, props) { + function prependLen(input) { + return Buffer.concat([ + helpers.int32ToBuffer(input.length), + input + ]); + } + + var algId = props.algorithm || "", + keyLen = CONSTANTS.KEYLENGTH[algId], + apu = util.asBuffer(props.apu || "", "base64url"), + apv = util.asBuffer(props.apv || "", "base64url"); + var otherInfo = Buffer.concat([ + prependLen(new Buffer(algId, "utf8")), + prependLen(apu), + prependLen(apv), + helpers.int32ToBuffer(keyLen) + ]); + + var params = { + public: pubKey, + length: keyLen / 8, + hash: "SHA-256", + otherInfo: otherInfo + }; + return ecdh["ECDH-CONCAT"].derive(privKey, params); +} + +function ecdhesDirEncryptFn() { + // NOTE: no nodejs/webcrypto/fallback model, since this algorithm is + // implemented using other primitives + var fn = function(key, pdata, props) { + props = props || {}; + + // {props.epk} is private + if (!props.epk || !props.epk.d) { + return Promise.reject(new Error("missing ephemeral private key")); + } + var epk = ecUtil.convertToObj(props.epk, false); + + // {key} is public + if (!key || !key.x || !key.y) { + return Promise.reject(new Error("missing static public key")); + } + var spk = ecUtil.convertToObj(key, true); + + // derive ECDH shared + var promise = doEcdhesCommonDerive(epk, spk, { + algorithm: props.enc, + apu: props.apu, + apv: props.apv + }); + promise = promise.then(function(shared) { + return { + data: shared, + once: true, + direct: true + }; + }); + return promise; + }; + + return fn; +} +function ecdhesDirDecryptFn() { + // NOTE: no nodejs/webcrypto/fallback model, since this algorithm is + // implemented using other primitives + var fn = function(key, cdata, props) { + props = props || {}; + + // {props.epk} is public + if (!props.epk || !props.epk.x || !props.epk.y) { + return Promise.reject(new Error("missing ephemeral public key")); + } + var epk = ecUtil.convertToObj(props.epk, true); + + // {key} is private + if (!key || !key.d) { + return Promise.reject(new Error("missing static private key")); + } + var spk = ecUtil.convertToObj(key, false); + + // derive ECDH shared + var promise = doEcdhesCommonDerive(spk, epk, { + algorithm: props.enc, + apu: props.apu, + apv: props.apv + }); + promise = promise.then(function(shared) { + return shared; + }); + return promise; + }; + + return fn; +} + +function ecdhesKwEncryptFn(wrap) { + // NOTE: no nodejs/webcrypto/fallback model, since this algorithm is + // implemented using other primitives + var fn = function(key, pdata, props) { + props = props || {}; + + // {props.epk} is private + if (!props.epk || !props.epk.d) { + return Promise.reject(new Error("missing ephemeral private key")); + } + var epk = ecUtil.convertToObj(props.epk, false); + + // {key} is public + if (!key || !key.x || !key.y) { + return Promise.reject(new Error("missing static public key")); + } + var spk = ecUtil.convertToObj(key, true); + + // derive ECDH shared + var promise = doEcdhesCommonDerive(epk, spk, { + algorithm: props.alg, + apu: props.apu, + apv: props.apv + }); + promise = promise.then(function(shared) { + // wrap provided key with ECDH shared + return wrap(shared, pdata); + }); + return promise; + }; + + return fn; +} + +function ecdhesKwDecryptFn(unwrap) { + // NOTE: no nodejs/webcrypto/fallback model, since this algorithm is + // implemented using other primitives + var fn = function(key, cdata, props) { + props = props || {}; + + // {props.epk} is public + if (!props.epk || !props.epk.x || !props.epk.y) { + return Promise.reject(new Error("missing ephemeral public key")); + } + var epk = ecUtil.convertToObj(props.epk, true); + + // {key} is private + if (!key || !key.d) { + return Promise.reject(new Error("missing static private key")); + } + var spk = ecUtil.convertToObj(key, false); + + // derive ECDH shared + var promise = doEcdhesCommonDerive(spk, epk, { + algorithm: props.alg, + apu: props.apu, + apv: props.apv + }); + promise = promise.then(function(shared) { + // unwrap provided key with ECDH shared + return unwrap(shared, cdata); + }); + return promise; + }; + + return fn; +} + +// ### Public API +// * [name].derive +[ + "ECDH", + "ECDH-HKDF", + "ECDH-CONCAT" +].forEach(function(name) { + var kdf = /^ECDH(?:-(\w+))?$/g.exec(name || "")[1]; + var op = ecdh[name] = ecdh[name] || {}; + switch (kdf || "") { + case "CONCAT": + op.derive = ecdhConcatDeriveFn(); + break; + case "HKDF": + op.derive = ecdhHkdfDeriveFn(); + break; + case "": + op.derive = ecdhDeriveFn(); + break; + default: + op.derive = null; + } +}); + +// * [name].encrypt +// * [name].decrypt +[ + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW" +].forEach(function(name) { + var kw = /^ECDH-ES(?:\+(.+))?/g.exec(name || "")[1]; + var op = ecdh[name] = ecdh[name] || {}; + if (!kw) { + op.encrypt = ecdhesDirEncryptFn(); + op.decrypt = ecdhesDirDecryptFn(); + } else { + kw = aesKw[kw]; + if (kw) { + op.encrypt = ecdhesKwEncryptFn(kw.encrypt); + op.decrypt = ecdhesKwDecryptFn(kw.decrypt); + } else { + op.ecrypt = op.decrypt = null; + } + } +}); +//*/ diff --git a/lib/algorithms/ecdsa.js b/lib/algorithms/ecdsa.js new file mode 100644 index 0000000..fa1b542 --- /dev/null +++ b/lib/algorithms/ecdsa.js @@ -0,0 +1,177 @@ +/*! + * algorithms/ecdsa.js - Elliptic Curve Digitial Signature Algorithms + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var ecUtil = require("./ec-util.js"), + helpers = require("./helpers.js"), + sha = require("./sha.js"); + +function idealCurve(hash) { + switch (hash) { + case "SHA-256": + return "P-256"; + case "SHA-384": + return "P-384"; + case "SHA-512": + return "P-521"; + default: + throw new Error("unsupported hash: " + hash); + } +} + +function ecdsaSignFN(hash) { + var curve = idealCurve(hash); + + // ### Fallback implementation -- uses forge + var fallback = function(key, pdata /*, props */) { + if (curve !== key.crv) { + return Promise.reject(new Error("invalid curve")); + } + var pk = ecUtil.convertToForge(key, false); + + var promise; + // generate hash + promise = sha[hash].digest(pdata); + // sign hash + promise = promise.then(function(result) { + result = pk.sign(result); + result = Buffer.concat([result.r, result.s]); + return { + data: pdata, + mac: result + }; + }); + return promise; + }; + + // ### WebCrypto API implementation + var webcrypto = function(key, pdata /*, props */) { + if (curve !== key.crv) { + return Promise.reject(new Error("invalid curve")); + } + var pk = ecUtil.convertToJWK(key, false); + + var promise; + var alg = { + name: "ECDSA", + namedCurve: pk.crv, + hash: { + name: hash + } + }; + promise = helpers.subtleCrypto.importKey("jwk", + pk, + alg, + true, + [ "sign" ]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.sign(alg, key, pdata); + }); + promise = promise.then(function(result) { + result = new Uint8Array(result); + result = Buffer._augment(result); + return { + data: pdata, + mac: result + }; + }); + return promise; + }; + + return helpers.setupFallback(null, webcrypto, fallback); +} + +function ecdsaVerifyFN(hash) { + var curve = idealCurve(hash); + + // ### Fallback implementation -- uses forge + var fallback = function(key, pdata, mac /*, props */) { + if (curve !== key.crv) { + return Promise.reject(new Error("invalid curve")); + } + var pk = ecUtil.convertToForge(key, true); + + var promise; + // generate hash + promise = sha[hash].digest(pdata); + // verify hash + promise = promise.then(function(result) { + var len = mac.length / 2; + var rs = { + r: mac.slice(0, len), + s: mac.slice(len) + }; + if (!pk.verify(result, rs)) { + return Promise.reject(new Error("verification failed")); + } + return { + data: pdata, + mac: mac, + valid: true + }; + }); + return promise; + }; + + // ### WebCrypto API implementation + var webcrypto = function(key, pdata, mac /* , props */) { + if (curve !== key.crv) { + return Promise.reject(new Error("invalid curve")); + } + var pk = ecUtil.convertToJWK(key, true); + + var promise; + var alg = { + name: "ECDSA", + namedCurve: pk.crv, + hash: { + name: hash + } + }; + promise = helpers.subtleCrypto.importKey("jwk", + pk, + alg, + true, + ["verify"]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.verify(alg, key, mac, pdata); + }); + promise = promise.then(function(result) { + if (!result) { + return Promise.reject(new Error("verification failed")); + } + return { + data: pdata, + mac: mac, + valid: true + }; + }); + return promise; + }; + + return helpers.setupFallback(null, webcrypto, fallback); +} + +// ### Public API +var ecdsa = {}; + +// * [name].sign +// * [name].verify +[ + "ES256", + "ES384", + "ES512" +].forEach(function(name) { + var hash = name.replace(/ES(\d+)/g, function(m, size) { + return "SHA-" + size; + }); + ecdsa[name] = { + sign: ecdsaSignFN(hash), + verify: ecdsaVerifyFN(hash) + }; +}); + +module.exports = ecdsa; diff --git a/lib/algorithms/helpers.js b/lib/algorithms/helpers.js new file mode 100644 index 0000000..ea5267f --- /dev/null +++ b/lib/algorithms/helpers.js @@ -0,0 +1,144 @@ +/*! + * algorithms/helpers.js - Internal functions and fields used in Cryptographic + * Algorithms + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +require("es6-promise").polyfill(); + +// ### +exports.int32ToBuffer = function(v, b) { + b = b || new Buffer(4); + b[0] = (v >>> 24) & 0xff; + b[1] = (v >>> 16) & 0xff; + b[2] = (v >>> 8) & 0xff; + b[3] = v & 0xff; + return b; +}; + +var MAX_INT32 = Math.pow(2, 32); +exports.int64ToBuffer = function(v, b) { + b = b || new Buffer(8); + var hi = Math.floor(v / MAX_INT32), + lo = v % MAX_INT32; + hi = exports.int32ToBuffer(hi); + lo = exports.int32ToBuffer(lo); + b = Buffer.concat([hi, lo]); + return b; +}; + +// ### crypto and DOMException in browsers ### +/* global crypto:false, DOMException:false */ + +function getCryptoSubtle() { + if ("undefined" !== typeof crypto) { + if ("undefined" !== typeof crypto.subtle) { + return crypto.subtle; + } + if ("undefined" !== typeof crypto.webkitSubtle) { + return crypto.webkitSubtle; + } + } + + return undefined; +} +function getCryptoNodeJS() { + var crypto; + try { + var pkgname = "crypto"; + crypto = require(pkgname); + } catch (err) { + return undefined; + } + + return crypto; +} + +var supported = {}; +Object.defineProperty(exports, "subtleCrypto", { + get: function() { + var result; + + if ("subtleCrypto" in supported) { + result = supported.subtleCrypto; + } else { + result = supported.subtleCrypto = getCryptoSubtle(); + } + + return result; + }, + enumerable: true +}); +Object.defineProperty(exports, "nodeCrypto", { + get: function() { + var result; + + if ("nodeCrypto" in supported) { + result = supported.nodeCrypto; + } else { + result = supported.nodeCrypto = getCryptoNodeJS(); + } + + return result; + }, + enumerable: true +}); + +exports.setupFallback = function(nodejs, webcrypto, fallback) { + var impl; + + if (nodejs && exports.nodeCrypto) { + impl = function main() { + var args = arguments, + promise; + + function check(err) { + if (0 === err.message.indexOf("unsupported algorithm:")) { + impl = fallback; + return impl.apply(null, args); + } + + return Promise.reject(err); + } + + try { + promise = Promise.resolve(nodejs.apply(null, args)); + } catch(err) { + promise = check(err); + } + + return promise; + }; + } else if (webcrypto && exports.subtleCrypto) { + impl = function main() { + var args = arguments, + promise; + + function check(err) { + if (err.code === DOMException.NOT_SUPPORTED_ERR || + err.message === "Only ArrayBuffer and ArrayBufferView objects can be passed as CryptoOperationData") { + // not actually supported -- always use fallback + impl = fallback; + return impl.apply(null, args); + } + + return Promise.reject(err); + } + + try { + promise = webcrypto.apply(null, args); + promise = promise.catch(check); + } catch(err) { + promise = check(err); + } + + return promise; + }; + } else { + impl = fallback; + } + + return impl; +}; diff --git a/lib/algorithms/hkdf.js b/lib/algorithms/hkdf.js new file mode 100644 index 0000000..5da56ae --- /dev/null +++ b/lib/algorithms/hkdf.js @@ -0,0 +1,87 @@ +/*! + * algorithms/hkdf.js - HMAC-based Extract-and-Expand Key Derivation + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var CONSTANTS = require("./constants.js"), + hmac = require("./hmac.js"); + +function hkdfDeriveFn(name) { + var hash = name.replace("HKDF-", ""), + op = name.replace("HKDF-SHA-", "HS"); + + // NOTE: no nodejs/webcrypto/fallback model, since this HKDF is + // implemented using the HMAC algorithms + + var fn = function(key, props) { + var hashLen = CONSTANTS.HASHLENGTH[hash] / 8; + + if ("string" === typeof op) { + op = hmac[op].sign; + } + + // prepare options + props = props || {}; + var salt = props.salt; + if (!salt || 0 === salt.length) { + salt = new Buffer(hashLen); + salt.fill(0); + } + var info = props.info || new Buffer(0); + var keyLen = props.length || hashLen; + + var promise; + + // Setup Expansion + var N = Math.ceil(keyLen / hashLen), + okm = [], + idx = 0; + function expand(key, T) { + if (N === idx++) { + return Buffer.concat(okm).slice(0, keyLen); + } + + if (!T) { + T = new Buffer(0); + } + T = Buffer.concat([T, info, new Buffer([idx])]); + T = op(key, T, { loose: true }); + T = T.then(function(result) { + T = result.mac; + okm.push(T); + + return expand(key, T); + }); + return T; + } + + // Step 1: Extract + promise = op(salt, key, { loose: true }); + promise = promise.then(function(result) { + // Step 2: Expand + return expand(result.mac); + }); + + return promise; + }; + + return fn; +} + +// Public API +// * [name].derive +var hkdf = {}; +[ + "HKDF-SHA-1", + "HKDF-SHA-256", + "HKDF-SHA-384", + "HKDF-SHA-512" +].forEach(function(name) { + hkdf[name] = { + derive: hkdfDeriveFn(name) + }; +}); + +module.exports = hkdf; diff --git a/lib/algorithms/hmac.js b/lib/algorithms/hmac.js new file mode 100644 index 0000000..e2e9eb6 --- /dev/null +++ b/lib/algorithms/hmac.js @@ -0,0 +1,206 @@ +/*! + * algorithms/hmac.js - HMAC-based "signatures" + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var CONSTANTS = require("./constants"), + forge = require("../deps/forge.js"), + DataBuffer = require("../util/databuffer.js"), + helpers = require("./helpers.js"); + +function hmacSignFN(name) { + var md = name.replace("HS", "SHA").toLowerCase(), + hash = name.replace("HS", "SHA-"); + + // ### Fallback Implementation -- uses forge + var fallback = function(key, pdata, props) { + props = props || {}; + if (!props.loose && CONSTANTS.HASHLENGTH[hash] > (key.length << 3)) { + return Promise.reject(new Error("invalid key length")); + } + + var sig = forge.hmac.create(); + sig.start(md, key.toString("binary")); + sig.update(pdata); + sig = sig.digest().native(); + + return Promise.resolve({ + data: pdata, + mac: sig + }); + }; + + // ### WebCryptoAPI Implementation + var webcrypto = function(key, pdata, props) { + props = props || {}; + if (!props.loose && CONSTANTS.HASHLENGTH[hash] > (key.length << 3)) { + return Promise.reject(new Error("invalid key length")); + } + + var alg = { + name: "HMAC", + hash: { + name: hash + } + }; + var promise; + promise = helpers.subtleCrypto.importKey("raw", key, alg, true, ["sign"]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.sign(alg, key, pdata); + }); + promise = promise.then(function(result) { + // wrap in *augmented* Uint8Array - Buffer without copies + var sig = new Uint8Array(result); + sig = Buffer._augment(sig); + return { + data: pdata, + mac: sig + }; + }); + + return promise; + }; + + // ### NodeJS implementation + var nodejs = function(key, pdata, props) { + props = props || {}; + if (!props.loose && CONSTANTS.HASHLENGTH[hash] > (key.length << 3)) { + return Promise.reject(new Error("invalid key length")); + } + + var hmac = helpers.nodeCrypto.createHmac(md, key); + hmac.update(pdata); + + var sig = hmac.digest(); + return { + data: pdata, + mac: sig + }; + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} + +function hmacVerifyFN(name) { + var md = name.replace("HS", "SHA").toLowerCase(), + hash = name.replace("HS", "SHA-"); + + function compare(loose, expected, actual) { + var len = loose ? expected.length : CONSTANTS.HASHLENGTH[hash] / 8, + valid = true; + for (var idx = 0; len > idx; idx++) { + valid = valid && (expected[idx] === actual[idx]); + } + return valid; + } + + // ### Fallback Implementation -- uses forge + var fallback = function(key, pdata, mac, props) { + props = props || {}; + if (!props.loose && CONSTANTS.HASHLENGTH[hash] > (key.length << 3)) { + return Promise.reject(new Error("invalid key length")); + } + + var vrfy = forge.hmac.create(); + vrfy.start(md, new DataBuffer(key)); + vrfy.update(pdata); + vrfy = vrfy.digest().native(); + + if (compare(props.loose, mac, vrfy)) { + return Promise.resolve({ + data: pdata, + mac: mac, + valid: true + }); + } else { + return Promise.reject(new Error("verification failed")); + } + }; + + var webcrypto = function(key, pdata, mac, props) { + props = props || {}; + if (!props.loose && CONSTANTS.HASHLENGTH[hash] > (key.length << 3)) { + return Promise.reject(new Error("invalid key length")); + } + + var alg = { + name: "HMAC", + hash: { + name: hash + } + }; + var promise; + if (props.loose) { + promise = helpers.subtleCrypto.importKey("raw", key, alg, true, ["sign"]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.sign(alg, key, pdata); + }); + promise = promise.then(function(result) { + // wrap in *augmented* Uint8Array - Buffer without copies + var sig = new Uint8Array(result); + sig = Buffer._augment(sig); + return compare(true, mac, sig); + }); + } else { + promise = helpers.subtleCrypto.importKey("raw", key, alg, true, ["verify"]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.verify(alg, key, mac, pdata); + }); + } + promise = promise.then(function(result) { + if (!result) { + return Promise.reject("verifaction failed"); + } + + return { + data: pdata, + mac: mac, + valid: true + }; + }); + + return promise; + }; + + var nodejs = function(key, pdata, mac, props) { + props = props || {}; + if (!props.loose && CONSTANTS.HASHLENGTH[hash] > (key.length << 3)) { + return Promise.reject(new Error("invalid key length")); + } + + var hmac = helpers.nodeCrypto.createHmac(md, key); + hmac.update(pdata); + + var sig = hmac.digest(); + if (!compare(props.loose, mac, sig)) { + throw new Error("verification failed"); + } + return { + data: pdata, + mac: sig, + valid: true + }; + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} + +// ### Public API +// * [name].sign +// * [name].verify +var hmac = {}; +[ + "HS1", + "HS256", + "HS384", + "HS512" +].forEach(function(alg) { + hmac[alg] = { + sign: hmacSignFN(alg), + verify: hmacVerifyFN(alg) + }; +}); + +module.exports = hmac; diff --git a/lib/algorithms/index.js b/lib/algorithms/index.js new file mode 100644 index 0000000..7918d7e --- /dev/null +++ b/lib/algorithms/index.js @@ -0,0 +1,110 @@ +/*! + * algorithms/index.js - Cryptographic Algorithms Entry Point + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +// setup implementations +var implementations = [ + require("./aes-cbc-hmac-sha2.js"), + require("./aes-gcm.js"), + require("./aes-kw.js"), + require("./concat.js"), + require("./dir.js"), + require("./ecdh.js"), + require("./ecdsa.js"), + require("./hkdf.js"), + require("./hmac.js"), + require("./pbes2.js"), + require("./rsaes.js"), + require("./rsassa.js"), + require("./sha.js") +]; + +var ALGS_DIGEST = {}; +var ALGS_DERIVE = {}; +var ALGS_SIGN = {}, + ALGS_VRFY = {}; +var ALGS_ENC = {}, + ALGS_DEC = {}; + +implementations.forEach(function(mod) { + Object.keys(mod).forEach(function(alg) { + var op = mod[alg]; + + if ("function" === typeof op.encrypt) { + ALGS_ENC[alg] = op.encrypt; + } + if ("function" === typeof op.decrypt) { + ALGS_DEC[alg] = op.decrypt; + } + if ("function" === typeof op.sign) { + ALGS_SIGN[alg] = op.sign; + } + if ("function" === typeof op.verify) { + ALGS_VRFY[alg] = op.verify; + } + if ("function" === typeof op.digest) { + ALGS_DIGEST[alg] = op.digest; + } + if ("function" === typeof op.derive) { + ALGS_DERIVE[alg] = op.derive; + } + }); +}); + +// public API +exports.digest = function(alg, data, props) { + var op = ALGS_DIGEST[alg]; + if (!op) { + return Promise.reject(new Error("unsupported algorithm: " + alg)); + } + + return op(data, props); +}; + +exports.derive = function(alg, key, props) { + var op = ALGS_DERIVE[alg]; + if (!op) { + return Promise.reject(new Error("unsupported algorithm: " + alg)); + } + + return op(key, props); +}; + +exports.sign = function(alg, key, pdata, props) { + var op = ALGS_SIGN[alg]; + if (!op) { + return Promise.reject(new Error("unsupported algorithm: " + alg)); + } + + return op(key, pdata, props || {}); +}; + +exports.verify = function(alg, key, pdata, mac, props) { + var op = ALGS_VRFY[alg]; + if (!op) { + return Promise.reject(new Error("unsupported algorithm: " + alg)); + } + + return op(key, pdata, mac, props || {}); +}; + +exports.encrypt = function(alg, key, pdata, props) { + var op = ALGS_ENC[alg]; + if (!op) { + return Promise.reject(new Error("unsupported algorithm: " + alg)); + } + + return op(key, pdata, props || {}); +}; + +exports.decrypt = function(alg, key, cdata, props) { + var op = ALGS_DEC[alg]; + if (!op) { + return Promise.reject(new Error("unsupported algorithm: " + alg)); + } + + return op(key, cdata, props || {}); +}; diff --git a/lib/algorithms/pbes2.js b/lib/algorithms/pbes2.js new file mode 100644 index 0000000..a6d8bc7 --- /dev/null +++ b/lib/algorithms/pbes2.js @@ -0,0 +1,234 @@ +/*! + * algorithms/pbes2.js - Password-Based Encryption (v2) Algorithms + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var forge = require("../deps/forge.js"), + util = require("../util"), + helpers = require("./helpers.js"), + CONSTANTS = require("./constants.js"), + KW = require("./aes-kw.js"); + +var NULL_BUFFER = new Buffer([0]); + +function fixSalt(hmac, kw, salt) { + var alg = "PBES2-" + hmac + "+" + kw; + var output = [ + new Buffer(alg, "utf8"), + NULL_BUFFER, + salt + ]; + return Buffer.concat(output); +} + +function pbes2EncryptFN(hmac, kw) { + var keyLen = CONSTANTS.KEYLENGTH[kw] / 8; + + var fallback = function(key, pdata, props) { + props = props || {}; + + var salt = util.asBuffer(props.p2s || new Buffer(0), "base64url"), + itrs = props.p2c || 0; + + if (0 >= itrs) { + return Promise.reject(new Error("invalid iteration count")); + } + + if (8 > salt.length) { + return Promise.reject(new Error("salt too small")); + } + salt = fixSalt(hmac, kw, salt); + + var promise; + + // STEP 1: derive shared key + promise = new Promise(function(resolve, reject) { + var md = forge.md[hmac.replace("HS", "SHA").toLowerCase()].create(); + var cb = function(err, dk) { + if (err) { + reject(err); + } else { + dk = new Buffer(dk, "binary"); + resolve(dk); + } + }; + + forge.pkcs5.pbkdf2(key.toString("binary"), + salt.toString("binary"), + itrs, + keyLen, + md, + cb); + }); + + // STEP 2: encrypt cek + promise = promise.then(function(dk) { + return KW[kw].encrypt(dk, pdata); + }); + return promise; + }; + + // NOTE: WebCrypto API missing until there's better support + var webcrypto = null; + + var nodejs = function(key, pdata, props) { + if (6 > helpers.nodeCrypto.pbkdf2.length) { + throw new Error("unsupported algorithm: PBES2-" + hmac + "+" + kw); + } + + props = props || {}; + + var salt = util.asBuffer(props.p2s || new Buffer(0), "base64url"), + itrs = props.p2c || 0; + + if (0 >= itrs) { + return Promise.reject(new Error("invalid iteration count")); + } + + if (8 > salt.length) { + return Promise.reject(new Error("salt too small")); + } + salt = fixSalt(hmac, kw, salt); + + var promise; + + // STEP 1: derive shared key + var hash = hmac.replace("HS", "SHA"); + promise = new Promise(function(resolve, reject) { + function cb(err, dk) { + if (err) { + reject(err); + } else { + resolve(dk); + } + } + helpers.nodeCrypto.pbkdf2(key, salt, itrs, keyLen, hash, cb); + }); + + // STEP 2: encrypt cek + promise = promise.then(function(dk) { + return KW[kw].encrypt(dk, pdata); + }); + + return promise; + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} + +function pbes2DecryptFN(hmac, kw) { + var keyLen = CONSTANTS.KEYLENGTH[kw] / 8; + + var fallback = function(key, cdata, props) { + props = props || {}; + + var salt = util.asBuffer(props.p2s || new Buffer(0), "base64url"), + itrs = props.p2c || 0; + + if (0 >= itrs) { + return Promise.reject(new Error("invalid iteration count")); + } + + if (8 > salt.length) { + return Promise.reject(new Error("salt too small")); + } + salt = fixSalt(hmac, kw, salt); + + var promise; + + // STEP 1: derived shared key + promise = new Promise(function(resolve, reject) { + var md = forge.md[hmac.replace("HS", "SHA").toLowerCase()].create(); + var cb = function(err, dk) { + if (err) { + reject(err); + } else { + dk = new Buffer(dk, "binary"); + resolve(dk); + } + }; + + forge.pkcs5.pbkdf2(key.toString("binary"), + salt.toString("binary"), + itrs, + keyLen, + md, + cb); + }); + + // STEP 2: decrypt cek + promise = promise.then(function(dk) { + return KW[kw].decrypt(dk, cdata); + }); + return promise; + }; + + // NOTE: WebCrypto API missing until there's better support + var webcrypto = null; + + var nodejs = function(key, cdata, props) { + if (6 > helpers.nodeCrypto.pbkdf2.length) { + throw new Error("unsupported algorithm: PBES2-" + hmac + "+" + kw); + } + + props = props || {}; + + var salt = util.asBuffer(props.p2s || new Buffer(0), "base64url"), + itrs = props.p2c || 0; + + if (0 >= itrs) { + return Promise.reject(new Error("invalid iteration count")); + } + + if (8 > salt.length) { + return Promise.reject(new Error("salt too small")); + } + salt = fixSalt(hmac, kw, salt); + + var promise; + + // STEP 1: derive shared key + var hash = hmac.replace("HS", "SHA"); + promise = new Promise(function(resolve, reject) { + function cb(err, dk) { + if (err) { + reject(err); + } else { + resolve(dk); + } + } + helpers.nodeCrypto.pbkdf2(key, salt, itrs, keyLen, hash, cb); + }); + + // STEP 2: decrypt cek + promise = promise.then(function(dk) { + return KW[kw].decrypt(dk, cdata); + }); + + return promise; + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} + +// ### Public API +// [name].encrypt +// [name].decrypt +var pbes2 = {}; +[ + "PBES2-HS256+A128KW", + "PBES2-HS384+A192KW", + "PBES2-HS512+A256KW" +].forEach(function(alg) { + var parts = /PBES2-(HS\d+)\+(A\d+KW)/g.exec(alg); + var hmac = parts[1], + kw = parts[2]; + pbes2[alg] = { + encrypt: pbes2EncryptFN(hmac, kw), + decrypt: pbes2DecryptFN(hmac, kw) + }; +}); + +module.exports = pbes2; diff --git a/lib/algorithms/rsa-util.js b/lib/algorithms/rsa-util.js new file mode 100644 index 0000000..818797c --- /dev/null +++ b/lib/algorithms/rsa-util.js @@ -0,0 +1,55 @@ +/*! + * algorithms/rsa-util.js - RSA Utility Functions + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var clone = require("lodash.clone"), + forge = require("../deps/forge.js"), + util = require("../util"); + +// ### RSA-specific Helpers +function convertToForge(key, isPublic) { + var parts = isPublic ? + ["n", "e"] : + ["n", "e", "d", "p", "q", "dp", "dq", "qi"]; + parts = parts.map(function(f) { + return new forge.jsbn.BigInteger(key[f].toString("hex"), 16); + }); + + var fn = isPublic ? + forge.pki.rsa.setPublicKey : + forge.pki.rsa.setPrivateKey; + return fn.apply(forge.pki.rsa, parts); +} +function convertToJWK(key, isPublic) { + var result = clone(key); + var parts = isPublic ? + ["n", "e"] : + ["n", "e", "d", "p", "q", "dp", "dq", "qi"]; + parts.forEach(function(f) { + result[f] = util.base64url.encode(result[f]); + }); + + // remove potentially troublesome properties + delete result.key_ops; + delete result.use; + delete result.alg; + + if (isPublic) { + delete result.d; + delete result.p; + delete result.q; + delete result.dp; + delete result.dq; + delete result.qi; + } + + return result; +} + +module.exports = { + convertToForge: convertToForge, + convertToJWK: convertToJWK +}; diff --git a/lib/algorithms/rsaes.js b/lib/algorithms/rsaes.js new file mode 100644 index 0000000..af5231e --- /dev/null +++ b/lib/algorithms/rsaes.js @@ -0,0 +1,163 @@ +/*! + * algorithms/rsassa.js - RSA Signatures + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var forge = require("../deps/forge.js"), + helpers = require("./helpers.js"), + DataBuffer = require("../util/databuffer.js"), + rsaUtil = require("./rsa-util.js"); + +// ### RSAES-PKCS1-v1_5 + +// ### RSAES-OAEP +function rsaesEncryptFn(name) { + var alg = { + name: name + }; + + if ("RSA-OAEP-256" === name) { + alg.name = "RSA-OAEP"; + alg.hash = { + name: "SHA-256" + }; + } else if ("RSA-OAEP" === name) { + alg.hash = { + name: "SHA-1" + }; + } else { + alg.name = "RSAES-PKCS1-v1_5"; + } + + // ### Fallback Implementation -- uses forge + var fallback = function(key, pdata) { + // convert pdata to byte string + pdata = new DataBuffer(pdata).bytes(); + + // encrypt it + var pki = rsaUtil.convertToForge(key, true), + params = {}; + if ("RSA-OAEP" === alg.name) { + params.md = alg.hash.name.toLowerCase().replace(/\-/g, ""); + params.md = forge.md[params.md].create(); + } + var cdata = pki.encrypt(pdata, alg.name.toUpperCase(), params); + + // convert cdata to Buffer + cdata = new DataBuffer(cdata).native(); + + return Promise.resolve({ + data: cdata + }); + }; + + // ### WebCryptoAPI Implementation + var webcrypto; + if ("RSAES-PKCS1-v1_5" !== alg.name) { + webcrypto = function(key, pdata) { + key = rsaUtil.convertToJWK(key, true); + var promise; + promise = helpers.subtleCrypto.importKey("jwk", key, alg, true, ["encrypt"]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.encrypt(alg, key, pdata); + }); + promise = promise.then(function(result) { + // wrap in *augmented* Uint8Array - Buffer without copies + var cdata = new Uint8Array(result); + cdata = Buffer._augment(cdata); + return { + data: cdata + }; + }); + + return promise; + }; + } else { + webcrypto = null; + } + + return helpers.setupFallback(null, webcrypto, fallback); +} + +function rsaesDecryptFn(name) { + var alg = { + name: name + }; + + if ("RSA-OAEP-256" === name) { + alg.name = "RSA-OAEP"; + alg.hash = { + name: "SHA-256" + }; + } else if ("RSA-OAEP" === name) { + alg.hash = { + name: "SHA-1" + }; + } else { + alg.name = "RSAES-PKCS1-v1_5"; + } + + // ### Fallback Implementation -- uses forge + var fallback = function(key, cdata) { + // convert cdata to byte string + cdata = new DataBuffer(cdata).bytes(); + + // decrypt it + var pki = rsaUtil.convertToForge(key, false), + params = {}; + if ("RSA-OAEP" === alg.name) { + params.md = alg.hash.name.toLowerCase().replace(/\-/g, ""); + params.md = forge.md[params.md].create(); + } + var pdata = pki.decrypt(cdata, alg.name.toUpperCase(), params); + + // convert pdata to Buffer + pdata = new DataBuffer(pdata).native(); + + return Promise.resolve(pdata); + }; + + // ### WebCryptoAPI Implementation + var webcrypto; + if ("RSAES-PKCS1-v1_5" !== alg.name) { + webcrypto = function(key, pdata) { + key = rsaUtil.convertToJWK(key, false); + var promise; + promise = helpers.subtleCrypto.importKey("jwk", key, alg, true, ["decrypt"]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.decrypt(alg, key, pdata); + }); + promise = promise.then(function(result) { + // wrap in *augmented* Uint8Array - Buffer without copies + var pdata = new Uint8Array(result); + pdata = Buffer._augment(pdata); + return pdata; + }); + + return promise; + }; + } else { + webcrypto = null; + } + + return helpers.setupFallback(null, webcrypto, fallback); +} + +// ### Public API +// * [name].encrypt +// * [name].decrypt +var rsaes = {}; +[ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" +].forEach(function(name) { + rsaes[name] = { + encrypt: rsaesEncryptFn(name), + decrypt: rsaesDecryptFn(name) + }; +}); + +module.exports = rsaes; diff --git a/lib/algorithms/rsassa.js b/lib/algorithms/rsassa.js new file mode 100644 index 0000000..e79b735 --- /dev/null +++ b/lib/algorithms/rsassa.js @@ -0,0 +1,219 @@ +/*! + * algorithms/rsassa.js - RSA Signatures + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var forge = require("../deps/forge.js"), + CONSTANTS = require("./constants"), + helpers = require("./helpers.js"), + rsaUtil = require("./rsa-util.js"); + +// ### RSASSA-PKCS1-v1_5 + +function rsassaV15SignFn(name) { + var md = name.replace("RS", "SHA").toLowerCase(), + hash = name.replace("RS", "SHA-"); + + var alg = { + name: "RSASSA-PKCS1-V1_5", + hash: { + name: hash + } + }; + + // ### Fallback Implementation -- uses forge + var fallback = function(key, pdata) { + // create the digest + var digest = forge.md[md].create(); + digest.start(); + digest.update(pdata); + + // sign it + var pki = rsaUtil.convertToForge(key, false); + var sig = pki.sign(digest, "RSASSA-PKCS1-V1_5"); + sig = new Buffer(sig, "binary"); + + return Promise.resolve({ + data: pdata, + mac: sig + }); + }; + + // ### WebCryptoAPI Implementation + var webcrypto = function(key, pdata) { + key = rsaUtil.convertToJWK(key, false); + var promise; + promise = helpers.subtleCrypto.importKey("jwk", key, alg, true, ["sign"]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.sign(alg, key, pdata); + }); + promise = promise.then(function(result) { + // wrap in *augmented* Uint8Array - Buffer without copies + var sig = new Uint8Array(result); + sig = Buffer._augment(sig); + return { + data: pdata, + mac: sig + }; + }); + + return promise; + }; + + return helpers.setupFallback(null, webcrypto, fallback); +} + +function rsassaV15VerifyFn(name) { + var md = name.replace("RS", "SHA").toLowerCase(), + hash = name.replace("RS", "SHA-"); + var alg = { + name: "RSASSA-PKCS1-V1_5", + hash: { + name: hash + } + }; + + // ### Fallback implementation -- uses forge + var fallback = function(key, pdata, mac) { + // create the digest + var digest = forge.md[md].create(); + digest.start(); + digest.update(pdata); + digest = digest.digest().bytes(); + + // verify it + var pki = rsaUtil.convertToForge(key, true); + var sig = mac.toString("binary"); + var result = pki.verify(digest, sig, "RSASSA-PKCS1-V1_5"); + if (!result) { + return Promise.reject(new Error("verification failed")); + } + return Promise.resolve({ + data: pdata, + mac: mac, + valid: true + }); + }; + + // ### WebCryptoAPI Implementation + var webcrypto = function(key, pdata, mac) { + key = rsaUtil.convertToJWK(key, true); + var promise; + promise = helpers.subtleCrypto.importKey("jwk", key, alg, true, ["verify"]); + promise = promise.then(function(key) { + return helpers.subtleCrypto.verify(alg, key, mac, pdata); + }); + promise = promise.then(function(result) { + if (!result) { + return Promise.reject(new Error("verification failed")); + } + + return { + data: pdata, + mac: mac, + valid: true + }; + }); + + return promise; + }; + + return helpers.setupFallback(null, webcrypto, fallback); +} + +// ### RSA-PSS +// NOTE: no WebCryptoAPI variant -- too many browsers don't +// implement it yet (e.g., Firefox, Safari) + +function rsassaPssSignFn(name) { + var md = name.replace("PS", "SHA").toLowerCase(), + hash = name.replace("PS", "SHA-"); + + return function(key, pdata) { + // create the digest + var digest = forge.md[md].create(); + digest.start(); + digest.update(pdata); + + // setup padding + var pss = forge.pss.create({ + md: forge.md[md].create(), + mgf: forge.mgf.mgf1.create(forge.md[md].create()), + saltLength: CONSTANTS.HASHLENGTH[hash] / 8 + }); + + // sign it + var pki = rsaUtil.convertToForge(key, false); + var sig = pki.sign(digest, pss); + sig = new Buffer(sig, "binary"); + + return Promise.resolve({ + data: pdata, + mac: sig + }); + }; +} + +function rsassaPssVerifyFn(name) { + var md = name.replace("PS", "SHA").toLowerCase(), + hash = name.replace("PS", "SHA-"); + + // ### Fallback implementation -- uses forge + return function(key, pdata, mac) { + // create the digest + var digest = forge.md[md].create(); + digest.start(); + digest.update(pdata); + digest = digest.digest().bytes(); + + // setup padding + var pss = forge.pss.create({ + md: forge.md[md].create(), + mgf: forge.mgf.mgf1.create(forge.md[md].create()), + saltLength: CONSTANTS.HASHLENGTH[hash] / 8 + }); + + // verify it + var pki = rsaUtil.convertToForge(key, true); + var sig = mac.toString("binary"); + var result = pki.verify(digest, sig, pss); + if (!result) { + return Promise.reject(new Error("verification failed")); + } + return Promise.resolve({ + data: pdata, + mac: mac, + valid: true + }); + }; +} + +// ### Public API +// * [name].sign +// * [name].verify +var rsassa = {}; +[ + "PS256", + "PS384", + "PS512" +].forEach(function(name) { + rsassa[name] = { + sign: rsassaPssSignFn(name), + verify: rsassaPssVerifyFn(name) + }; +}); + +[ + "RS256", + "RS384", + "RS512" +].forEach(function(name) { + rsassa[name] = { + sign: rsassaV15SignFn(name), + verify: rsassaV15VerifyFn(name) + }; +}); + +module.exports = rsassa; diff --git a/lib/algorithms/sha.js b/lib/algorithms/sha.js new file mode 100644 index 0000000..76ec861 --- /dev/null +++ b/lib/algorithms/sha.js @@ -0,0 +1,64 @@ +/*! + * algorithms/sha.js - Cryptographic Secure Hash Algorithms, versions 1 and 2 + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var forge = require("../deps/forge.js"), + helpers = require("./helpers.js"); + +function hashDigestFN(hash) { + var md = hash.replace("SHA-", "SHA").toLowerCase(); + + var alg = { + name: hash + }; + + // ### Fallback Implementation -- uses forge + var fallback = function(pdata /* props */) { + var digest = forge.md[md].create(); + digest.update(pdata); + digest = digest.digest().native(); + + return Promise.resolve(digest); + }; + + // ### WebCryptoAPI Implementation + var webcrypto = function(pdata /* props */) { + var promise; + promise = helpers.subtleCrypto.digest(alg, pdata); + promise = promise.then(function(result) { + // wrap in *augmented* Uint8Array -- Buffer without copies + result = new Uint8Array(result); + result = Buffer._augment(result); + return result; + }); + return promise; + }; + + // ### nodejs Implementation + var nodejs = function(pdata /* props */) { + var digest = helpers.nodeCrypto.createHash(md); + digest.update(pdata); + return digest.digest(); + }; + + return helpers.setupFallback(nodejs, webcrypto, fallback); +} + +// Public API +// * [name].digest +var sha = {}; +[ + "SHA-1", + "SHA-256", + "SHA-384", + "SHA-512" +].forEach(function(name) { + sha[name] = { + digest: hashDigestFN(name) + }; +}); + +module.exports = sha; diff --git a/lib/deps/ciphermodes/gcm/helpers.js b/lib/deps/ciphermodes/gcm/helpers.js new file mode 100644 index 0000000..046ff71 --- /dev/null +++ b/lib/deps/ciphermodes/gcm/helpers.js @@ -0,0 +1,259 @@ +/*! + * deps/ciphermodes/gcm/helpers.js - AES-GCM Helper Functions + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var Long = require("long"), + fill = require("lodash.fill"), + pack = require("../pack.js"); + +var E1 = 0xe1000000, + E1B = 0xe1, + E1L = new Long(E1 >> 8); + +function generateLookup() { + var lookup = []; + + for (var c = 0; c < 256; ++c) { + var v = 0; + for (var i = 7; i >= 0; --i) { + if ((c & (1 << i)) !== 0) { + v ^= (E1 >>> (7 - i)); + } + } + lookup.push(v); + } + + return lookup; +} + +var helpers = module.exports = { + // ### Constants + E1: E1, + E1B: E1B, + E1L: E1L, + LOOKUP: generateLookup(), + + // ### Array Helpers + arrayCopy: function(src, srcPos, dest, destPos, length) { + // Start by checking for negatives since arrays in JS auto-expand + if (srcPos < 0 || destPos < 0 || length < 0) { + throw new TypeError("Invalid input."); + } + + if (dest instanceof Uint8Array) { + // Check for overflow if dest is a typed-array + if (destPos >= dest.length || (destPos + length) > dest.length) { + throw new TypeError("Invalid input."); + } + + if (srcPos !== 0 || length < src.length) { + if (src instanceof Uint8Array) { + src = src.subarray(srcPos, srcPos + length); + } else { + src = src.slice(srcPos, srcPos + length); + } + } + + dest.set(src, destPos); + } else { + for (var i = 0; i < length; ++i) { + dest[destPos + i] = src[srcPos + i]; + } + } + }, + arrayEqual: function(a1, a2) { + a1 = a1 || []; + a2 = a2 || []; + + var len = Math.min(a1.length, a2.length), + result = (a1.length === a2.length); + + for (var idx = 0; idx < len; idx++) { + result = result && + ("undefined" !== typeof a1[idx]) && + ("undefined" !== typeof a2[idx]) && + (a1[idx] === a2[idx]); + } + + return result; + }, + + // ### Conversions + asBytes: function(x, z) { + switch (arguments.length) { + case 1: + z = new Buffer(16); + z.fill(0); + pack.intToBigEndian(x, z, 0); + return z; + case 2: + pack.intToBigEndian(x, z, 0); + break; + default: + throw new TypeError("Expected 1 or 2 arguments."); + } + }, + asInts: function(x, z) { + switch (arguments.length) { + case 1: + z = []; + fill(z, 0, 0, 4); + pack.bigEndianToInt(x, 0, z); + return z; + case 2: + pack.bigEndianToInt(x, 0, z); + break; + default: + throw new TypeError("Expected 1 or 2 arguments."); + } + }, + oneAsInts: function() { + var tmp = []; + for (var c = 0; c < 4; ++c) { + tmp.push(1 << 31); + } + return tmp; + }, + + // ## Bit-wise + shiftRight: function(x, z) { + var b, c; + switch (arguments.length) { + case 1: + b = x[0]; + x[0] = b >>> 1; + c = b << 31; + b = x[1]; + x[1] = (b >>> 1) | c; + c = b << 31; + b = x[2]; + x[2] = (b >>> 1) | c; + c = b << 31; + b = x[3]; + x[3] = (b >>> 1) | c; + return (b << 31) & 0xffffffff; + case 2: + b = x[0]; + z[0] = b >>> 1; + c = b << 31; + b = x[1]; + z[1] = (b >>> 1) | c; + c = b << 31; + b = x[2]; + z[2] = (b >>> 1) | c; + c = b << 31; + b = x[3]; + z[3] = (b >>> 1) | c; + return (b << 31) & 0xffffffff; + default: + throw new TypeError("Expected 1 or 2 arguments."); + } + }, + shiftRightN: function(x, n, z) { + var nInv, b, c; + switch (arguments.length) { + case 2: + b = x[0]; + nInv = 32 - n; + x[0] = b >>> n; + c = b << nInv; + b = x[1]; + x[1] = (b >>> n) | c; + c = b << nInv; + b = x[2]; + x[2] = (b >>> n) | c; + c = b << nInv; + b = x[3]; + x[3] = (b >>> n) | c; + return b << nInv; + case 3: + b = x[0]; + nInv = 32 - n; + z[0] = b >>> n; + c = b << nInv; + b = x[1]; + z[1] = (b >>> n) | c; + c = b << nInv; + b = x[2]; + z[2] = (b >>> n) | c; + c = b << nInv; + b = x[3]; + z[3] = (b >>> n) | c; + return b << nInv; + default: + throw new TypeError("Expected 2 or 3 arguments."); + } + }, + xor: function(x, y, z) { + switch (arguments.length) { + case 2: + x[0] ^= y[0]; + x[1] ^= y[1]; + x[2] ^= y[2]; + x[3] ^= y[3]; + break; + case 3: + z[0] = x[0] ^ y[0]; + z[1] = x[1] ^ y[1]; + z[2] = x[2] ^ y[2]; + z[3] = x[3] ^ y[3]; + break; + default: + throw new TypeError("Expected 2 or 3 arguments."); + } + }, + + multiply: function(x, y) { + var r0 = x.slice(); + var r1 = []; + + for (var i = 0; i < 4; ++i) { + var bits = y[i]; + for (var j = 31; j >= 0; --j) { + if ((bits & (1 << j)) !== 0) { + helpers.xor(r1, r0); + } + + if (helpers.shiftRight(r0) !== 0) { + r0[0] ^= helpers.E1; + } + } + } + + helpers.arrayCopy(r1, 0, x, 0, 4); + }, + multiplyP: function(x, y) { + switch (arguments.length) { + case 1: + if (helpers.shiftRight(x) !== 0) { + x[0] ^= helpers.E1; + } + break; + case 2: + if (helpers.shiftRight(x, y) !== 0) { + y[0] ^= helpers.E1; + } + break; + default: + throw new TypeError("Expected 1 or 2 arguments."); + } + }, + multiplyP8: function(x, y) { + var c; + switch (arguments.length) { + case 1: + c = helpers.shiftRightN(x, 8); + x[0] ^= helpers.LOOKUP[c >>> 24]; + break; + case 2: + c = helpers.shiftRightN(x, 8, y); + y[0] ^= helpers.LOOKUP[c >>> 24]; + break; + default: + throw new TypeError("Expected 1 or 2 arguments."); + } + } +}; diff --git a/lib/deps/ciphermodes/gcm/index.js b/lib/deps/ciphermodes/gcm/index.js new file mode 100644 index 0000000..6e00f4f --- /dev/null +++ b/lib/deps/ciphermodes/gcm/index.js @@ -0,0 +1,311 @@ +/*! + * deps/ciphermodes/gcm/index.js - AES-GCM implementation Entry Point + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ + "use strict"; + +var Long = require("long"), + forge = require("../../../deps/forge.js"), + multipliers = require("./multipliers.js"), + helpers = require("./helpers.js"), + pack = require("../pack.js"), + DataBuffer = require("../../../util/databuffer.js"), + cipherHelpers = require("../helpers.js"); + +var BLOCK_SIZE = 16; + +// ### GCM Mode +// ### Constructor +function Gcm(options) { + options = options || {}; + + this.name = "GCM"; + this.cipher = options.cipher; + this.blockSize = this.blockSize || 16; +} + +// ### exports +module.exports = { + createCipher: function(options) { + var alg = new forge.aes.Algorithm("AES-GCM", Gcm); + alg.initialize({ + key: new DataBuffer(options.key) + }); + alg.mode.start(options); + + return alg.mode; + }, + createDecipher: function(options) { + var alg = new forge.aes.Algorithm("AES-GCM", Gcm); + alg.initialize({ + key: new DataBuffer(options.key) + }); + alg.mode._decrypt = true; + alg.mode.start(options); + + return alg.mode; + } +}; + +// ### Public API +Gcm.prototype.start = function(options) { + this.tag = null; + + options = options || {}; + + if (!("iv" in options)) { + throw new Error("Gcm needs ParametersWithIV or AEADParameters"); + } + this.nonce = options.iv; + if (this.nonce == null || this.nonce.length < 1) { + throw new Error("IV must be at least 1 byte"); + } + + // TODO: variable tagLength? + this.tagLength = 16; + + // TODO: validate tag + if ("tag" in options) { + this.tag = new Buffer(options.tag); + } + + var bufLength = !this._decrypt ? + this.blockSize : + (this.blockSize + this.tagLength); + this.bufBlock = new Buffer(bufLength); + this.bufBlock.fill(0); + + var multiplier = options.multiplier; + if (multiplier == null) { + multiplier = new (multipliers["8k"])(); + } + this.multiplier = multiplier; + + this.H = this.zeroBlock(); + cipherHelpers.encrypt(this.cipher, this.H, 0, this.H, 0); + + // GcmMultiplier tables don"t change unless the key changes + // (and are expensive to init) + this.multiplier.init(this.H); + this.exp = null; + + this.J0 = this.zeroBlock(); + + if (this.nonce.length === 12) { + this.nonce.copy(this.J0, 0, 0, this.nonce.length); + this.J0[this.blockSize - 1] = 0x01; + } else { + this.gHASH(this.J0, this.nonce, this.nonce.length); + var X = this.zeroBlock(); + pack.longToBigEndian(new Long(this.nonce.length). + multiply(8), X, 8); + this.gHASHBlock(this.J0, X); + } + + this.S = this.zeroBlock(); + this.SAt = this.zeroBlock(); + this.SAtPre = this.zeroBlock(); + this.atBlock = this.zeroBlock(); + this.atBlockPos = 0; + this.atLength = Long.ZERO; + this.atLengthPre = Long.ZERO; + this.counter = new Buffer(this.J0); + this.bufOff = 0; + this.totalLength = Long.ZERO; + + if ("additionalData" in options) { + this.processAADBytes(options.additionalData, 0, options.additionalData.length); + } +}; + +Gcm.prototype.update = function(inV, inOff, len, out, outOff) { + var resultLen = 0; + + while (len > 0) { + var inLen = Math.min(len, this.bufBlock.length - this.bufOff); + inV.copy(this.bufBlock, this.bufOff, inOff, inOff + inLen); + len -= inLen; + inOff += inLen; + this.bufOff += inLen; + if (this.bufOff === this.bufBlock.length) { + this.outputBlock(out, outOff + resultLen); + resultLen += this.blockSize; + } + } + + return resultLen; +}; +Gcm.prototype.finish = function(out, outOff) { + var resultLen = 0; + + if (this._decrypt) { + // append tag + resultLen += this.update(this.tag, 0, this.tag.length, out, outOff); + } + + if (this.totalLength.isZero()) { + this.initCipher(); + } + + var extra = this.bufOff; + if (this._decrypt) { + if (extra < this.tagLength) { + throw new Error("data too short"); + } + extra -= this.tagLength; + } + + if (extra > 0) { + this.gCTRPartial(this.bufBlock, 0, extra, out, outOff + resultLen); + resultLen += extra; + } + + this.atLength = this.atLength.add(this.atBlockPos); + + // Final gHASH + var X = this.zeroBlock(); + pack.longToBigEndian(this.atLength.multiply(8), + X, + 0); + pack.longToBigEndian(this.totalLength.multiply(8), + X, + 8); + + this.gHASHBlock(this.S, X); + + // TODO Fix this if tagLength becomes configurable + // T = MSBt(GCTRk(J0,S)) + var tag = new Buffer(this.blockSize); + tag.fill(0); + cipherHelpers.encrypt(this.cipher, this.J0, 0, tag, 0); + this.xor(tag, this.S); + + if (this._decrypt) { + if (!helpers.arrayEqual(this.tag, tag)) { + throw new Error("mac check in Gcm failed"); + } + } else { + // We place into tag our calculated value for T + this.tag = new Buffer(this.tagLength); + tag.copy(this.tag, 0, 0, this.tagLength); + } + + return resultLen; +}; + +// ### "Internal" Helper Functions +Gcm.prototype.initCipher = function() { + if (this.atLength.greaterThan(Long.ZERO)) { + this.SAt.copy(this.SAtPre, 0, 0, this.blockSize); + this.atLengthPre = this.atLength.add(Long.ZERO); + } + + // Finish hash for partial AAD block + if (this.atBlockPos > 0) { + this.gHASHPartial(this.SAtPre, this.atBlock, 0, this.atBlockPos); + this.atLengthPre = this.atLengthPre.add(this.atBlockPos); + } + + if (this.atLengthPre.greaterThan(Long.ZERO)) { + this.SAtPre.copy(this.S, 0, 0, this.blockSize); + } +}; + +Gcm.prototype.outputBlock = function(output, offset) { + if (this.totalLength.isZero()) { + this.initCipher(); + } + this.gCTRBlock(this.bufBlock, output, offset); + if (!this._decrypt) { + this.bufOff = 0; + } else { + this.bufBlock.copy(this.bufBlock, 0, this.blockSize, this.blockSize + this.tagLength); + this.bufOff = this.tagLength; + } +}; + +Gcm.prototype.processAADBytes = function(inV, inOff, len) { + for (var i = 0; i < len; ++i) { + this.atBlock[this.atBlockPos] = inV[inOff + i]; + if (++this.atBlockPos === this.blockSize) { + // Hash each block as it fills + this.gHASHBlock(this.SAt, this.atBlock); + this.atBlockPos = 0; + this.atLength = this.atLength.add(this.blockSize); + } + } +}; + +Gcm.prototype.getNextCounterBlock = function() { + for (var i = 15; i >= 12; --i) { + var b = ((this.counter[i] + 1) & 0xff); + this.counter[i] = b; + + if (b !== 0) { + break; + } + } + + // encrypt counter + var outb = new Buffer(this.blockSize); + outb.fill(0); + cipherHelpers.encrypt(this.cipher, this.counter, 0, outb, 0); + + return outb; +}; + +Gcm.prototype.gCTRBlock = function(block, out, outOff) { + var tmp = this.getNextCounterBlock(); + + this.xor(tmp, block); + tmp.copy(out, outOff, 0, this.blockSize); + + this.gHASHBlock(this.S, !this._decrypt ? tmp : block); + + this.totalLength = this.totalLength.add(this.blockSize); +}; +Gcm.prototype.gCTRPartial = function(buf, off, len, out, outOff) { + var tmp = this.getNextCounterBlock(); + + this.xor(tmp, buf, off, len); + tmp.copy(out, outOff, 0, len); + + this.gHASHPartial(this.S, !this._decrypt ? tmp : buf, 0, len); + + this.totalLength = this.totalLength.add(len); +}; + +Gcm.prototype.gHASHBlock = function(Y, b) { + this.xor(Y, b); + this.multiplier.multiplyH(Y); +}; +Gcm.prototype.gHASHPartial = function(Y, b, off, len) { + this.xor(Y, b, off, len); + this.multiplier.multiplyH(Y); +}; + +Gcm.prototype.xor = function(block, val, off, len) { + switch (arguments.length) { + case 2: + for (var i = 15; i >= 0; --i) { + block[i] ^= val[i]; + } + break; + case 4: + while (len-- > 0) { + block[len] ^= val[off + len]; + } + break; + default: + throw new TypeError("Expected 2 or 4 arguments."); + } + + return block; +}; + +Gcm.prototype.zeroBlock = function() { + var block = new Buffer(BLOCK_SIZE); + block.fill(0); + return block; +}; diff --git a/lib/deps/ciphermodes/gcm/multipliers.js b/lib/deps/ciphermodes/gcm/multipliers.js new file mode 100644 index 0000000..90614a6 --- /dev/null +++ b/lib/deps/ciphermodes/gcm/multipliers.js @@ -0,0 +1,93 @@ +/*! + * deps/ciphermodes/gcm/multipliers.js - AES-GCM Multipliers + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ + "use strict"; + +var helpers = require("./helpers.js"), + pack = require("../pack.js"); + + +// ### 8K Table Multiplier +function Gcm8KMultiplier() { + this.H = []; + this.M = null; +} + +Gcm8KMultiplier.prototype.init = function(H) { + var i, j, k; + if (this.M == null) { + // sc: I realize this UGLY... + //M = new int[32][16][4]; + this.M = []; + for (i = 0; i < 32; ++i) { + this.M[i] = []; + for (j = 0; j < 16; ++j) { + this.M[i][j] = []; + for (k = 0; k < 4; ++k) { + this.M[i][j][k] = 0; + } + } + } + } else if (helpers.arrayEqual(this.H, H)) { + return; + } + + this.H = H.slice(); + + // M[0][0] is ZEROES; + // M[1][0] is ZEROES; + helpers.asInts(H, this.M[1][8]); + + for (j = 4; j >= 1; j >>= 1) { + helpers.multiplyP(this.M[1][j + j], this.M[1][j]); + } + helpers.multiplyP(this.M[1][1], this.M[0][8]); + + for (j = 4; j >= 1; j >>= 1) { + helpers.multiplyP(this.M[0][j + j], this.M[0][j]); + } + + i = 0; + for (;;) { + for (j = 2; j < 16; j += j) { + for (k = 1; k < j; ++k) { + helpers.xor(this.M[i][j], this.M[i][k], this.M[i][j + k]); + } + } + + if (++i === 32) { + return; + } + + if (i > 1) { + // M[i][0] is ZEROES; + for (j = 8; j > 0; j >>= 1) { + helpers.multiplyP8(this.M[i - 2][j], this.M[i][j]); + } + } + } +}; +Gcm8KMultiplier.prototype.multiplyH = function(x) { + var z = []; + for (var i = 15; i >= 0; --i) { + var m = this.M[i + i][x[i] & 0x0f]; + z[0] ^= m[0]; + z[1] ^= m[1]; + z[2] ^= m[2]; + z[3] ^= m[3]; + m = this.M[i + i + 1][(x[i] & 0xf0) >>> 4]; + z[0] ^= m[0]; + z[1] ^= m[1]; + z[2] ^= m[2]; + z[3] ^= m[3]; + } + + pack.intToBigEndian(z, x, 0); +}; + + +module.exports = { + "8k": Gcm8KMultiplier +}; diff --git a/lib/deps/ciphermodes/helpers.js b/lib/deps/ciphermodes/helpers.js new file mode 100644 index 0000000..8a59784 --- /dev/null +++ b/lib/deps/ciphermodes/helpers.js @@ -0,0 +1,21 @@ +/*! + * deps/ciphermodes/helpers.js - Cipher Helper Functions + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var pack = require("./pack.js"); + +function doEncrypt(cipher, inb, inOff, outb, outOff) { + var input = new Array(4), + output = new Array(4); + + pack.bigEndianToInt(inb, inOff, input); + cipher.encrypt(input, output); + pack.intToBigEndian(output, outb, outOff); +} + +module.exports = { + encrypt: doEncrypt +}; diff --git a/lib/deps/ciphermodes/pack.js b/lib/deps/ciphermodes/pack.js new file mode 100644 index 0000000..40e1402 --- /dev/null +++ b/lib/deps/ciphermodes/pack.js @@ -0,0 +1,123 @@ +/*! + * deps/ciphermodes/pack.js - Pack/Unpack Functions + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var Long = require("long"); + +var pack = module.exports = { + intToBigEndian: function(n, bs, off) { + if (typeof n === "number") { + switch (arguments.length) { + case 1: + bs = new Buffer(4); + bs.fill(0); + pack.intToBigEndian(n, bs, 0); + break; + case 3: + bs[off] = 0xff & (n >>> 24); + bs[++off] = 0xff & (n >>> 16); + bs[++off] = 0xff & (n >>> 8); + bs[++off] = 0xff & (n); + break; + default: + throw new TypeError("Expected 1 or 3 arguments."); + } + } else { + switch (arguments.length) { + case 1: + bs = new Buffer(4 * n.length); + bs.fill(0); + pack.intToBigEndian(n, bs, 0); + break; + case 3: + for (var i = 0; i < n.length; ++i) { + pack.intToBigEndian(n[i], bs, off); + off += 4; + } + break; + default: + throw new TypeError("Expected 1 or 3 arguments."); + } + } + + return bs; + }, + longToBigEndian: function(n, bs, off) { + if (!Array.isArray(n)) { + // Single + switch (arguments.length) { + case 1: + bs = new Buffer(8); + bs.fill(0); + pack.longToBigEndian(n, bs, 0); + break; + case 3: + var lo = n.low, + hi = n.high; + pack.intToBigEndian(hi, bs, off); + pack.intToBigEndian(lo, bs, off + 4); + break; + default: + throw new TypeError("Expected 1 or 3 arguments."); + } + } else { + // Array + switch (arguments.length) { + case 1: + bs = new Buffer(8 * n.length); + bs.fill(0); + pack.longToBigEndian(n, bs, 0); + break; + case 3: + for (var i = 0; i < n.length; ++i) { + pack.longToBigEndian(n[i], bs, off); + off += 8; + } + break; + default: + throw new TypeError("Expected 1 or 3 arguments."); + } + } + + return bs; + }, + + bigEndianToInt: function(bs, off, ns) { + switch (arguments.length) { + case 2: + var n = bs[off] << 24; + n |= (bs[++off] & 0xff) << 16; + n |= (bs[++off] & 0xff) << 8; + n |= (bs[++off] & 0xff); + return n; + case 3: + for (var i = 0; i < ns.length; ++i) { + ns[i] = pack.bigEndianToInt(bs, off); + off += 4; + } + break; + default: + throw new TypeError("Expected 2 or 3 arguments."); + } + }, + bigEndianToLong: function(bs, off, ns) { + switch (arguments.length) { + case 2: + var hi = pack.bigEndianToInt(bs, off); + var lo = pack.bigEndianToInt(bs, off + 4); + var num = new Long(lo, hi); + return num; + case 3: + for (var i = 0; i < ns.length; ++i) { + ns[i] = pack.bigEndianToLong(bs, off); + off += 8; + } + break; + default: + throw new TypeError("Expected 2 or 3 arguments."); + } + } +}; diff --git a/lib/deps/ecc/curves.js b/lib/deps/ecc/curves.js new file mode 100644 index 0000000..2b8585d --- /dev/null +++ b/lib/deps/ecc/curves.js @@ -0,0 +1,109 @@ +/** + * deps/ecc/curves.js - Elliptic Curve NIST/SECG/X9.62 Parameters + * Original Copyright (c) 2003-2005 Tom Wu. + * Modifications Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + * + * Ported from Tom Wu, which is ported from BouncyCastle + * Modified to reuse existing external NPM modules, restricted to the + * NIST//SECG/X9.62 prime curves only, and formatted to match project + * coding styles. + */ +"use strict"; + +// Named EC curves + +var BigInteger = require("jsbn").BigInteger, + ec = require("./math.js"); + +// ---------------- +// X9ECParameters + +// constructor +function X9ECParameters(curve, g, n, h) { + this.curve = curve; + this.g = g; + this.n = n; + this.h = h; +} + +function x9getCurve() { + return this.curve; +} + +function x9getG() { + return this.g; +} + +function x9getN() { + return this.n; +} + +function x9getH() { + return this.h; +} + +X9ECParameters.prototype.getCurve = x9getCurve; +X9ECParameters.prototype.getG = x9getG; +X9ECParameters.prototype.getN = x9getN; +X9ECParameters.prototype.getH = x9getH; + +// ---------------- +// SECNamedCurves + +function fromHex(s) { return new BigInteger(s, 16); } + +function secp256r1() { + // p = 2^224 (2^32 - 1) + 2^192 + 2^96 - 1 + var p = fromHex("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF"); + var a = fromHex("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC"); + var b = fromHex("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B"); + var n = fromHex("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"); + var h = BigInteger.ONE; + var curve = new ec.ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"); + return new X9ECParameters(curve, G, n, h); +} + +function secp384r1() { + // p = 2^384 - 2^128 - 2^96 + 2^32 - 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF"); + var a = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC"); + var b = fromHex("B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF"); + var n = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973"); + var h = BigInteger.ONE; + var curve = new ec.ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7" + + "3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F"); + return new X9ECParameters(curve, G, n, h); +} + +function secp521r1() { + // p = 2^521 - 1 + var p = fromHex("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + var a = fromHex("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC"); + var b = fromHex("0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00"); + var n = fromHex("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409"); + var h = BigInteger.ONE; + var curve = new ec.ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66" + + "011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650"); + return new X9ECParameters(curve, G, n, h); +} + +// ---------------- +// Public API + +var CURVES = module.exports = { + "secp256r1": secp256r1(), + "secp384r1": secp384r1(), + "secp521r1": secp521r1() +}; + +// also export NIST names +CURVES["P-256"] = CURVES.secp256r1; +CURVES["P-384"] = CURVES.secp384r1; +CURVES["P-521"] = CURVES.secp521r1; diff --git a/lib/deps/ecc/index.js b/lib/deps/ecc/index.js new file mode 100644 index 0000000..d591eec --- /dev/null +++ b/lib/deps/ecc/index.js @@ -0,0 +1,234 @@ +/** + * deps/ecc/index.js - Elliptic Curve Entry Point + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var forge = require("../../deps/forge"), + BigInteger = require("jsbn").BigInteger, + ec = require("./math.js"), + CURVES = require("./curves.js"); + +// ### Helpers +function hex2bn(s) { + return new BigInteger(s, 16); +} + +function bn2bin(bn, len) { + if (!len) { + len = Math.ceil(bn.bitLength() / 8); + } + len = len * 2; + + var hex = bn.toString(16); + // truncate-left if too large + hex = hex.substring(Math.max(hex.length - len, 0)); + // pad-left if too small + while (len > hex.length) { + hex = "0" + hex; + } + + return new Buffer(hex, "hex"); +} +function bin2bn(s) { + if ("string" === typeof s) { + s = new Buffer(s, "binary"); + } + return hex2bn(s.toString("hex")); +} + +function keySizeBytes(params) { + return Math.ceil(params.getN().bitLength() / 8); +} + +function namedCurve(curve) { + var params = CURVES[curve]; + if (!params) { + throw new TypeError("unsupported named curve: " + curve); + } + + return params; +} + +function normalizeEcdsa(params, md) { + var log2n = params.getN().bitLength(), + mdLen = md.length * 8; + + var e = bin2bn(md); + if (log2n < mdLen) { + e = e.shiftRight(mdLen - log2n); + } + + return e; +} + +// ### EC Public Key + +/** + * + * @param {String} curve The named curve + * @param {BigInteger} x The X coordinate + * @param {BigInteger} y The Y coordinate + */ +function ECPublicKey(curve, x, y) { + var params = namedCurve(curve), + c = params.getCurve(); + var key = new ec.ECPointFp(c, + c.fromBigInteger(x), + c.fromBigInteger(y)); + + this.curve = curve; + this.params = params; + this.point = key; + + var size = keySizeBytes(params); + this.x = bn2bin(x, size); + this.y = bn2bin(y, size); +} + +// ECDSA +ECPublicKey.prototype.verify = function(md, sig) { + var N = this.params.getN(), + G = this.params.getG(); + + // prepare and validate (r, s) + var r = bin2bn(sig.r), + s = bin2bn(sig.s); + if (r.compareTo(BigInteger.ONE) < 0 || r.compareTo(N) >= 0) { + return false; + } + if (s.compareTo(BigInteger.ONE) < 0 || r.compareTo(N) >= 0) { + return false; + } + + // normalize input + var e = normalizeEcdsa(this.params, md); + // verify (r, s) + var w = s.modInverse(N), + u1 = e.multiply(w).mod(N), + u2 = r.multiply(w).mod(N); + + var v = G.multiplyTwo(u1, this.point, u2).getX().toBigInteger(); + v = v.mod(N); + + return v.equals(r); +}; + +// ### EC Private Key + +/** + * @param {String} curve The named curve + * @param {Buffer} key The private key value + */ +function ECPrivateKey(curve, key) { + var params = namedCurve(curve); + this.curve = curve; + this.params = params; + + var size = keySizeBytes(params); + this.d = bn2bin(key, size); +} + +ECPrivateKey.prototype.toPublicKey = function() { + var d = bin2bn(this.d); + var P = this.params.getG().multiply(d); + return new ECPublicKey(this.curve, + P.getX().toBigInteger(), + P.getY().toBigInteger()); +}; + +// ECDSA +ECPrivateKey.prototype.sign = function(md) { + var keysize = keySizeBytes(this.params), + N = this.params.getN(), + G = this.params.getG(), + e = normalizeEcdsa(this.params, md), + d = bin2bn(this.d); + + var r, s; + var k, x1, z; + do { + do { + // determine random nonce + do { + k = bin2bn(forge.random.getBytes(keysize)); + } while (k.equals(BigInteger.ZERO) || k.compareTo(N) >= 0); + // (x1, y1) = k * G + x1 = G.multiply(k).getX().toBigInteger(); + // r = x1 mod N + r = x1.mod(N); + } while (r.equals(BigInteger.ZERO)); + // s = (k^-1 * (e + r * d)) mod N + z = d.multiply(r); + z = e.add(z); + s = k.modInverse(N).multiply(z).mod(N); + } while (s.equals(BigInteger.ONE)); + + // convert (r, s) to bytes + var len = keySizeBytes(this.params); + r = bn2bin(r, len); + s = bn2bin(s, len); + + return { + r: r, + s: s + }; +}; + +// ECDH +ECPrivateKey.prototype.computeSecret = function(pubkey) { + var d = bin2bn(this.d); + var S = pubkey.point.multiply(d).getX().toBigInteger(); + S = bn2bin(S, keySizeBytes(this.params)); + return S; +}; + +// ### Public API +exports.generateKeyPair = function(curve) { + var params = namedCurve(curve), + n = params.getN(); + + // generate random within range [1, N-1) + var r = forge.random.getBytes(keySizeBytes(params)); + r = bin2bn(r); + + var n1 = n.subtract(BigInteger.ONE); + var d = r.mod(n1).add(BigInteger.ONE); + + var privkey = new ECPrivateKey(curve, d), + pubkey = privkey.toPublicKey(); + + return { + "private": privkey, + "public": pubkey + }; +}; + +exports.asPublicKey = function(curve, x, y) { + if ("string" === typeof x) { + x = hex2bn(x); + } else if (Buffer.isBuffer(x)) { + x = bin2bn(x); + } + + if ("string" === typeof y) { + y = hex2bn(y); + } else if (Buffer.isBuffer(y)) { + y = bin2bn(y); + } + + var pubkey = new ECPublicKey(curve, x, y); + return pubkey; +}; +exports.asPrivateKey = function(curve, d) { + // Elaborate way to get to a Buffer from a (String|Buffer|BigInteger) + if ("string" === typeof d) { + d = hex2bn(d); + } else if (Buffer.isBuffer(d)) { + d = bin2bn(d); + } + + var privkey = new ECPrivateKey(curve, d); + return privkey; +}; diff --git a/lib/deps/ecc/math.js b/lib/deps/ecc/math.js new file mode 100644 index 0000000..65702a2 --- /dev/null +++ b/lib/deps/ecc/math.js @@ -0,0 +1,395 @@ +/** + * deps/ecc/math.js - Elliptic Curve Math + * Original Copyright (c) 2003-2005 Tom Wu. + * Modifications Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + * + * Ported from Tom Wu, which is ported from BouncyCastle + * Modified to reuse existing external NPM modules, restricted to the + * NIST//SECG/X9.62 prime curves only, and formatted to match project + * coding styles. + */ +"use strict"; + +// Basic Javascript Elliptic Curve implementation +// Ported loosely from BouncyCastle's Java EC code +// Only Fp curves implemented for now + +// Requires jsbn.js and jsbn2.js +var jsbn = require("jsbn"); + +var BigInteger = jsbn.BigInteger, + Barrett = BigInteger.prototype.Barrett; + +// ---------------- +// ECFieldElementFp + +// constructor +function ECFieldElementFp(q, x) { + this.x = x; + // TODO if(x.compareTo(q) >= 0) error + this.q = q; +} + +function feFpEquals(other) { + if (other === this) { + return true; + } + return (this.q.equals(other.q) && this.x.equals(other.x)); +} + +function feFpToBigInteger() { + return this.x; +} + +function feFpNegate() { + return new ECFieldElementFp(this.q, this.x.negate().mod(this.q)); +} + +function feFpAdd(b) { + return new ECFieldElementFp(this.q, this.x.add(b.toBigInteger()).mod(this.q)); +} + +function feFpSubtract(b) { + return new ECFieldElementFp(this.q, this.x.subtract(b.toBigInteger()).mod(this.q)); +} + +function feFpMultiply(b) { + return new ECFieldElementFp(this.q, this.x.multiply(b.toBigInteger()).mod(this.q)); +} + +function feFpSquare() { + return new ECFieldElementFp(this.q, this.x.square().mod(this.q)); +} + +function feFpDivide(b) { + return new ECFieldElementFp(this.q, this.x.multiply(b.toBigInteger().modInverse(this.q)).mod(this.q)); +} + +ECFieldElementFp.prototype.equals = feFpEquals; +ECFieldElementFp.prototype.toBigInteger = feFpToBigInteger; +ECFieldElementFp.prototype.negate = feFpNegate; +ECFieldElementFp.prototype.add = feFpAdd; +ECFieldElementFp.prototype.subtract = feFpSubtract; +ECFieldElementFp.prototype.multiply = feFpMultiply; +ECFieldElementFp.prototype.square = feFpSquare; +ECFieldElementFp.prototype.divide = feFpDivide; + +// ---------------- +// ECPointFp + +// constructor +function ECPointFp(curve, x, y, z) { + this.curve = curve; + this.x = x; + this.y = y; + // Projective coordinates: either zinv == null or z * zinv == 1 + // z and zinv are just BigIntegers, not fieldElements + if (!z) { + this.z = BigInteger.ONE; + } else { + this.z = z; + } + this.zinv = null; + //TODO: compression flag +} + +function pointFpGetX() { + if(!this.zinv) { + this.zinv = this.z.modInverse(this.curve.q); + } + var r = this.x.toBigInteger().multiply(this.zinv); + this.curve.reduce(r); + return this.curve.fromBigInteger(r); +} + +function pointFpGetY() { + if(!this.zinv) { + this.zinv = this.z.modInverse(this.curve.q); + } + var r = this.y.toBigInteger().multiply(this.zinv); + this.curve.reduce(r); + return this.curve.fromBigInteger(r); +} + +function pointFpEquals(other) { + if (other === this) { + return true; + } + if (this.isInfinity()) { + return other.isInfinity(); + } + if (other.isInfinity()) { + return this.isInfinity(); + } + var u, v; + // u = Y2 * Z1 - Y1 * Z2 + u = other.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(other.z)).mod(this.curve.q); + if (!u.equals(BigInteger.ZERO)) { + return false; + } + // v = X2 * Z1 - X1 * Z2 + v = other.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(other.z)).mod(this.curve.q); + return v.equals(BigInteger.ZERO); +} + +function pointFpIsInfinity() { + if ((this.x == null) && (this.y == null)) { + return true; + } + return (this.z.equals(BigInteger.ZERO) && !this.y.toBigInteger().equals(BigInteger.ZERO)); +} + +function pointFpNegate() { + return new ECPointFp(this.curve, this.x, this.y.negate(), this.z); +} + +function pointFpAdd(b) { + if (this.isInfinity()) { + return b; + } + if (b.isInfinity()) { + return this; + } + + // u = Y2 * Z1 - Y1 * Z2 + var u = b.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(b.z)).mod(this.curve.q); + // v = X2 * Z1 - X1 * Z2 + var v = b.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(b.z)).mod(this.curve.q); + + if (BigInteger.ZERO.equals(v)) { + if (BigInteger.ZERO.equals(u)) { + return this.twice(); // this == b, so double + } + return this.curve.getInfinity(); // this = -b, so infinity + } + + var THREE = new BigInteger("3"); + var x1 = this.x.toBigInteger(); + var y1 = this.y.toBigInteger(); + + var v2 = v.square(); + var v3 = v2.multiply(v); + var x1v2 = x1.multiply(v2); + var zu2 = u.square().multiply(this.z); + + // x3 = v * (z2 * (z1 * u^2 - 2 * x1 * v^2) - v^3) + var x3 = zu2.subtract(x1v2.shiftLeft(1)).multiply(b.z).subtract(v3).multiply(v).mod(this.curve.q); + // y3 = z2 * (3 * x1 * u * v^2 - y1 * v^3 - z1 * u^3) + u * v^3 + var y3 = x1v2.multiply(THREE).multiply(u).subtract(y1.multiply(v3)).subtract(zu2.multiply(u)).multiply(b.z).add(u.multiply(v3)).mod(this.curve.q); + // z3 = v^3 * z1 * z2 + var z3 = v3.multiply(this.z).multiply(b.z).mod(this.curve.q); + + return new ECPointFp(this.curve, this.curve.fromBigInteger(x3), this.curve.fromBigInteger(y3), z3); +} + +function pointFpTwice() { + if(this.isInfinity()) { + return this; + } + if (this.y.toBigInteger().signum() === 0) { + return this.curve.getInfinity(); + } + + // TODO: optimized handling of constants + var THREE = new BigInteger("3"); + var x1 = this.x.toBigInteger(); + var y1 = this.y.toBigInteger(); + + var y1z1 = y1.multiply(this.z); + var y1sqz1 = y1z1.multiply(y1).mod(this.curve.q); + var a = this.curve.a.toBigInteger(); + + // w = 3 * x1^2 + a * z1^2 + var w = x1.square().multiply(THREE); + if (!BigInteger.ZERO.equals(a)) { + w = w.add(this.z.square().multiply(a)); + } + w = w.mod(this.curve.q); + //this.curve.reduce(w); + // x3 = 2 * y1 * z1 * (w^2 - 8 * x1 * y1^2 * z1) + var x3 = w.square().subtract(x1.shiftLeft(3).multiply(y1sqz1)).shiftLeft(1).multiply(y1z1).mod(this.curve.q); + // y3 = 4 * y1^2 * z1 * (3 * w * x1 - 2 * y1^2 * z1) - w^3 + var y3 = w.multiply(THREE).multiply(x1).subtract(y1sqz1.shiftLeft(1)).shiftLeft(2).multiply(y1sqz1).subtract(w.square().multiply(w)).mod(this.curve.q); + // z3 = 8 * (y1 * z1)^3 + var z3 = y1z1.square().multiply(y1z1).shiftLeft(3).mod(this.curve.q); + + return new ECPointFp(this.curve, this.curve.fromBigInteger(x3), this.curve.fromBigInteger(y3), z3); +} + +// Simple NAF (Non-Adjacent Form) multiplication algorithm +// TODO: modularize the multiplication algorithm +function pointFpMultiply(k) { + if (this.isInfinity()) { + return this; + } + if (k.signum() === 0) { + return this.curve.getInfinity(); + } + + var e = k; + var h = e.multiply(new BigInteger("3")); + + var neg = this.negate(); + var R = this; + + var i; + for(i = h.bitLength() - 2; i > 0; --i) { + R = R.twice(); + + var hBit = h.testBit(i); + var eBit = e.testBit(i); + + if (hBit !== eBit) { + R = R.add(hBit ? this : neg); + } + } + + return R; +} + +// Compute this*j + x*k (simultaneous multiplication) +function pointFpMultiplyTwo(j, x, k) { + var i; + if (j.bitLength() > k.bitLength()) { + i = j.bitLength() - 1; + } else { + i = k.bitLength() - 1; + } + + var R = this.curve.getInfinity(); + var both = this.add(x); + while (i >= 0) { + R = R.twice(); + if (j.testBit(i)) { + if (k.testBit(i)) { + R = R.add(both); + } + else { + R = R.add(this); + } + } + else { + if (k.testBit(i)) { + R = R.add(x); + } + } + --i; + } + + return R; +} + +ECPointFp.prototype.getX = pointFpGetX; +ECPointFp.prototype.getY = pointFpGetY; +ECPointFp.prototype.equals = pointFpEquals; +ECPointFp.prototype.isInfinity = pointFpIsInfinity; +ECPointFp.prototype.negate = pointFpNegate; +ECPointFp.prototype.add = pointFpAdd; +ECPointFp.prototype.twice = pointFpTwice; +ECPointFp.prototype.multiply = pointFpMultiply; +ECPointFp.prototype.multiplyTwo = pointFpMultiplyTwo; + +// ---------------- +// ECCurveFp + +// constructor +function ECCurveFp(q, a, b) { + this.q = q; + this.a = this.fromBigInteger(a); + this.b = this.fromBigInteger(b); + this.infinity = new ECPointFp(this, null, null); + this.reducer = new Barrett(this.q); +} + +function curveFpGetQ() { + return this.q; +} + +function curveFpGetA() { + return this.a; +} + +function curveFpGetB() { + return this.b; +} + +function curveFpEquals(other) { + if (other === this) { + return true; + } + return (this.q.equals(other.q) && this.a.equals(other.a) && this.b.equals(other.b)); +} + +function curveFpGetInfinity() { + return this.infinity; +} + +function curveFpFromBigInteger(x) { + return new ECFieldElementFp(this.q, x); +} + +function curveReduce(x) { + this.reducer.reduce(x); +} + +// for now, work with hex strings because they're easier in JS +function curveFpDecodePointHex(s) { + switch (parseInt(s.substring(0, 2), 16)) { + // first byte + case 0: + return this.infinity; + case 2: + case 3: + // point compression not supported yet + return null; + case 4: + case 6: + case 7: + var len = (s.length - 2) / 2; + var xHex = s.substr(2, len); + var yHex = s.substr(len + 2, len); + + return new ECPointFp(this, + this.fromBigInteger(new BigInteger(xHex, 16)), + this.fromBigInteger(new BigInteger(yHex, 16))); + + default: // unsupported + return null; + } +} + +function curveFpEncodePointHex(p) { + if (p.isInfinity()) { + return "00"; + } + var xHex = p.getX().toBigInteger().toString(16); + var yHex = p.getY().toBigInteger().toString(16); + var oLen = this.getQ().toString(16).length; + if ((oLen % 2) !== 0) { + oLen++; + } + while (xHex.length < oLen) { + xHex = "0" + xHex; + } + while (yHex.length < oLen) { + yHex = "0" + yHex; + } + return "04" + xHex + yHex; +} + +ECCurveFp.prototype.getQ = curveFpGetQ; +ECCurveFp.prototype.getA = curveFpGetA; +ECCurveFp.prototype.getB = curveFpGetB; +ECCurveFp.prototype.equals = curveFpEquals; +ECCurveFp.prototype.getInfinity = curveFpGetInfinity; +ECCurveFp.prototype.fromBigInteger = curveFpFromBigInteger; +ECCurveFp.prototype.reduce = curveReduce; +ECCurveFp.prototype.decodePointHex = curveFpDecodePointHex; +ECCurveFp.prototype.encodePointHex = curveFpEncodePointHex; + +// Exports +module.exports = { + ECFieldElementFp: ECFieldElementFp, + ECPointFp: ECPointFp, + ECCurveFp: ECCurveFp +}; diff --git a/lib/deps/forge.js b/lib/deps/forge.js new file mode 100644 index 0000000..c3b837f --- /dev/null +++ b/lib/deps/forge.js @@ -0,0 +1,102 @@ +/*! + * deps/forge.js - Forge Package Customization + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var forge = { + aes: require("node-forge/js/aes"), + asn1: require("node-forge/js/asn1"), + cipher: require("node-forge/js/cipher"), + hmac: require("node-forge/js/hmac"), + jsbn: require("node-forge/js/jsbn"), + md: require("node-forge/js/md"), + mgf: require("node-forge/js/mgf"), + pem: require("node-forge/js/pem"), + pkcs1: require("node-forge/js/pkcs1"), + pkcs5: require("node-forge/js/pkcs5"), + pkcs7: require("node-forge/js/pkcs7"), + pki: require("node-forge/js/x509"), + prime: require("node-forge/js/prime"), + prng: require("node-forge/js/prng"), + pss: require("node-forge/js/pss"), + random: require("node-forge/js/random"), + util: require("node-forge/js/util") +}; + +// load hash algorithms +require("node-forge/js/sha1"); +require("node-forge/js/sha256"); +require("node-forge/js/sha512"); + +// load symmetric cipherModes +require("node-forge/js/cipherModes"); + +// load AES cipher suites +// TODO: move this to a separate file +require("node-forge/js/aesCipherSuites"); + +// Define AES "raw" cipher mode +function modeRaw(options) { + options = options || {}; + this.name = ""; + this.cipher = options.cipher; + this.blockSize = options.blockSize || 16; + this._blocks = this.blockSize / 4; + this._inBlock = new Array(this._blocks); + this._outBlock = new Array(this._blocks); +} + +modeRaw.prototype.start = function() {}; + +modeRaw.prototype.encrypt = function(input, output) { + var i; + + // get next block + for(i = 0; i < this._blocks; ++i) { + this._inBlock[i] = input.getInt32(); + } + + // encrypt block + this.cipher.encrypt(this._inBlock, this._outBlock); + + // write output + for(i = 0; i < this._blocks; ++i) { + output.putInt32(this._outBlock[i]); + } +}; + +modeRaw.prototype.decrypt = function(input, output) { + var i; + + // get next block + for(i = 0; i < this._blocks; ++i) { + this._inBlock[i] = input.getInt32(); + } + + // decrypt block + this.cipher.decrypt(this._inBlock, this._outBlock); + + // write output + for(i = 0; i < this._blocks; ++i) { + output.putInt32(this._outBlock[i]); + } +}; + +(function() { + var name = "AES", + mode = modeRaw, + factory; + factory = function() { return new forge.aes.Algorithm(name, mode); }; + forge.cipher.registerAlgorithm(name, factory); +})(); + +// Redefine util.setImmediate(cb) to always be util.nextTick(cb) +(function() { + if (forge.util.nextTick !== forge.util.setImmediate) { + forge.util.setImmediate = forge.util.nextTick; + } +})(); + +module.exports = forge; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..0ec3b56 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,18 @@ +/*! + * index.js - Main Entry Point + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +if (typeof Promise === "undefined") { + require("es6-promise").polyfill(); +} + +module.exports = { + JWA: require("./algorithms"), + JWE: require("./jwe"), + JWK: require("./jwk"), + JWS: require("./jws"), + util: require("./util") +}; diff --git a/lib/jwe/decrypt.js b/lib/jwe/decrypt.js new file mode 100644 index 0000000..f7b8ca9 --- /dev/null +++ b/lib/jwe/decrypt.js @@ -0,0 +1,206 @@ +/*! + * jwe/decrypt.js - Decrypt from a JWE + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var assign = require("lodash.assign"), + base64url = require("../util/base64url"), + JWK = require("../jwk"), + zlib = require("zlib"); + +/** + * @class JWE.Decrypter + * @classdesc Processor of encrypted data. + * + * @description + * **NOTE:** This class cannot be instantiated directly. Instead + * call {@link JWE.createDecrypt}. + */ +function JWEDecrypter(ks) { + var assumedKey, + keystore; + + if (JWK.isKey(ks)) { + assumedKey = ks; + keystore = assumedKey.keystore; + } else if (JWK.isKeyStore(ks)) { + keystore = ks; + } else { + throw new TypeError("Keystore must be provided"); + } + + Object.defineProperty(this, "decrypt", { + value: function(input) { + /* eslint camelcase: [0] */ + if (typeof input === "string") { + input = input.split("."); + input = { + protected: input[0], + recipients: [ + { + encrypted_key: input[1] + } + ], + iv: input[2], + ciphertext: input[3], + tag: input[4] + }; + } else if (!input || typeof input !== "object") { + throw new Error("invalid input"); + } + if ("encrypted_key" in input) { + input.recipients = [ + { + encrypted_key: input.encrypted_key + } + ]; + } + + // ensure recipients exists + var rcptList = input.recipients || [{}]; + + //combine fields + var fields; + fields = input.protected ? + JSON.parse(base64url.decode(input.protected, "binary")) : + {}; + fields = assign(input.unprotected || {}, fields); + rcptList = rcptList.map(function(r) { + var promise = Promise.resolve(); + var header = r.header || {}; + header = assign(header, fields); + r.header = header; + if (header.epk) { + promise = promise.then(function() { + return JWK.asKey(header.epk); + }); + promise = promise.then(function(epk) { + header.epk = epk.toObject(false); + }); + } + return promise.then(function() { + return r; + }); + }); + + var promise = Promise.all(rcptList); + + // decrypt with first key found + var algKey, + encKey; + promise = promise.then(function(rcptList) { + var jwe = {}; + return new Promise(function(resolve, reject) { + var processKey = function() { + var rcpt = rcptList.shift(); + if (!rcpt) { + reject(new Error("no key found")); + return; + } + + var algPromise, + prekey; + + prekey = rcpt.encrypted_key || ""; + prekey = base64url.decode(prekey); + algKey = keystore.get({ + use: "enc", + alg: rcpt.header.alg, + kid: rcpt.header.kid + }); + if (algKey) { + algPromise = algKey.unwrap(rcpt.header.alg, prekey, rcpt.header); + } else { + algPromise = Promise.reject(); + } + algPromise.then(function(key) { + encKey = { + "kty": "oct", + "k": base64url.encode(key) + }; + encKey = JWK.asKey(encKey); + jwe.key = algKey; + jwe.header = rcpt.header; + resolve(jwe); + }, processKey); + }; + processKey(); + }); + }); + + // prepare decipher inputs + promise = promise.then(function(jwe) { + jwe.iv = input.iv; + jwe.tag = input.tag; + jwe.ciphertext = base64url.decode(input.ciphertext); + + return jwe; + }); + + // decrypt it! + promise = promise.then(function(jwe) { + var adata = input.protected; + if ("aad" in input && null != input.aad) { + adata += "." + input.aad; + } + var params = { + iv: jwe.iv, + adata: adata, + tag: jwe.tag + }; + var cdata = jwe.ciphertext; + + delete jwe.iv; + delete jwe.tag; + delete jwe.ciphertext; + + return encKey. + then(function(enkKey) { + return enkKey.decrypt(jwe.header.enc, cdata, params). + then(function(pdata) { + jwe.plaintext = pdata; + return jwe; + }); + }); + }); + + // (OPTIONAL) decompress plaintext + if (fields.zip === "DEF") { + promise = promise.then(function(jwe) { + return new Promise(function(resolve, reject) { + zlib.inflateRaw(new Buffer(jwe.plaintext), function(err, data) { + if (err) { + reject(err); + } + else { + jwe.plaintext = data; + resolve(jwe); + } + }); + }); + }); + } + + return promise; + } + }); +} + +/** + * @description + * Creates a new Decrypter for the given Key or KeyStore. + * + * @param {JWK.Key|JWK.KeyStore} ks The Key or KeyStore to use for decryption. + * @returns {JWE.Decrypter} The new Decrypter. + */ +function createDecrypt(ks) { + var dec = new JWEDecrypter(ks); + return dec; +} + +module.exports = { + decrypter: JWEDecrypter, + createDecrypt: createDecrypt +}; diff --git a/lib/jwe/defaults.js b/lib/jwe/defaults.js new file mode 100644 index 0000000..45f2b1d --- /dev/null +++ b/lib/jwe/defaults.js @@ -0,0 +1,38 @@ +/*! + * jwe/defaults.js - Defaults for JWEs + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +/** + * @description + * The default options for {@link JWE.createEncrypt}. + * + * @property {Boolean|String} zip Determines the compression algorithm to + * apply to the plaintext (if any) before it is encrypted. This can + * also be `true` (which is equivalent to `"DEF"`) or **`false`** + * (the default, which is equivalent to no compression). + * @property {String} format Determines the serialization format of the + * output. Expected to be `"general"` for general JSON + * Serialization, `"flattened"` for flattened JSON Serialization, + * or `"compact"` for Compact Serialization (default is + * **`"general"`**). + * @property {Boolean} compact Determines if the output is the Compact + * serialization (`true`) or the JSON serialization (**`false`**, + * the default). + * @property {String} contentAlg The algorithm used to encrypt the plaintext + * (default is **`"A128CBC-HS256"`**). + * @property {String|String[]} protect The names of the headers to integrity + * protect. The value `""` means that none of header parameters + * are integrity protected, while `"*"` (the default) means that all + * headers parameter sare integrity protected. + */ +var JWEDefaults = { + zip: false, + format: "general", + contentAlg: "A128CBC-HS256", + protect: "*" +}; + +module.exports = JWEDefaults; diff --git a/lib/jwe/encrypt.js b/lib/jwe/encrypt.js new file mode 100644 index 0000000..eb6bc28 --- /dev/null +++ b/lib/jwe/encrypt.js @@ -0,0 +1,624 @@ +/*! + * jwe/encrypt.js - Encrypt to a JWE + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var assign = require("lodash.assign"), + clone = require("lodash.clone"), + util = require("../util"), + generateCEK = require("./helpers").generateCEK, + JWK = require("../jwk"), + slice = require("./helpers").slice, + zlib = require("zlib"), + CONSTANTS = require("../algorithms/constants"); + +var DEFAULTS = require("./defaults"); + +/** + * @class JWE.Encrypter + * @classdesc + * Generator of encrypted data. + * + * @description + * **NOTE:** This class cannot be instantiated directly. Instead call {@link + * JWE.createEncrypt}. + */ +function JWEEncrypter(cfg, fields, recipients) { + var finalized = false, + format = cfg.format || "general", + protectAll = !!cfg.protectAll, + content = new Buffer(0); + + /** + * @member {String} JWE.Encrypter#zip + * @readonly + * @description + * Indicates the compression algorithm applied to the plaintext + * before it is encrypted. The possible values are: + * + * + **`"DEF"`**: Compress the plaintext using the DEFLATE algorithm. + * + **`""`**: Do not compress the plaintext. + */ + Object.defineProperty(this, "zip", { + get: function() { + return fields.zip || ""; + }, + enumerable: true + }); + /** + * @member {Boolean} JWE.Encrypter#compact + * @readonly + * @description + * Indicates whether the output of this encryption generator is + * using the Compact serialization (`true`) or the JSON + * serialization (`false`). + */ + Object.defineProperty(this, "compact", { + get: function() { return "compact" === format; }, + enumerable: true + }); + /** + * @member {String} JWE.Encrypter#format + * @readonly + * @description + * Indicates the format the output of this encryption generator takes. + */ + Object.defineProperty(this, "format", { + get: function() { return format; }, + enumerable: true + }); + /** + * @member {String[]} JWE.Encrypter#protected + * @readonly + * @description + * The header parameter names that are protected. Protected header fields + * are first serialized to UTF-8 then encoded as util.base64url, then used as + * the additional authenticated data in the encryption operation. + */ + Object.defineProperty(this, "protected", { + get: function() { + return clone(cfg.protect); + }, + enumerable: true + }); + /** + * @member {Object} JWE.Encrypter#header + * @readonly + * @description + * The global header parameters, both protected and unprotected. Call + * {@link JWE.Encrypter#protected} to determine which parameters will + * be protected. + */ + Object.defineProperty(this, "header", { + get: function() { + return clone(fields); + }, + enumerable: true + }); + + /** + * @method JWE.Encrypter#update + * @description + * Updates the plaintext data for the encryption generator. The plaintext + * is appended to the end of any other plaintext already applied. + * + * If {data} is a Buffer, {encoding} is ignored. Otherwise, {data} is + * converted to a Buffer internally to {encoding}. + * + * @param {Buffer|String} [data] The plaintext to apply. + * @param {String} [encoding] The encoding of the plaintext. + * @returns {JWE.Encrypter} This encryption generator. + * @throws {Error} If ciphertext has already been generated. + */ + Object.defineProperty(this, "update", { + value: function(data, encoding) { + if (finalized) { + throw new Error("already final"); + } + if (data != null) { + data = util.asBuffer(data, encoding); + if (content.length) { + content = Buffer.concat([content, data], + content.length + data.length); + } else { + content = data; + } + } + + return this; + } + }); + /** + * @method JWE.Encrypter#final + * @description + * Finishes the encryption operation. + * + * The returned Promise, when fulfilled, is the JSON Web Encryption (JWE) + * object, either in the Compact (if {@link JWE.Encrypter#compact} is + * `true`) or the JSON serialization. + * + * @param {Buffer|String} [data] The final plaintext data to apply. + * @param {String} [encoding] The encoding of the final plaintext data + * (if any). + * @returns {Promise} A promise for the encryption operation. + * @throws {Error} If ciphertext has already been generated. + */ + Object.defineProperty(this, "final", { + value: function(data, encoding) { + if (finalized) { + throw new Error("already final"); + } + + // last-minute data + this.update(data, encoding); + + // mark as done...ish + finalized = true; + var promise = Promise.resolve({}); + + // determine CEK and IV + var encAlg = fields.enc; + var encKey; + promise = promise.then(function(jwe) { + if (cfg.cek) { + encKey = JWK.asKey(cfg.cek); + } + return jwe; + }); + + // process recipients + promise = promise.then(function(jwe) { + var procR = function(r, one) { + var props = {}; + props = assign(props, fields); + props = assign(props, r.header); + + var algKey = r.key, + algAlg = props.alg; + + // generate Ephemeral EC Key + var tks, + rpromise; + if (props.alg.indexOf("ECDH-ES") === 0) { + tks = algKey.keystore.temp(); + if (r.epk) { + rpromise = Promise.resolve(r.epk). + then(function(epk) { + r.header.epk = epk.toJSON(false, ["kid"]); + props.epk = epk.toObject(true, ["kid"]); + }); + } else { + rpromise = tks.generate("EC", algKey.get("crv")). + then(function(epk) { + r.header.epk = epk.toJSON(false, ["kid"]); + props.epk = epk.toObject(true, ["kid"]); + }); + } + } else { + rpromise = Promise.resolve(); + } + + // encrypt the CEK + rpromise = rpromise.then(function() { + var cek, + p; + // special case 'alg=dir' + if ("dir" === algAlg && one) { + encKey = Promise.resolve(algKey); + p = encKey.then(function(jwk) { + // fixup encAlg + if (!encAlg) { + fields.enc = encAlg = jwk.algorithms(JWK.MODE_ENCRYPT)[0]; + } + return { + once: true, + direct: true + }; + }); + } else { + if (!encKey) { + if (!encAlg) { + fields.enc = encAlg = cfg.contentAlg; + } + encKey = generateCEK(encAlg); + } + p = encKey.then(function(jwk) { + cek = jwk.get("k", true); + // algKey may or may not be a promise + return algKey; + }); + p = p.then(function(algKey) { + return algKey.wrap(algAlg, cek, props); + }); + } + return p; + }); + rpromise = rpromise.then(function(wrapped) { + if (wrapped.once && !one) { + return Promise.reject(new Error("cannot use 'alg':'" + algAlg + "' with multiple recipients")); + } + + var rjwe = {}, + cek; + if (wrapped.data) { + cek = wrapped.data; + cek = util.base64url.encode(cek); + } + + if (wrapped.direct && cek) { + // replace content key + encKey = JWK.asKey({ + kty: "oct", + k: cek + }); + } else if (cek) { + /* eslint camelcase: [0] */ + rjwe.encrypted_key = cek; + } + + if (r.header && Object.keys(r.header).length) { + rjwe.header = clone(r.header || {}); + } + if (wrapped.header) { + rjwe.header = assign(rjwe.header || {}, + wrapped.header); + } + + return rjwe; + }); + return rpromise; + }; + + var p = Promise.all(recipients); + p = p.then(function(rcpts) { + var single = (1 === rcpts.length); + rcpts = rcpts.map(function(r) { + return procR(r, single); + }); + return Promise.all(rcpts); + }); + p = p.then(function(rcpts) { + jwe.recipients = rcpts.filter(function(r) { return !!r; }); + return jwe; + }); + return p; + }); + + // normalize headers + var props = {}; + promise = promise.then(function(jwe) { + var protect, + lenProtect, + unprotect, + lenUnprotect; + + unprotect = clone(fields); + if ((protectAll && jwe.recipients.length === 1) || "compact" === format) { + // merge single recipient into fields + protect = assign(jwe.recipients[0].header || {}, + unprotect); + lenProtect = Object.keys(protect).length; + + unprotect = undefined; + lenUnprotect = 0; + + delete jwe.recipients[0].header; + if (Object.keys(jwe.recipients[0]).length === 0) { + jwe.recipients.splice(0, 1); + } + } else { + protect = {}; + lenProtect = 0; + lenUnprotect = Object.keys(unprotect).length; + cfg.protect.forEach(function(f) { + if (!(f in unprotect)) { + return; + } + protect[f] = unprotect[f]; + lenProtect++; + + delete unprotect[f]; + lenUnprotect--; + }); + } + + if (!jwe.recipients || jwe.recipients.length === 0) { + delete jwe.recipients; + } + + // "serialize" (and setup merged props) + if (unprotect && lenUnprotect > 0) { + props = assign(props, unprotect); + jwe.unprotected = unprotect; + } + if (protect && lenProtect > 0) { + props = assign(props, protect); + protect = JSON.stringify(protect); + jwe.protected = util.base64url.encode(protect, "utf8"); + } + + return jwe; + }); + + // (OPTIONAL) compress plaintext + promise = promise.then(function(jwe) { + var pdata = content; + if (!props.zip) { + jwe.plaintext = pdata; + return jwe; + } else if (props.zip === "DEF") { + return new Promise(function(resolve, reject) { + zlib.deflateRaw(new Buffer(pdata, "binary"), function(err, data) { + if (err) { + reject(err); + } + else { + jwe.plaintext = data; + resolve(jwe); + } + }); + }); + } + return Promise.reject(new Error("unsupported 'zip' mode")); + }); + + // encrypt plaintext + promise = promise.then(function(jwe) { + props.adata = jwe.protected; + if ("aad" in cfg && cfg.aad != null) { + props.adata += "." + cfg.aad; + props.adata = new Buffer(props.adata, "utf8"); + } + // calculate IV + var iv = cfg.iv || + util.randomBytes(CONSTANTS.NONCELENGTH[encAlg] / 8); + if ("string" === typeof iv) { + iv = util.base64url.decode(iv); + } + props.iv = iv; + + var pdata = jwe.plaintext; + delete jwe.plaintext; + return encKey.then(function(encKey) { + var p = encKey.encrypt(encAlg, pdata, props); + p = p.then(function(result) { + jwe.iv = util.base64url.encode(iv, "binary"); + if ("aad" in cfg && cfg.aad != null) { + jwe.aad = cfg.aad; + } + jwe.ciphertext = util.base64url.encode(result.data, "binary"); + jwe.tag = util.base64url.encode(result.tag, "binary"); + return jwe; + }); + return p; + }); + }); + + // (OPTIONAL) compact/flattened results + switch (format) { + case "compact": + promise = promise.then(function(jwe) { + var compact = new Array(5); + + compact[0] = jwe.protected; + if (jwe.recipients && jwe.recipients[0]) { + compact[1] = jwe.recipients[0].encrypted_key; + } + + compact[2] = jwe.iv; + compact[3] = jwe.ciphertext; + compact[4] = jwe.tag; + compact = compact.join("."); + + return compact; + }); + break; + case "flattened": + promise = promise.then(function(jwe) { + var flattened = {}, + rcpt = jwe.recipients && jwe.recipients[0]; + + if (jwe.protected) { + flattened.protected = jwe.protected; + } + if (jwe.unprotected) { + flattened.unprotected = jwe.unprotected; + } + ["header", "encrypted_key"].forEach(function(f) { + if (!rcpt) { return; } + if (!(f in rcpt)) { return; } + flattened[f] = rcpt[f]; + }); + if (jwe.aad) { + flattened.aad = jwe.aad; + } + flattened.iv = jwe.iv; + flattened.ciphertext = jwe.ciphertext; + flattened.tag = jwe.tag; + + return flattened; + }); + break; + } + + return promise; + } + }); +} + +function createEncrypt(opts, rcpts) { + // fixup recipients + var options = opts, + rcptStart = 1, + rcptList = rcpts; + + if (arguments.length === 0) { + throw new Error("at least one recipient must be provided"); + } + if (arguments.length === 1) { + // assume opts is the recipient list + rcptList = opts; + rcptStart = 0; + options = {}; + } else if (JWK.isKey(opts) || + (opts && "kty" in opts) || + (opts && "key" in opts && + (JWK.isKey(opts.key) || "kty" in opts.key))) { + rcptList = opts; + rcptStart = 0; + options = {}; + } else { + options = clone(opts); + } + if (!Array.isArray(rcptList)) { + rcptList = slice(arguments, rcptStart); + } + + // fixup options + options = assign(clone(DEFAULTS), options); + + // setup header fields + var fields = clone(options.fields || {}); + if (options.zip) { + fields.zip = (typeof options.zip === "boolean") ? + (options.zip ? "DEF" : false) : + options.zip; + } + options.format = (options.compact ? "compact" : options.format) || "general"; + switch (options.format) { + case "compact": + if ("aad" in opts) { + throw new Error("additional authenticated data cannot be used for compact serialization"); + } + /* eslint no-fallthrough: [0] */ + case "flattened": + if (rcptList.length > 1) { + throw new Error("too many recipients for compact serialization"); + } + break; + } + + // note protected fields (globally) + // protected fields are global only + var protectAll = false; + if ("compact" === options.format || "*" === options.protect) { + protectAll = true; + options.protect = Object.keys(fields).concat("enc"); + } else if (typeof options.protect === "string") { + options.protect = [options.protect]; + } else if (Array.isArray(options.protect)) { + options.protect = options.protect.concat(); + } else if (!options.protect) { + options.protect = []; + } else { + throw new Error("protect must be a list of fields"); + } + + if (protectAll && 1 < rcptList.length) { + throw new Error("too many recipients to protect all header parameters"); + } + + rcptList = rcptList.map(function(r, idx) { + var p; + + // resolve a key + if (r && "kty" in r) { + p = JWK.asKey(r); + p = p.then(function(k) { + return { + key: k + }; + }); + } else if (r) { + p = JWK.asKey(r.key); + p = p.then(function(k) { + return { + header: r.header, + reference: r.reference, + key: k + }; + }); + } else { + p = Promise.reject(new Error("missing key for recipient " + idx)); + } + + // convert ephemeral key (if present) + if (r.epk) { + p = p.then(function(recipient) { + return JWK.asKey(r.epk). + then(function(epk) { + recipient.epk = epk; + return recipient; + }); + }); + } + + // resolve the complete recipient + p = p.then(function(recipient) { + var key = recipient.key; + + // prepare the recipient header + var header = recipient.header || {}; + recipient.header = header; + var props = {}; + props = assign(props, fields); + props = assign(props, recipient.header); + + // ensure key protection algorithm is set + if (!props.alg) { + props.alg = key.algorithms(JWK.MODE_WRAP)[0]; + } + header.alg = props.alg; + + // determine the key reference + var ref = recipient.reference; + delete recipient.reference; + if (undefined === ref) { + // header already contains the key reference + ref = ["kid", "jku", "x5c", "x5t", "x5u"].some(function(k) { + return (k in header); + }); + ref = !ref ? "kid" : null; + } else if ("boolean" === typeof ref) { + // explicit (positive | negative) request for key reference + ref = ref ? "kid" : null; + } + var jwk; + if (ref) { + jwk = key.toJSON(); + if ("jwk" === ref) { + header.jwk = jwk; + } else if (ref in jwk) { + header[ref] = jwk[ref]; + } + } + + // freeze recipient + recipient = Object.freeze(recipient); + return recipient; + }); + + return p; + }); + + // create and configure encryption + var cfg = { + aad: ("aad" in options) ? util.base64url.encode(options.aad || "") : null, + contentAlg: options.contentAlg, + format: options.format, + protect: options.protect, + cek: options.cek, + iv: options.iv, + protectAll: protectAll + }; + var enc = new JWEEncrypter(cfg, fields, rcptList); + + return enc; +} + +module.exports = { + encrypter: JWEEncrypter, + createEncrypt: createEncrypt +}; diff --git a/lib/jwe/helpers.js b/lib/jwe/helpers.js new file mode 100644 index 0000000..b26beff --- /dev/null +++ b/lib/jwe/helpers.js @@ -0,0 +1,25 @@ +/*! + * jwe/helpers.js - JWE Internal Helper Functions + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var CONSTANTS = require("../algorithms/constants"), + JWK = require("../jwk"); + +module.exports = { + slice: function(input, start) { + return Array.prototype.slice.call(input, start || 0); + }, + generateCEK: function(enc) { + var ks = JWK.createKeyStore(); + var len = CONSTANTS.KEYLENGTH[enc]; + + if (len) { + return ks.generate("oct", len); + } + + throw new Error("unsupported encryption algorithm"); + } +}; diff --git a/lib/jwe/index.js b/lib/jwe/index.js new file mode 100644 index 0000000..78fed4d --- /dev/null +++ b/lib/jwe/index.js @@ -0,0 +1,13 @@ +/*! + * jwe/index.js - JSON Web Encryption (JWE) Entry Point + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var JWE = { + createEncrypt: require("./encrypt").createEncrypt, + createDecrypt: require("./decrypt").createDecrypt +}; + +module.exports = JWE; diff --git a/lib/jwk/basekey.js b/lib/jwk/basekey.js new file mode 100644 index 0000000..205f82d --- /dev/null +++ b/lib/jwk/basekey.js @@ -0,0 +1,630 @@ +/*! + * jwk/basekey.js - JWK Key Base Class Implementation + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var clone = require("lodash.clone"), + flatten = require("lodash.flatten"), + intersection = require("lodash.intersection"), + merge = require("../util/merge"), + omit = require("lodash.omit"), + pick = require("lodash.pick"), + uniq = require("lodash.uniq"), + uuid = require("uuid"); + +var ALGORITHMS = require("../algorithms"), + CONSTANTS = require("./constants.js"), + HELPERS = require("./helpers.js"); + +/** + * @class JWK.Key + * @classdesc + * Represents a JSON Web Key instance. + * + * @description + * **NOTE:** This class cannot be instantiated directly. Instead call + * {@link JWK.asKey}, {@link JWK.KeyStore#add}, or + * {@link JWK.KeyStore#generate}. + */ +var JWKBaseKeyObject = function(kty, ks, props, cfg) { + // ### validate/coerce arguments ### + if (!kty) { + throw new Error("kty cannot be null"); + } + + if (!ks) { + throw new Error("keystore cannot be null"); + } + + if (!props) { + throw new Error("props cannot be null"); + } else if ("string" === typeof props) { + props = JSON.parse(props); + } + + if (!cfg) { + throw new Error("cfg cannot be null"); + } + + var excluded = []; + var keys = {}, + json = {}; + + // force certain values + props = clone(props); + props.kty = kty; + props.kid = props.kid || uuid(); + + // setup base info + var included = Object.keys(HELPERS.COMMON_PROPS).map(function(p) { + return HELPERS.COMMON_PROPS[p].name; + }); + json.base = pick(props, included); + excluded = excluded.concat(Object.keys(json.base)); + + // setup public information + json.public = clone(props); + keys.public = cfg.publicKey(json.public); + if (keys.public) { + // exclude public values from extra + excluded = excluded.concat(Object.keys(json.public)); + } + + // setup private information + json.private = clone(props); + keys.private = cfg.privateKey(json.private); + if (keys.private) { + // exclude private values from extra + excluded = excluded.concat(Object.keys(json.private)); + } + + // setup extra information + json.extra = omit(props, excluded); + + // TODO: validate 'alg' against supported algorithms + + // setup calculated values + var keyLen; + if (keys.public && ("length" in keys.public)) { + keyLen = keys.public.length; + } else if (keys.private && ("length" in keys.private)) { + keyLen = keys.private.length; + } else { + keyLen = NaN; + } + + // ### Public Properties ### + /** + * @member {JWK.KeyStore} JWK.Key#keystore + * @description + * The owning keystore. + */ + Object.defineProperty(this, "keystore", { + value: ks, + enumerable: true + }); + /** + * @member {Number} JWK.Key#length + * @description + * The size of this Key, in bits. + */ + Object.defineProperty(this, "length", { + value: keyLen, + enumerable: true + }); + /** + * @member {String} JWK.Key#kty + * @description + * The type of Key. + */ + Object.defineProperty(this, "kty", { + value: kty, + enumerable: true + }); + + /** + * @member {String} JWK.Key#kid + * @description + * The identifier for this Key. + */ + Object.defineProperty(this, "kid", { + value: json.base.kid, + enumerable: true + }); + /** + * @member {String} JWK.Key#use + * @description + * The usage for this Key. + */ + Object.defineProperty(this, "use", { + value: json.base.use || "", + enumerable: true + }); + /** + * @member {String} JWK.Key#alg + * @description + * The sole algorithm this key can be used for. + */ + Object.defineProperty(this, "alg", { + value: json.base.alg || "", + enumerable: true + }); + + // ### Public Methods ### + /** + * @method JWK.Key#algorithms + * @description + * The possible algorithms this Key can be used for. The returned + * list is not any particular order, but is filtered based on the + * Key's intended usage. + * + * @param {String} mode The operation mode + * @returns {String[]} The list of supported algorithms + * @see JWK.Key#supports + */ + Object.defineProperty(this, "algorithms", { + value: function(mode) { + var modes = []; + if (!this.use || this.use === "sig") { + if (!mode || CONSTANTS.MODE_SIGN === mode) { + modes.push(CONSTANTS.MODE_SIGN); + } + if (!mode || CONSTANTS.MODE_VERIFY === mode) { + modes.push(CONSTANTS.MODE_VERIFY); + } + } + if (!this.use || this.use === "enc") { + if (!mode || CONSTANTS.MODE_ENCRYPT === mode) { + modes.push(CONSTANTS.MODE_ENCRYPT); + } + if (!mode || CONSTANTS.MODE_DECRYPT === mode) { + modes.push(CONSTANTS.MODE_DECRYPT); + } + if (!mode || CONSTANTS.MODE_WRAP === mode) { + modes.push(CONSTANTS.MODE_WRAP); + } + if (!mode || CONSTANTS.MODE_UNWRAP === mode) { + modes.push(CONSTANTS.MODE_UNWRAP); + } + } + + var self = this; + var algs = modes.map(function(m) { + return cfg.algorithms.call(self, keys, m); + }); + algs = flatten(algs); + algs = uniq(algs); + if (this.alg) { + // TODO: fix this correctly + var valid; + if ("oct" === kty) { + valid = [this.alg, "dir"]; + } else { + valid = [this.alg]; + } + algs = intersection(algs, valid); + } + + return algs; + } + }); + /** + * @method JWK.Key#supports + * @description + * Determines if the given algorithm is supported. + * + * @param {String} alg The algorithm in question + * @param {String} [mode] The operation mode + * @returns {Boolean} `true` if {alg} is supported, and `false` otherwise. + * @see JWK.Key#algorithms + */ + Object.defineProperty(this, "supports", { + value: function(alg, mode) { + return (this.algorithms(mode).indexOf(alg) !== -1); + } + }); + /** + * @method JWK.Key#has + * @description + * Determines if this Key contains the given parameter. + * + * @param {String} name The name of the parameter + * @param {Boolean} [isPrivate=false] `true` if private parameters should be + * checked. + * @returns {Boolean} `true` if the given parameter is present; `false` + * otherwise. + */ + Object.defineProperty(this, "has", { + value: function(name, isPrivate) { + var contains = false; + contains = contains || !!(json.base && + (name in json.base)); + contains = contains || !!(keys.public && + (name in keys.public)); + contains = contains || !!(json.extra && + (name in json.extra)); + contains = contains || !!(isPrivate && + keys.private && + (name in keys.private)); + // TODO: check for export restrictions + + return contains; + } + }); + /** + * @method JWK.Key#get + * @description + * Retrieves the value of the given parameter. The value returned by this + * method is in its natural format, which might not exactly match its + * JSON encoding (e.g., a binary string rather than a base64url-encoded + * string). + * + * **NOTE:** This method can return `false`. Call + * {@link JWK.Key#has} to determine if the parameter is present. + * + * @param {String} name The name of the parameter + * @param {Boolean} [isPrivate=false] `true` if private parameters should + * be checked. + * @returns {any} The value of the named parameter, or undefined if + * it is not present. + */ + Object.defineProperty(this, "get", { + value: function(name, isPrivate) { + var src; + if (json.base && (name in json.base)) { + src = json.base; + } else if (keys.public && (name in keys.public)) { + src = keys.public; + } else if (json.extra && (name in json.extra)) { + src = json.extra; + } else if (isPrivate && keys.private && (name in keys.private)) { + // TODO: check for export restrictions + src = keys.private; + } + + return src && src[name] || null; + } + }); + /** + * @method JWK.Key#toJSON + * @description + * Returns the JSON representation of this Key. All properties of the + * returned JSON object are properly encoded (e.g., base64url encoding for + * any binary strings). + * + * @param {Boolean} [isPrivate=false] `true` if private parameters should be + * included. + * @param {String[]} [excluded] The list of parameters to exclude from + * the returned JSON. + * @returns {Object} The plain JSON object + */ + Object.defineProperty(this, "toJSON", { + value: function(isPrivate, excluded) { + // coerce arguments + if (Array.isArray(isPrivate)) { + excluded = isPrivate; + isPrivate = false; + } + var result = {}; + + // TODO: check for export restrictions + result = merge(result, + json.base, + json.public, + (isPrivate) ? json.private : {}, + json.extra); + result = omit(result, excluded || []); + + return result; + } + }); + + /** + * @method JWK.Key#toObject + * @description + * Returns the plain object representing this Key. All properties of the + * returned object are in their natural encoding (e.g., binary strings + * instead of base64url encoded). + * + * @param {Boolean} [isPrivate=false] `true` if private parameters should be + * included. + * @param {String[]} [excluded] The list of parameters to exclude from + * the returned object. + * @returns {Object} The plain Object. + */ + Object.defineProperty(this, "toObject", { + value: function(isPrivate, excluded) { + // coerce arguments + if (Array.isArray(isPrivate)) { + excluded = isPrivate; + isPrivate = false; + } + var result = {}; + + // TODO: check for export restrictions + result = merge(result, + json.base, + keys.public, + (isPrivate) ? keys.private : {}, + json.extra); + result = omit(result, (excluded || []).concat("length")); + + return result; + } + }); + + /** + * @method JWK.Key#sign + * @description + * Sign the given data using the specified algorithm. + * + * **NOTE:** This is the primitive signing operation; the output is + * _**NOT**_ a JSON Web Signature (JWS) object. + * + * The Promise, when fulfilled, returns an Object with the following + * properties: + * + * + **data**: The data that was signed (and should be equal to {data}). + * + **mac**: The signature or message authentication code (MAC). + * + * @param {String} alg The signing algorithm + * @param {String|Buffer} data The data to sign + * @param {Object} [props] Additional properties for the signing + * algorithm. + * @returns {Promise} The promise for the signing operation. + * @throws {Error} If {alg} is not appropriate for this Key; or if + * this Key does not contain the appropriate parameters. + */ + Object.defineProperty(this, "sign", { + value: function(alg, data, props) { + // validate appropriateness + if (this.algorithms("sign").indexOf(alg) === -1) { + return Promise.reject(new Error("unsupported algorithm")); + } + var k = cfg.signKey.call(this, alg, keys); + if (!k) { + return Promise.reject(new Error("improper key")); + } + + // prepare properties (if any) + props = (props) ? + clone(props) : + {}; + if (cfg.signProps) { + props = merge(props, cfg.signProps.call(this, alg, props)); + } + return ALGORITHMS.sign(alg, k, data, props); + } + }); + /** + * @method JWK.Key#verify + * @description + * Verify the given data and signature using the specified algorithm. + * + * **NOTE:** This is the primitive verification operation; the input is + * _**NOT**_ a JSON Web Signature.

+ * + * The Promise, when fulfilled, returns an Object with the following + * properties: + * + * + **data**: The data that was verified (and should be equal to + * {data}). + * + **mac**: The signature or MAC that was verified (and should be equal + * to {mac}). + * + **valid**: `true` if {mac} is valid for {data}. + * + * @param {String} alg The verification algorithm + * @param {String|Buffer} data The data to verify + * @param {String|Buffer} mac The signature or MAC to verify + * @param {Object} [props] Additional properties for the verification + * algorithm. + * @returns {Promise} The promise for the verification operation. + * @throws {Error} If {alg} is not appropriate for this Key; or if + * the Key does not contain the appropriate properties. + */ + Object.defineProperty(this, "verify", { + value: function(alg, data, mac, props) { + // validate appropriateness + if (this.algorithms("verify").indexOf(alg) === -1) { + return Promise.reject(new Error("unsupported algorithm")); + } + var k = cfg.verifyKey.call(this, alg, keys); + if (!k) { + return Promise.reject(new Error("improper key")); + } + + // prepare properties (if any) + props = (props) ? + clone(props) : + {}; + if (cfg.verifyProps) { + props = merge(props, cfg.verifyProps.call(this, alg, props)); + } + return ALGORITHMS.verify(alg, k, data, mac, props); + } + }); + + /** + * @method JWK.Key#encrypt + * @description + * Encrypts the given data using the specified algorithm. + * + * **NOTE:** This is the primitive encryption operation; the output is + * _**NOT**_ a JSON Web Encryption (JWE) object. + * + * **NOTE:** This operation is treated as distinct from {@link + * JWK.Key#wrap}, as different algorithms and properties are often + * used for wrapping a key versues encrypting arbitrary data. + * + * The Promise, when fulfilled, returns an object with the following + * properties: + * + * + **data**: The ciphertext data + * + **mac**: The associated message authentication code (MAC). + * + * @param {String} alg The encryption algorithm + * @param {Buffer|String} data The data to encrypt + * @param {Object} [props] Additional properties for the encryption + * algorithm. + * @returns {Promise} The promise for the encryption operation. + * @throws {Error} If {alg} is not appropriate for this Key; or if + * this Key does not contain the appropriate parameters. + */ + Object.defineProperty(this, "encrypt", { + value: function(alg, data, props) { + // validate appropriateness + if (this.algorithms("encrypt").indexOf(alg) === -1) { + return Promise.reject(new Error("unsupported algorithm")); + } + var k = cfg.encryptKey.call(this, alg, keys); + if (!k) { + return Promise.reject(new Error("improper key")); + } + + // prepare properties (if any) + props = (props) ? + clone(props) : + {}; + if (cfg.encryptProps) { + props = merge(props, cfg.encryptProps.call(this, alg, props)); + } + return ALGORITHMS.encrypt(alg, k, data, props); + } + }); + /** + * @method JWK.Key#decrypt + * @description + * Decrypts the given data using the specified algorithm. + * + * **NOTE:** This is the primitive decryption operation; the input is + * _**NOT**_ a JSON Web Encryption (JWE) object. + * + * **NOTE:** This operation is treated as distinct from {@link + * JWK.Key#unwrap}, as different algorithms and properties are often used + * for unwrapping a key versues decrypting arbitrary data. + * + * The Promise, when fulfilled, returns the plaintext data. + * + * @param {String} alg The decryption algorithm. + * @param {Buffer|String} data The data to decypt. + * @param {Object} [props] Additional data for the decryption operation. + * @returns {Promise} The promise for the decryption operation. + * @throws {Error} If {alg} is not appropriate for this Key; or if + * the Key does not contain the appropriate properties. + */ + Object.defineProperty(this, "decrypt", { + value: function(alg, data, props) { + // validate appropriateness + if (this.algorithms("decrypt").indexOf(alg) === -1) { + return Promise.reject(new Error("unsupported algorithm")); + } + var k = cfg.decryptKey.call(this, alg, keys); + if (!k) { + return Promise.reject(new Error("improper key")); + } + + // prepare properties (if any) + props = (props) ? + clone(props) : + {}; + if (cfg.decryptProps) { + props = merge(props, cfg.decryptProps.call(this, alg, props)); + } + return ALGORITHMS.decrypt(alg, k, data, props); + } + }); + + /** + * @method JWK.Key#wrap + * @description + * Wraps the given key using the specified algorithm. + * + * **NOTE:** This is the primitive encryption operation; the output is + * _**NOT**_ a JSON Web Encryption (JWE) object. + * + * **NOTE:** This operation is treated as distinct from {@link + * JWK.Key#encrypt}, as different algorithms and properties are + * often used for wrapping a key versues encrypting arbitrary data. + * + * The Promise, when fulfilled, returns an object with the following + * properties: + * + * + **data**: The ciphertext data + * + **headers**: The additional header parameters to apply to a JWE. + * + * @param {String} alg The encryption algorithm + * @param {Buffer|String} data The data to encrypt + * @param {Object} [props] Additional properties for the encryption + * algorithm. + * @returns {Promise} The promise for the encryption operation. + * @throws {Error} If {alg} is not appropriate for this Key; or if + * this Key does not contain the appropriate parameters. + */ + Object.defineProperty(this, "wrap", { + value: function(alg, data, props) { + // validate appropriateness + if (this.algorithms("wrap").indexOf(alg) === -1) { + return Promise.reject(new Error("unsupported algorithm")); + } + var k = cfg.wrapKey.call(this, alg, keys); + if (!k) { + return Promise.reject(new Error("improper key")); + } + + // prepare properties (if any) + props = (props) ? + clone(props) : + {}; + if (cfg.wrapProps) { + props = merge(props, cfg.wrapProps.call(this, alg, props)); + } + return ALGORITHMS.encrypt(alg, k, data, props); + } + }); + /** + * @method JWK.Key#unwrap + * @description + * Unwraps the given key using the specified algorithm. + * + * **NOTE:** This is the primitive unwrap operation; the input is + * _**NOT**_ a JSON Web Encryption (JWE) object. + * + * **NOTE:** This operation is treated as distinct from {@link + * JWK.Key#decrypt}, as different algorithms and properties are often used + * for unwrapping a key versues decrypting arbitrary data. + * + * The Promise, when fulfilled, returns the unwrapped key. + * + * @param {String} alg The unwrap algorithm. + * @param {Buffer|String} data The data to unwrap. + * @param {Object} [props] Additional data for the unwrap operation. + * @returns {Promise} The promise for the unwrap operation. + * @throws {Error} If {alg} is not appropriate for this Key; or if + * the Key does not contain the appropriate properties. + */ + Object.defineProperty(this, "unwrap", { + value: function(alg, data, props) { + // validate appropriateness + if (this.algorithms("unwrap").indexOf(alg) === -1) { + return Promise.reject(new Error("unsupported algorithm")); + } + var k = cfg.unwrapKey.call(this, alg, keys); + if (!k) { + return Promise.reject(new Error("improper key")); + } + + // prepare properties (if any) + props = (props) ? + clone(props) : + {}; + if (cfg.unwrapProps) { + props = merge(props, cfg.unwrapProps.call(this, alg, props)); + } + return ALGORITHMS.decrypt(alg, k, data, props); + } + }); +}; + +module.exports = JWKBaseKeyObject; diff --git a/lib/jwk/constants.js b/lib/jwk/constants.js new file mode 100644 index 0000000..f140380 --- /dev/null +++ b/lib/jwk/constants.js @@ -0,0 +1,15 @@ +/*! + * jwk/constants.js - Constants for JWKs + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +module.exports = { + MODE_SIGN: "sign", + MODE_VERIFY: "verify", + MODE_ENCRYPT: "encrypt", + MODE_DECRYPT: "decrypt", + MODE_WRAP: "wrap", + MODE_UNWRAP: "unwrap" +}; diff --git a/lib/jwk/eckey.js b/lib/jwk/eckey.js new file mode 100644 index 0000000..5a0aee2 --- /dev/null +++ b/lib/jwk/eckey.js @@ -0,0 +1,143 @@ +/*! + * jwk/rsa.js - RSA Key Representation + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var ecutil = require("../algorithms/ec-util.js"), + depsecc = require("../deps/ecc"); + +var JWK = { + BaseKey: require("./basekey.js"), + helpers: require("./helpers.js") +}; + +var SIG_ALGS = [ + "ES256", + "ES384", + "ES512" +]; +var WRAP_ALGS = [ + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW" +]; + +var JWKEcCfg = { + publicKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + {name: "crv", type: "string"}, + {name: "x", type: "binary"}, + {name: "y", type: "binary"} + ]); + var pk = JWK.helpers.unpackProps(props, fields); + if (pk && pk.crv && pk.x && pk.y) { + pk.length = ecutil.curveSize(pk.crv); + } else { + delete pk.crv; + delete pk.x; + delete pk.y; + } + + return pk; + }, + privateKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + {name: "crv", type: "string"}, + {name: "x", type: "binary"}, + {name: "y", type: "binary"}, + {name: "d", type: "binary"} + ]); + var pk = JWK.helpers.unpackProps(props, fields); + if (pk && pk.crv && pk.x && pk.y && pk.d) { + pk.length = ecutil.curveSize(pk.crv); + } else { + pk = undefined; + } + + return pk; + }, + algorithms: function(keys, mode) { + var len = (keys.public && keys.public.length) || + (keys.private && keys.private.length) || + 0; + // NOTE: 521 is the actual, but 512 is the expected + if (len === 521) { + len = 512; + } + + switch (mode) { + case "encrypt": + case "decrypt": + return []; + case "wrap": + return (keys.public && WRAP_ALGS) || []; + case "unwrap": + return (keys.private && WRAP_ALGS) || []; + case "sign": + if (!keys.private) { + return []; + } + return SIG_ALGS.filter(function(a) { + return (a === ("ES" + len)); + }); + case "verify": + if (!keys.public) { + return []; + } + return SIG_ALGS.filter(function(a) { + return (a === ("ES" + len)); + }); + } + }, + + encryptKey: function(alg, keys) { + return keys.public; + }, + decryptKey: function(alg, keys) { + return keys.private; + }, + + wrapKey: function(alg, keys) { + return keys.public; + }, + unwrapKey: function(alg, keys) { + return keys.private; + }, + + signKey: function(alg, keys) { + return keys.private; + }, + verifyKey: function(alg, keys) { + return keys.public; + } +}; + +var JWKEcFactory = { + kty: "EC", + prepare: function() { + return Promise.resolve(JWKEcCfg); + }, + generate: function(size) { + var keypair = depsecc.generateKeyPair(size); + var result = { + "crv": size, + "x": keypair.public.x, + "y": keypair.public.y, + "d": keypair.private.d + }; + return Promise.resolve(result); + } +}; +// public API +module.exports = Object.freeze({ + config: JWKEcCfg, + factory: JWKEcFactory +}); + +// registration +(function(REGISTRY) { + REGISTRY.register(JWKEcFactory); +})(require("./keystore").registry); diff --git a/lib/jwk/helpers.js b/lib/jwk/helpers.js new file mode 100644 index 0000000..d3f7f8e --- /dev/null +++ b/lib/jwk/helpers.js @@ -0,0 +1,70 @@ +/*! + * jwk/helpers.js - JWK Internal Helper Functions and Constants + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var clone = require("lodash.clone"), + util = require("../util"); + +module.exports = { + unpackProps: function(props, allowed) { + var output; + + // apply all of the existing values + allowed.forEach(function(cfg) { + if (!(cfg.name in props)) { + return; + } + output = output || {}; + var value = props[cfg.name]; + switch (cfg.type) { + case "binary": + if (Buffer.isBuffer(value)) { + value = value; + props[cfg.name] = util.base64url.encode(value); + } else { + value = util.base64url.decode(value); + } + break; + case "string": + case "number": + case "boolean": + value = value; + break; + case "array": + value = [].concat(value); + break; + case "object": + value = clone(value); + break; + default: + // TODO: deep clone? + value = value; + break; + } + output[cfg.name] = value; + }); + + // remove any from json that didn't apply + var check = output || {}; + Object.keys(props). + forEach(function(n) { + if (n in check) { return; } + delete props[n]; + }); + + return output; + }, + COMMON_PROPS: [ + {name: "kty", type: "string"}, + {name: "kid", type: "string"}, + {name: "use", type: "string"}, + {name: "alg", type: "string"}, + {name: "x5c", type: "array"}, + {name: "x5t", type: "binary"}, + {name: "x5u", type: "string"}, + {name: "key_ops", type: "array"} + ] +}; diff --git a/lib/jwk/index.js b/lib/jwk/index.js new file mode 100644 index 0000000..ff45202 --- /dev/null +++ b/lib/jwk/index.js @@ -0,0 +1,24 @@ +/*! + * jwk/index.js - JSON Web Key (JWK) Entry Point + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var JWKStore = require("./keystore.js"); + +// Public API -- Key and KeyStore methods +Object.keys(JWKStore.KeyStore).forEach(function(name) { + exports[name] = JWKStore.KeyStore[name]; +}); + +// Public API -- constants +var CONSTANTS = require("./constants.js"); +Object.keys(CONSTANTS).forEach(function(name) { + exports[name] = CONSTANTS[name]; +}); + +// Registered Key Types +require("./octkey.js"); +require("./rsakey.js"); +require("./eckey.js"); diff --git a/lib/jwk/keystore.js b/lib/jwk/keystore.js new file mode 100644 index 0000000..7645825 --- /dev/null +++ b/lib/jwk/keystore.js @@ -0,0 +1,481 @@ +/*! + * jwk/keystore.js - JWK KeyStore Implementation + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var clone = require("lodash.clone"), + merge = require("../util/merge"); + +var JWK = { + BaseKey: require("./basekey.js") +}; + +/** + * @class JWK.KeyStoreRegistry + * @classdesc + * A registry of JWK.Key types that can be used. + * + * @description + * **NOTE:** This constructor cannot be called directly. Instead use the + * global {JWK.registry} + */ +var JWKRegistry = function() { + var types = {}; + + Object.defineProperty(this, "register", { + value: function(factory) { + if (!factory || "string" !== typeof factory.kty || !factory.kty) { + throw new Error("invalid Key factory"); + } + + var kty = factory.kty; + types[kty] = factory; + return this; + } + }); + Object.defineProperty(this, "unregister", { + value: function(factory) { + if (!factory || "string" !== typeof factory.kty || !factory.kty) { + throw new Error("invalid Key factory"); + } + + var kty = factory.kty; + if (factory === types[kty]) { + delete types[kty]; + } + return this; + } + }); + + Object.defineProperty(this, "get", { + value: function(kty) { + return types[kty || ""] || undefined; + } + }); +}; + +// Globals +var GLOBAL_REGISTRY = new JWKRegistry(); + +/** + * @class JWK.KeyStore + * @classdesc + * Represents a collection of Keys. + * + * @description + * **NOTE:** This constructor cannot be called directly. Instead call {@link + * JWK.createKeyStore}. + */ +var JWKStore = function(registry, parent) { + var keysets = {}; + + /** + * @method JWK.KeyStore#generate + * @description + * Generates a new random Key into this KeyStore. + * + * The type of {size} depends on the value of {kty}: + * + * + **`EC`**: String naming the curve to use, which can be one of: + * `"P-256"`, `"P-384"`, or `"P-521"` (default is **`"P-256"`**). + * + **`RSA`**: Number describing the size of the key, in bits (default is + * **`2048`**). + * + **`oct`**: Number describing the size of the key, in bits (default is + * **`256`**). + * + * Any properties in {props} are applied before the key is generated, + * and are expected to be data types acceptable in JSON. This allows the + * generated key to have a specific key identifier, or to specify its + * acceptable usage. + * + * The returned Promise, when fulfilled, returns the generated Key. + * + * @param {String} kty The type of generated key + * @param {String|Number} [size] The size of the generated key + * @param {Object} [props] Additional properties to apply to the generated + * key. + * @returns {Promise} The promise for the generated Key + * @throws {Error} If {kty} is not supported + */ + Object.defineProperty(this, "generate", { + value: function(kty, size, props) { + var keytype = registry.get(kty); + if (!keytype) { + return Promise.reject(new Error("unsupported key type")); + } + + props = clone(props || {}); + props.kty = kty; + + var self = this, + promise = keytype.generate(size); + return promise.then(function(jwk) { + jwk = merge(props, jwk, { + kty: kty + }); + return self.add(jwk); + }); + } + }); + /** + * @method JWK.KeyStore#add + * @description + * Adds a Key to this KeyStore. If {jwk} is a string, it is first + * parsed into a plain JSON object. If {jwk} is already an instance + * of JWK.Key, its (public) JSON representation is first obtained + * then applied to a new JWK.Key object within this KeyStore. + * + * @param {String|Object} jwk The JSON Web Key (JWK) + * @returns {Promise} The promise for the added key + * @throws {Error} If the key type is not supported + */ + Object.defineProperty(this, "add", { + value: function(jwk) { + if (typeof jwk === "string") { + jwk = JSON.parse(jwk); + } else if (JWKStore.isKey(jwk)) { + // assume a complete duplicate is desired + jwk = jwk.toJSON(true); + } + + var keytype = registry.get(jwk.kty); + if (!keytype) { + return Promise.reject(new Error("unsupported key type")); + } + + var self = this, + promise = keytype.prepare(jwk); + return promise.then(function(cfg) { + return new JWK.BaseKey(jwk.kty, self, jwk, cfg); + }).then(function(jwk) { + var kid = jwk.kid || ""; + var keys = keysets[kid] = keysets[kid] || []; + keys.push(jwk); + + return jwk; + }); + } + }); + /** + * @method JWK.KeyStore#remove + * @description + * Removes a Key from this KeyStore. + * + * **NOTE:** The removed Key's {keystore} property is not changed. + * + * @param {JWK.Key} jwk The key to remove. + */ + Object.defineProperty(this, "remove", { + value: function(jwk) { + if (!jwk) { + return; + } + + var keys = keysets[jwk.kid]; + if (!keys) { + return; + } + + var pos = keys.indexOf(jwk); + if (pos === -1) { + return; + } + + keys.splice(pos, 1); + if (!keys.length) { + delete keysets[jwk.kid]; + } + } + }); + + /** + * @method JWK.KeyStore#all + * @description + * Retrieves all of the contained Keys that optinally match all of the + * given properties. + * + * If {props} are specified, this method only returns Keys which exactly + * match the given properties. The properties can be any of the + * following: + * + * + **alg**: The algorithm for the Key. + * + **use**: The usage for the Key. + * + **kid**: The identifier for the Key. + * + * If no properties are given, this method returns all of the Keys for this + * KeyStore. + * + * @param {Object} [props] The properties to match against + * @param {Boolean} [local = false] `true` if only the Keys + * directly contained by this KeyStore should be returned, or + * `false` if it should return all Keys of this KeyStore and + * its ancestors. + * @returns {JWK.Key[]} The list of matching Keys, or an empty array if no + * matches are found. + */ + Object.defineProperty(this, "all", { + value: function(props, local) { + props = props || {}; + + var candidates = []; + var matches = function(key) { + // match on 'kty' + if (props.kty && + key.kty && + props.kty !== key.kty) { + return false; + } + // match on 'use' + if (props.use && + key.use && + props.use !== key.use) { + return false; + } + // match on 'alg' + if (props.alg) { + if (props.alg !== "dir" && + key.alg && + props.alg !== key.alg) { + return false; + } + return key.supports(props.alg); + } + //TODO: match on 'key_ops' + + return true; + }; + Object.keys(keysets).forEach(function(id) { + if (props.kid && props.kid !== id) { + return; + } + + var keys = keysets[id].filter(matches); + if (keys.length) { + candidates = candidates.concat(keys); + } + }); + + if (!local && parent) { + candidates = candidates.concat(parent.all(props)); + } + + return candidates; + } + }); + /** + * @method JWK.KeyStore#get + * @description + * Retrieves the contained Key matching the given {kid}, and optionally + * all of the given properties. This method equivalent to calling + * {@link JWK.Store#all}, then returning the first Key whose + * "kid" is {kid}. If {kid} is undefined, then the first Key that + * is returned from `all()` is returned. + * + * @param {String} [kid] The key identifier to match against. + * @param {Object} [props] The properties to match against. + * @param {Boolean} [local = false] `true` if only the Keys + * directly contained by this KeyStore should be returned, or + * `false` if it should return all Keys of this KeyStore and + * its ancestors. + * @returns {JWK.Key} The Key matching {kid} and {props}, or `null` + * if no match is found. + */ + Object.defineProperty(this, "get", { + value: function(kid, props, local) { + // reconcile arguments + if (typeof kid === "boolean") { + local = kid; + props = kid = null; + } else if (typeof kid === "object") { + local = props; + props = kid; + kid = null; + } + + // fixup props + props = props || {}; + if (kid) { + props.kid = kid; + } + + var candidates = this.all(props, true); + if (!candidates.length && parent && !local) { + candidates = parent.get(props, local); + } + return candidates[0] || null; + } + }); + + /** + * @method JWK.KeyStore#toJSON + * @description + * Generates a JSON representation of this KeyStore, which conforms + * to a JWK Set from {I-D.ietf-jose-json-web-key}. + * + * @param {Boolean} [isPrivate = false] `true` if the private fields + * of stored keys are to be included. + * @returns {Object} The JSON representation of this KeyStore. + */ + Object.defineProperty(this, "toJSON", { + value: function(isPrivate) { + var keys = []; + + Object.keys(keysets).forEach(function(kid) { + var items = keysets[kid].map(function(k) { + return k.toJSON(isPrivate); + }); + keys = keys.concat(items); + }); + + return { + keys: keys + }; + } + }); +}; + +/** + * Determines if the given object is an instance of JWK.KeyStore. + * + * @param {Object} obj The object to test + * @returns {Boolean} `true` if {obj} is an instance of JWK.KeyStore, + * and `false` otherwise. + */ +JWKStore.isKeyStore = function(obj) { + if (!obj) { + return false; + } + + if ("object" !== typeof obj) { + return false; + } + + if ("function" !== typeof obj.get || + "function" !== typeof obj.all || + "function" !== typeof obj.generate || + "function" !== typeof obj.add || + "function" !== typeof obj.remove) { + return false; + } + + return true; +}; + +/** + * Creates a new empty KeyStore. + * + * @returns {JWK.KeyStore} The empty KeyStore. + */ +JWKStore.createKeyStore = function() { + return new JWKStore(GLOBAL_REGISTRY); +}; + +/** + * Coerces the given object into a KeyStore. This method uses the following + * algorithm to coerce {ks}: + * + * 1. if {ks} is an instance of JWK.KeyStore, it is returned directly + * 2. if {ks} is a string, it is parsed into a JSON value + * 3. if {ks} is an array, it creates a new JWK.KeyStore and calls {@link + * JWK.KeyStore#add} for each element in the {ks} array. + * 4. if {ks} is a JSON object, it creates a new JWK.KeyStore and calls {@link + * JWK.KeyStore#add} for each element in the "keys" property. + * + * @param {Object|String} ks The value to coerce into a + * KeyStore + * @returns {Promise(JWK.KeyStore)} A promise for the coerced KeyStore. + */ +JWKStore.asKeyStore = function(ks) { + if (JWKStore.isKeyStore(ks)) { + return Promise.resolve(ks); + } + + var store = JWKStore.createKeyStore(), + keys; + + if (typeof ks === "string") { + ks = JSON.parse(ks); + } + + if (Array.isArray(ks)) { + keys = ks; + } else if ("keys" in ks) { + keys = ks.keys; + } else { + return Promise.reject("invalid keystore"); + } + + keys = keys.map(function(k) { + return store.add(k); + }); + + var promise = Promise.all(keys); + promise = promise.then(function() { + return store; + }); + + return promise; +}; + + +/** + * Determines if the given object is a JWK.Key instance. + * + * @param {Object} obj The object to test + * @returns `true` if {obj} is a JWK.Key + */ +JWKStore.isKey = function(obj) { + if (!obj) { + return false; + } + + if ("object" !== typeof obj) { + return false; + } + + if (!JWKStore.isKeyStore(obj.keystore)) { + return false; + } + + if ("string" !== typeof obj.kty || + "number" !== typeof obj.length || + "function" !== typeof obj.algorithms || + "function" !== typeof obj.supports || + "function" !== typeof obj.encrypt || + "function" !== typeof obj.decrypt || + "function" !== typeof obj.wrap || + "function" !== typeof obj.unwrap || + "function" !== typeof obj.sign || + "function" !== typeof obj.verify) { + return false; + } + + return true; +}; + +/** + * Coerces the given object into a Key. If {key} is an instance of JWK.Key, + * it is returned directly. Otherwise, this method first creates a new + * JWK.KeyStore and calls {@link JWK.KeyStore#add} on this new KeyStore. + * + * @param {Object|String} key The value to coerce into a Key + * @returns {Promise(JWK.Key)} A promise for the coerced Key. + */ +JWKStore.asKey = function(key) { + if (JWKStore.isKey(key)) { + return Promise.resolve(key); + } + + var ks = JWKStore.createKeyStore(); + key = ks.add(key); + + return key; +}; + +module.exports = { + KeyRegistry: JWKRegistry, + KeyStore: JWKStore, + registry: GLOBAL_REGISTRY +}; diff --git a/lib/jwk/octkey.js b/lib/jwk/octkey.js new file mode 100644 index 0000000..b792925 --- /dev/null +++ b/lib/jwk/octkey.js @@ -0,0 +1,197 @@ +/*! + * jwk/octkey.js - Symmetric Octet Key Representation + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var util = require("../util"); + +var JWK = { + BaseKey: require("./basekey.js"), + helpers: require("./helpers.js") +}; + +var SIG_ALGS = [ + "HS256", + "HS384", + "HS512" +]; +var ENC_ALGS = [ + "A128GCM", + "A192GCM", + "A256GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" +]; +var WRAP_ALGS = [ + "A128KW", + "A192KW", + "A256KW", + "A128GCMKW", + "A192GCMKW", + "A256GCMKW", + "PBES2-HS256+A128KW", + "PBES2-HS384+A192KW", + "PBES2-HS512+A256KW", + "dir" +]; + +function adjustDecryptProps(alg, props) { + if ("iv" in props) { + props.iv = Buffer.isBuffer(props.iv) ? + props.iv : + util.base64url.decode(props.iv || ""); + } + if ("adata" in props) { + props.adata = Buffer.isBuffer(props.adata) ? + props.adata : + new Buffer(props.adata || "", "utf8"); + } + if ("mac" in props) { + props.mac = Buffer.isBuffer(props.mac) ? + props.mac : + util.base64url.decode(props.mac || ""); + } + if ("tag" in props) { + props.tag = Buffer.isBuffer(props.tag) ? + props.tag : + util.base64url.decode(props.tag || ""); + } + + return props; +} +function adjustEncryptProps(alg, props) { + if ("iv" in props) { + props.iv = Buffer.isBuffer(props.iv) ? + props.iv : + util.base64url.decode(props.iv || ""); + } + if ("adata" in props) { + props.adata = Buffer.isBuffer(props.adata) ? + props.adata : + new Buffer(props.adata || "", "utf8"); + } + + return props; +} + +var JWKOctetCfg = { + publicKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + ]); + + var pk; + pk = JWK.helpers.unpackProps(props, fields); + + return pk; + }, + privateKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + {name: "k", type: "binary"} + ]); + + var pk; + pk = JWK.helpers.unpackProps(props, fields); + if (pk && pk.k) { + pk.length = pk.k.length * 8; + } else { + pk = undefined; + } + + return pk; + }, + + algorithms: function(keys, mode) { + var len = keys.private && (keys.private.k.length * 8); + var mins = [256, 384, 512]; + + if (!len) { + return []; + } + switch (mode) { + case "encrypt": + case "decrypt": + return ENC_ALGS.filter(function(a) { + return (a === ("A" + (len / 2) + "CBC-HS" + len)) || + (a === ("A" + len + "GCM")); + }); + case "sign": + case "verify": + // TODO: allow for HS{less-than-keysize} + return SIG_ALGS.filter(function(a) { + var result = false; + mins.forEach(function(m) { + if (m > len) { return; } + result = result | (a === ("HS" + m)); + }); + return result; + }); + case "wrap": + case "unwrap": + return WRAP_ALGS.filter(function(a) { + return (a === ("A" + len + "KW")) || + (a === ("A" + len + "GCMKW")) || + (a.indexOf("PBES2-") === 0) || + (a === "dir"); + }); + } + + return []; + }, + encryptKey: function(alg, keys) { + return keys.private && keys.private.k; + }, + encryptProps: adjustEncryptProps, + + decryptKey: function(alg, keys) { + return keys.private && keys.private.k; + }, + decryptProps: adjustDecryptProps, + + wrapKey: function(alg, keys) { + return keys.private && keys.private.k; + }, + wrapProps: adjustEncryptProps, + + unwrapKey: function(alg, keys) { + return keys.private && keys.private.k; + }, + unwrapProps: adjustDecryptProps, + + signKey: function(alg, keys) { + return keys.private && keys.private.k; + }, + verifyKey: function(alg, keys) { + return keys.private && keys.private.k; + } +}; + +// Factory +var JWKOctetFactory = { + kty: "oct", + prepare: function() { + // TODO: validate key properties + return Promise.resolve(JWKOctetCfg); + }, + generate: function(size) { + // TODO: validate key sizes + var key = util.randomBytes(size / 8); + + return Promise.resolve({ + k: key + }); + } +}; + +// public API +module.exports = Object.freeze({ + config: JWKOctetCfg, + factory: JWKOctetFactory +}); + +// registration +(function(REGISTRY) { + REGISTRY.register(JWKOctetFactory); +})(require("./keystore").registry); diff --git a/lib/jwk/rsakey.js b/lib/jwk/rsakey.js new file mode 100644 index 0000000..65fbb39 --- /dev/null +++ b/lib/jwk/rsakey.js @@ -0,0 +1,164 @@ +/*! + * jwk/rsa.js - RSA Key Representation + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var forge = require("../deps/forge.js"); + +var JWK = { + BaseKey: require("./basekey.js"), + helpers: require("./helpers.js") +}; + +var SIG_ALGS = [ + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512" +]; +var WRAP_ALGS = [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" +]; + +var JWKRsaCfg = { + publicKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + {name: "n", type: "binary"}, + {name: "e", type: "binary"} + ]); + var pk; + pk = JWK.helpers.unpackProps(props, fields); + if (pk && pk.n && pk.e) { + pk.length = pk.n.length * 8; + } else { + delete pk.e; + delete pk.n; + } + + return pk; + }, + privateKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + {name: "n", type: "binary"}, + {name: "e", type: "binary"}, + {name: "d", type: "binary"}, + {name: "p", type: "binary"}, + {name: "q", type: "binary"}, + {name: "dp", type: "binary"}, + {name: "dq", type: "binary"}, + {name: "qi", type: "binary"} + ]); + + var pk; + pk = JWK.helpers.unpackProps(props, fields); + if (pk && pk.d && pk.n && pk.e && pk.p && pk.q && pk.dp && pk.dq && pk.qi) { + pk.length = pk.d.length * 8; + } else { + pk = undefined; + } + + return pk; + }, + algorithms: function(keys, mode) { + switch (mode) { + case "encrypt": + case "decrypt": + return []; + case "wrap": + return (keys.public && WRAP_ALGS.slice()) || []; + case "unwrap": + return (keys.private && WRAP_ALGS.slice()) || []; + case "sign": + return (keys.private && SIG_ALGS.slice()) || []; + case "verify": + return (keys.public && SIG_ALGS.slice()) || []; + } + + return []; + }, + + wrapKey: function(alg, keys) { + return keys.public; + }, + unwrapKey: function(alg, keys) { + return keys.private; + }, + + signKey: function(alg, keys) { + return keys.private; + }, + verifyKey: function(alg, keys) { + return keys.public; + } +}; + +function convertBNtoBuffer(bn) { + bn = bn.toString(16); + if (bn.length % 2) { + bn = "0" + bn; + } + return new Buffer(bn, "hex"); +} + +// Factory +var JWKRsaFactory = { + kty: "RSA", + prepare: function() { + // TODO: validate key properties + return Promise.resolve(JWKRsaCfg); + }, + generate: function(size) { + // TODO: validate key sizes + var key = forge.pki.rsa.generateKeyPair({ + bits: size, + e: 0x010001 + }); + key = key.privateKey; + + // convert to JSON-ish + var result = {}; + [ + "e", + "n", + "d", + "p", + "q", + {incoming: "dP", outgoing: "dp"}, + {incoming: "dQ", outgoing: "dq"}, + {incoming: "qInv", outgoing: "qi"} + ].forEach(function(f) { + var incoming, + outgoing; + + if ("string" === typeof f) { + incoming = outgoing = f; + } else { + incoming = f.incoming; + outgoing = f.outgoing; + } + + if (incoming in key) { + result[outgoing] = convertBNtoBuffer(key[incoming]); + } + }); + + return Promise.resolve(result); + } +}; + +// public API +module.exports = Object.freeze({ + config: JWKRsaCfg, + factory: JWKRsaFactory +}); + +// registration +(function(REGISTRY) { + REGISTRY.register(JWKRsaFactory); +})(require("./keystore").registry); diff --git a/lib/jws/defaults.js b/lib/jws/defaults.js new file mode 100644 index 0000000..8583da0 --- /dev/null +++ b/lib/jws/defaults.js @@ -0,0 +1,25 @@ +/*! + * jws/defaults.js - Defaults for JWSs + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +/** + * @description + * The default options for {@link JWS.createSign}. + * + * @property {Boolean} compact Determines if the output is the Compact + * serialization (`true`) or the JSON serialization (**`false`**, + * the default). + * @property {String|String[]} protect The names of the headers to integrity + * protect. The value `""` means that none of header parameters + * are integrity protected, while `"*"` (the default) means that all + * headers parameter sare integrity protected. + */ +var JWSDefaults = { + compact: false, + protect: "*" +}; + +module.exports = JWSDefaults; diff --git a/lib/jws/helpers.js b/lib/jws/helpers.js new file mode 100644 index 0000000..6fc212e --- /dev/null +++ b/lib/jws/helpers.js @@ -0,0 +1,12 @@ +/*! + * jws/helpers.js - JWS Internal Helper Functions + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +module.exports = { + slice: function(input, start) { + return Array.prototype.slice.call(input, start || 0); + } +}; diff --git a/lib/jws/index.js b/lib/jws/index.js new file mode 100644 index 0000000..adabcbc --- /dev/null +++ b/lib/jws/index.js @@ -0,0 +1,13 @@ +/*! + * jws/index.js - JSON Web Signature (JWS) Entry Point + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var JWS = { + createSign: require("./sign").createSign, + createVerify: require("./verify").createVerify +}; + +module.exports = JWS; diff --git a/lib/jws/sign.js b/lib/jws/sign.js new file mode 100644 index 0000000..470b765 --- /dev/null +++ b/lib/jws/sign.js @@ -0,0 +1,367 @@ +/*! + * jws/sign.js - Sign to JWS + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var clone = require("lodash.clone"), + merge = require("../util/merge"), + uniq = require("lodash.uniq"), + util = require("../util"), + JWK = require("../jwk"), + slice = require("./helpers").slice; + +var DEFAULTS = require("./defaults"); + +/** + * @class JWS.Signer + * @classdesc Generator of signed content. + * + * @description + * **NOTE:** this class cannot be instantiated directly. Instead call {@link + * JWS.createSign}. + */ +var JWSSigner = function(cfg, signatories) { + var finalized = false, + format = cfg.format || "general", + content = new Buffer(0); + + /** + * @member {Boolean} JWS.Signer#compact + * @description + * Indicates whether the outuput of this signature generator is using + * the Compact serialization (`true`) or the JSON serialization + * (`false`). + */ + Object.defineProperty(this, "compact", { + get: function() { + return "compact" === format; + }, + enumerable: true + }); + Object.defineProperty(this, "format", { + get: function() { + return format; + }, + enumerable: true + }); + + /** + * @method JWS.Signer#update + * @description + * Updates the signing content for this signature content. The content + * is appended to the end of any other content already applied. + * + * If {data} is a Buffer, {encoding} is ignored. Otherwise, {data} is + * converted to a Buffer internally to {encoding}. + * + * @param {Buffer|String} data The data to sign. + * @param {String} [encoding="binary"] The encoding of {data}. + * @returns {JWS.Signer} This signature generator. + * @throws {Error} If a signature has already been generated. + */ + Object.defineProperty(this, "update", { + value: function(data, encoding) { + if (finalized) { + throw new Error("already final"); + } + if (data != null) { + data = util.asBuffer(data, encoding); + if (content.length) { + content = Buffer.concat([content, data], + content.length + data.length); + } else { + content = data; + } + } + + return this; + } + }); + /** + * @method JWS.Signer#final + * @description + * Finishes the signature operation. + * + * The returned Promise, when fulfilled, is the JSON Web Signature (JWS) + * object, either in the Compact (if {@link JWS.Signer#format} is + * `"compact"`), the flattened JSON (if {@link JWS.Signer#format} is + * "flattened"), or the general JSON serialization. + * + * @param {Buffer|String} [data] The final content to apply. + * @param {String} [encoding="binary"] The encoding of the final content + * (if any). + * @returns {Promise} The promise for the signatures + * @throws {Error} If a signature has already been generated. + */ + Object.defineProperty(this, "final", { + value: function(data, encoding) { + if (finalized) { + throw new Error("already final"); + } + + // last-minute data + this.update(data, encoding); + + // mark as done...ish + finalized = true; + var promise; + + // map signatory promises to just signatories + promise = Promise.all(signatories); + promise = promise.then(function(sigs) { + // prepare content + content = util.base64url.encode(content); + + sigs = sigs.map(function(s) { + // prepare protected + var protect = {}, + lenProtect = 0, + unprotect = clone(s.header), + lenUnprotect = Object.keys(unprotect).length; + s.protected.forEach(function(h) { + if (!(h in unprotect)) { + return; + } + protect[h] = unprotect[h]; + lenProtect++; + delete unprotect[h]; + lenUnprotect--; + }); + if (lenProtect > 0) { + protect = JSON.stringify(protect); + protect = util.base64url.encode(protect); + } else { + protect = ""; + } + + // signit! + var data = new Buffer(protect + "." + content, "ascii"); + s = s.key.sign(s.header.alg, data, s.header); + s = s.then(function(result) { + var sig = {}; + if (0 < lenProtect) { + sig.protected = protect; + } + if (0 < lenUnprotect) { + sig.unprotected = unprotect; + } + sig.signature = util.base64url.encode(result.mac); + return sig; + }); + return s; + }); + sigs = [Promise.resolve(content)].concat(sigs); + return Promise.all(sigs); + }); + promise = promise.then(function(results) { + var content = results[0]; + return { + payload: content, + signatures: results.slice(1) + }; + }); + switch (format) { + case "compact": + promise = promise.then(function(jws) { + var compact = [ + jws.signatures[0].protected, + jws.payload, + jws.signatures[0].signature + ]; + compact = compact.join("."); + return compact; + }); + break; + case "flattened": + promise = promise.then(function(jws) { + var flattened = {}; + flattened.payload = jws.payload; + + var sig = jws.signatures[0]; + if (sig.protected) { + flattened.protected = sig.protected; + } + if (sig.header) { + flattened.header = sig.header; + } + flattened.signature = sig.signature; + + return flattened; + }); + break; + } + + return promise; + } + }); +}; + + +/** + * @description + * Creates a new JWS.Signer with the given options and signatories. + * + * @param {Object} [opts] The signing options + * @param {Boolean} [opts.compact] Use compact serialization? + * @param {String} [opts.format] The serialization format to use ("compact", + * "flattened", "general") + * @param {Object} [opts.fields] Additional header fields + * @param {JWK.Key[]|Object[]} [signs] Signatories, either as an array of + * JWK.Key instances; or an array of objects, each with the following + * properties + * @param {JWK.Key} signs.key Key used to sign content + * @param {Object} [signs.header] Per-signatory header fields + * @param {String} [signs.reference] Reference field to identify the key + * @param {String[]|String} [signs.protect] List of fields to integrity + * protect ("*" to protect all fields) + * @returns {JWS.Signer} The signature generator. + * @throws {Error} If Compact serialization is requested but there are + * multiple signatories + */ +function createSign(opts, signs) { + // fixup signatories + var options = opts, + signStart = 1, + signList = signs; + + if (arguments.length === 0) { + throw new Error("at least one signatory must be provided"); + } + if (arguments.length === 1) { + signList = opts; + signStart = 0; + options = {}; + } else if (JWK.isKey(opts) || + (opts && "kty" in opts) || + (opts && "key" in opts && + (JWK.isKey(opts.key) || "kty" in opts.key))) { + signList = opts; + signStart = 0; + options = {}; + } else { + options = clone(opts); + } + if (!Array.isArray(signList)) { + signList = slice(arguments, signStart); + } + + // fixup options + options = merge(clone(DEFAULTS), options); + + // setup header fields + var allFields = options.fields || {}; + // setup serialization format + var format = options.format; + if (!format) { + format = options.compact ? "compact" : "general"; + } + if (("compact" === format || "flattened" === format) && 1 < signList.length) { + throw new Error("too many signatories for compact or flattened JSON serialization"); + } + + // note protected fields (globally) + // protected fields are per signature + var protectAll = ("*" === options.protect); + if (options.compact) { + protectAll = true; + } + + signList = signList.map(function(s, idx) { + var p; + + // resolve a key + if (s && "kty" in s) { + p = JWK.asKey(s); + p = p.then(function(k) { + return { + key: k + }; + }); + } else if (s) { + p = JWK.asKey(s.key); + p = p.then(function(k) { + return { + header: s.header, + reference: s.reference, + protect: s.protect, + key: k + }; + }); + } else { + p = Promise.reject(new Error("missing key for signatory " + idx)); + } + + // resolve the complete signatory + p = p.then(function(signatory) { + var key = signatory.key; + + // make sure there is a header + var header = signatory.header || {}; + header = merge(merge({}, allFields), header); + signatory.header = header; + + // ensure an algorithm + if (!header.alg) { + header.alg = key.algorithms(JWK.MODE_SIGN)[0] || ""; + } + + // determine the key reference + var ref = signatory.reference; + delete signatory.reference; + if (undefined === ref) { + // header already contains the key reference + ref = ["kid", "jku", "x5c", "x5t", "x5u"].some(function(k) { + return (k in header); + }); + ref = !ref ? "kid" : null; + } else if ("boolean" === typeof ref) { + // explicit (positive | negative) request for key reference + ref = ref ? "kid" : null; + } + var jwk; + if (ref) { + jwk = key.toJSON(); + if ("jwk" === ref) { + header.jwk = jwk; + } else if (ref in jwk) { + header[ref] = jwk[ref]; + } + } + + // determine protected fields + var protect = signatory.protect; + if (protectAll || "*" === protect) { + protect = Object.keys(header); + } else if ("string" === protect) { + protect = [protect]; + } else if (Array.isArray(protect)) { + protect = protect.concat(); + } else if (!protect) { + protect = []; + } else { + return Promise.reject(new Error("protect must be a list of fields")); + } + protect = uniq(protect); + signatory.protected = protect; + + // freeze signatory + signatory = Object.freeze(signatory); + return signatory; + }); + + return p; + }); + + var cfg = { + format: format + }; + return new JWSSigner(cfg, + signList); +} + +module.exports = { + signer: JWSSigner, + createSign: createSign +}; diff --git a/lib/jws/verify.js b/lib/jws/verify.js new file mode 100644 index 0000000..360ba9f --- /dev/null +++ b/lib/jws/verify.js @@ -0,0 +1,140 @@ +/*! + * jws/verify.js - Verifies from a JWS + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var clone = require("lodash.clone"), + merge = require("../util/merge"), + base64url = require("../util/base64url"), + JWK = require("../jwk"); + +/** + * @class JWS.Verifier + * @classdesc Parser of signed content. + * + * @description + * **NOTE:** this class cannot be instantiated directly. Instead call {@link + * JWS.createVerify}. + */ +var JWSVerifier = function(ks) { + var assumedKey, + keystore; + + if (JWK.isKey(ks)) { + assumedKey = ks; + keystore = assumedKey.keystore; + } else if (JWK.isKeyStore(ks)) { + keystore = ks; + } else { + throw new TypeError("Keystore must be provided"); + } + + Object.defineProperty(this, "verify", { + value: function(input) { + if ("string" === typeof input) { + input = input.split("."); + input = { + payload: input[1], + signatures: [ + { + protected: input[0], + signature: input[2] + } + ] + }; + } else if (!input || "object" === input) { + throw new Error("invalid input"); + } + + // fixup "flattened JSON" to look like "general JSON" + if (input.signature) { + input.signatures = [ + { + protected: input.protected || undefined, + header: input.header || undefined, + signature: input.signature + } + ]; + } + + // ensure signatories exists + var sigList = input.signatures || [{}]; + + // combine fields and decode signature per signatory + sigList = sigList.map(function(s) { + var header = clone(s.header || {}); + var protect = s.protected ? + JSON.parse(base64url.decode(s.protected, "utf8")) : + {}; + header = merge(header, protect); + var signature = base64url.decode(s.signature); + + return { + protected: s.protected, + header: header, + signature: signature + }; + }); + + var promise = new Promise(function(resolve, reject) { + var processSig = function() { + var sig = sigList.shift(); + if (!sig) { + reject(new Error("no key found")); + return; + } + + var content = new Buffer((sig.protected || "") + "." + input.payload, "ascii"); + + var algPromise, + algKey = keystore.get({ + use: "sig", + alg: sig.header.alg, + kid: sig.header.kid + }); + if (algKey) { + algPromise = algKey.verify(sig.header.alg, + content, + sig.signature); + } else { + algPromise = Promise.reject("key does not match"); + } + algPromise = algPromise.then(function(result) { + var payload = result.data.toString("ascii"); + payload = payload.split(".")[1]; + payload = base64url.decode(payload); + var jws = { + header: sig.header, + payload: payload, + signature: result.mac + }; + resolve(jws); + }, processSig); + }; + processSig(); + }); + + return promise; + } + }); +}; + +/** + * @description + * Creates a new JWS.Verifier with the given Key or KeyStore. + * + * @param {JWK.Key|JWK.KeyStore} ks The Key or KeyStore to use for verification. + * @returns {JWS.Verifier} The new Verifier. + */ +function createVerify(ks) { + var vfy = new JWSVerifier(ks); + + return vfy; +} + +module.exports = { + verifier: JWSVerifier, + createVerify: createVerify +}; diff --git a/lib/util/base64url.js b/lib/util/base64url.js new file mode 100644 index 0000000..b6c2c16 --- /dev/null +++ b/lib/util/base64url.js @@ -0,0 +1,73 @@ +/*! + * util/base64url.js - Implementation of web-safe Base64 Encoder/Decoder + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +/** + * @namespace base64url + * @description + * Provides methods to encode and decode data according to the + * base64url alphabet. + */ +var base64url = exports; +/** + * Encodes the input to base64url. + * + * If {input} is a Buffer, then {encoding} is ignored. Otherwise, + * {encoding} can be one of "binary", "base64", "hex", "utf8". + * + * @param {Buffer|String} input The data to encode. + * @param {String} [encoding = binary] The input encoding format. + * @returns {String} the base64url encoding of {input}. + */ +base64url.encode = function(input, encoding) { + var fn = function(match) { + switch(match) { + case "+": return "-"; + case "/": return "_"; + case "=": return ""; + } + // should never happen + }; + + encoding = encoding || "binary"; + if (Buffer.isBuffer(input)) { + input = input.toString("base64"); + } else { + if ("undefined" !== typeof ArrayBuffer && input instanceof ArrayBuffer) { + input = new Uint8Array(input); + } + input = new Buffer(input, encoding).toString("base64"); + } + + return input.replace(/\+|\/|\=/g, fn); +}; +/** + * Decodes the input from base64url. + * + * If {encoding} is not specified, then this method returns a Buffer. + * Othewise, {encoding} can be one of "binary", "base64", "hex", "utf8"; + * this method then returns a string matching the given encoding. + * + * @param {String} input The data to decode. + * @param {String} [encoding] The output encoding format. + * @returns {Buffer|String} the base64url decoding of {input}. + */ +base64url.decode = function(input, encoding) { + var fn = function(match) { + switch(match) { + case "-": return "+"; + case "_": return "/"; + } + // should never happen + }; + + input = input.replace(/\-|\_/g, fn); + var output = new Buffer(input, "base64"); + if (encoding) { + output = output.toString(encoding); + } + return output; +}; diff --git a/lib/util/databuffer.js b/lib/util/databuffer.js new file mode 100644 index 0000000..dcb7718 --- /dev/null +++ b/lib/util/databuffer.js @@ -0,0 +1,480 @@ +/*! + * util/databuffer.js - Forge-compatible Buffer based on Node.js Buffers + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var forge = require("../deps/forge.js"), + base64url = require("./base64url.js"); + +/** + * + */ +function DataBuffer(b, options) { + options = options || {}; + + // treat (views of) (Array)Buffers special + // NOTE: default implementation creates copies, but efficiently + // wherever possible + if (Buffer.isBuffer(b)) { + this.data = b; + } else if (forge.util.isArrayBuffer(b)) { + b = new Uint8Array(b); + this.data = new Buffer(b); + } else if (forge.util.isArrayBufferView(b)) { + b = new Uint8Array(b.buffer, b.byteOffset, b.byteLength); + this.data = new Buffer(b); + } + + if (this.data) { + this.write = this.data.length; + b = undefined; + } + + // setup growth rate + this.growSize = options.growSize || DataBuffer.DEFAULT_GROW_SIZE; + + // initialize pointers and data + this.write = this.write || 0; + this.read = this.read || 0; + if (b) { + this.putBytes(b); + } else if (!this.data) { + this.accommodate(0); + } + + // massage read/write pointers + options.readOffset = ("readOffset" in options) ? + options.readOffset : + this.read; + this.write = ("writeOffset" in options) ? + options.writeOffset : + this.write; + this.read = Math.min(options.readOffset, this.write); +} +DataBuffer.DEFAULT_GROW_SIZE = 16; + +DataBuffer.prototype.length = function() { + return this.write - this.read; +}; +DataBuffer.prototype.available = function() { + return this.data.length - this.write; +}; +DataBuffer.prototype.isEmpty = function() { + return this.length() <= 0; +}; + +DataBuffer.prototype.accommodate = function(length) { + if (!this.data) { + // initializes a new buffer + length = Math.max(this.write + length, this.growSize); + + this.data = new Buffer(length); + } else if (this.available() < length) { + length = Math.max(length, this.growSize); + + // create a new empty buffer, and copy current one into it + var src = this.data; + var dst = new Buffer(src.length + length); + src.copy(dst, 0); + + // set data as the new buffer + this.data = dst; + } + // ensure the rest is 0 + this.data.fill(0, this.write); + + return this; +}; +DataBuffer.prototype.clear = function() { + this.read = this.write = 0; + this.data = new Buffer(0); + return this; +}; +DataBuffer.prototype.truncate = function(count) { + // chop off bytes from the end + this.write = this.read + Math.max(0, this.length() - count); + // ensure the remainder is 0 + this.data.fill(0, this.write); + return this; +}; +DataBuffer.prototype.compact = function() { + if (this.read > 0) { + if (this.write === this.read) { + this.read = this.write = 0; + } else { + this.data.copy(this.data, 0, this.read, this.write); + this.write = this.write - this.read; + this.read = 0; + } + // ensure remainder is 0 + this.data.fill(0, this.write); + } + return this; +}; +DataBuffer.prototype.copy = function() { + return new DataBuffer(this, { + readOffset: this.read, + writeOffset: this.write, + growSize: this.growSize + }); +}; + +DataBuffer.prototype.equals = function(test) { + if (!DataBuffer.isBuffer(test)) { + return false; + } + + if (test.length() !== this.length()) { + return false; + } + + var rval = true, + delta = this.read - test.read; + // constant time + for (var idx = test.read; test.write > idx; idx++) { + rval = rval && (this.data[idx + delta] === test.data[idx]); + } + return rval; +}; +DataBuffer.prototype.at = function(idx) { + return this.data[this.read + idx]; +}; +DataBuffer.prototype.setAt = function(idx, b) { + this.data[this.read + idx] = b; + return this; +}; +DataBuffer.prototype.last = function() { + return this.data[this.write - 1]; +}; +DataBuffer.prototype.bytes = function(count) { + var rval; + if (undefined === count) { + count = this.length(); + } else if (count) { + count = Math.min(count, this.length()); + } + + if (0 === count) { + rval = ""; + } else { + var begin = this.read, + end = begin + count, + data = this.data.slice(begin, end); + rval = String.fromCharCode.apply(null, data); + } + + return rval; +}; +DataBuffer.prototype.buffer = function(count) { + var rval; + if (undefined === count) { + count = this.length(); + } else if (count) { + count = Math.min(count, this.length()); + } + + if (0 === count) { + rval = new ArrayBuffer(0); + } else { + var begin = this.read, + end = begin + count, + data = this.data.slice(begin, end); + rval = new Uint8Array(end - begin); + rval.set(data); + } + + return rval; +}; +DataBuffer.prototype.native = function(count) { + var rval; + if ("undefined" === typeof count) { + count = this.length(); + } else if (count) { + count = Math.min(count, this.length()); + } + + if (0 === count) { + rval = new Buffer(0); + } else { + var begin = this.read, + end = begin + count; + rval = this.data.slice(begin, end); + } + + return rval; +}; + +DataBuffer.prototype.toHex = function() { + return this.toString("hex"); +}; +DataBuffer.prototype.toString = function(encoding) { + // short circuit empty string + if (0 === this.length()) { + return ""; + } + + var view = this.data.slice(this.read, this.write); + encoding = encoding || "utf8"; + // special cases, then built-in support + switch (encoding) { + case "raw": + return view.toString("binary"); + case "base64url": + return base64url.encode(view); + case "utf16": + return view.toString("ucs2"); + default: + return view.toString(encoding); + } +}; + +DataBuffer.prototype.fillWithByte = function(b, n) { + if (!n) { + n = this.available(); + } + this.accommodate(n); + this.data.fill(b, this.write, this.write + n); + this.write += n; + + return this; +}; + +DataBuffer.prototype.getBuffer = function(count) { + var rval = this.buffer(count); + this.read += rval.byteLength; + + return rval; +}; +DataBuffer.prototype.putBuffer = function(bytes) { + return this.putBytes(bytes); +}; + +DataBuffer.prototype.getBytes = function(count) { + var rval = this.bytes(count); + this.read += rval.length; + return rval; +}; +DataBuffer.prototype.putBytes = function(bytes, encoding) { + function augmentIt(src) { + return (Buffer._augment) ? + Buffer._augment(src) : + new Buffer(src); + } + + if ("string" === typeof bytes) { + // fixup encoding + encoding = encoding || "binary"; + switch (encoding) { + case "utf16": + // treat as UCS-2/UTF-16BE + encoding = "ucs-2"; + break; + case "raw": + encoding = "binary"; + break; + case "base64url": + // NOTE: this returns a Buffer + bytes = base64url.decode(bytes); + break; + } + + // replace bytes with decoded Buffer (if not already) + if (!Buffer.isBuffer(bytes)) { + bytes = new Buffer(bytes, encoding); + } + } + + var src, dst; + if (bytes instanceof DataBuffer) { + // be slightly more efficient + var orig = bytes; + bytes = orig.data.slice(orig.read, orig.write); + orig.read = orig.write; + } else if (bytes instanceof forge.util.ByteStringBuffer) { + bytes = bytes.getBytes(); + } + + // process array + if (Buffer.isBuffer(bytes)) { + src = bytes; + } else if (Array.isArray(bytes)) { + src = new Buffer(bytes); + } else if (forge.util.isArrayBuffer(bytes)) { + src = new Uint8Array(bytes); + src = augmentIt(src); + } else if (forge.util.isArrayBufferView(bytes)) { + src = (bytes instanceof Uint8Array) ? + bytes : + new Uint8Array(bytes.buffer, + bytes.byteOffset, + bytes.byteLength); + src = augmentIt(src); + } else { + throw new TypeError("invalid source type"); + } + + this.accommodate(src.length); + dst = this.data; + src.copy(dst, this.write); + this.write += src.length; + + return this; +}; + +DataBuffer.prototype.getNative = function(count) { + var rval = this.native(count); + this.read += rval.length; + return rval; +}; +DataBuffer.prototype.putNative = DataBuffer.prototype.putBuffer; + +DataBuffer.prototype.getByte = function() { + var b = this.data[this.read]; + this.read = Math.min(this.read + 1, this.write); + return b; +}; +DataBuffer.prototype.putByte = function(b) { + this.accommodate(1); + this.data[this.write] = b & 0xff; + this.write++; + + return this; +}; + +DataBuffer.prototype.getInt16 = function() { + var n = (this.data[this.read] << 8) ^ + (this.data[this.read + 1]); + this.read = Math.min(this.read + 2, this.write); + return n; +}; +DataBuffer.prototype.putInt16 = function(n) { + this.accommodate(2); + this.data[this.write] = (n >>> 8) & 0xff; + this.data[this.write + 1] = n & 0xff; + this.write += 2; + return this; +}; + +DataBuffer.prototype.getInt24 = function() { + var n = (this.data[this.read] << 16) ^ + (this.data[this.read + 1] << 8) ^ + this.data[this.read + 2]; + this.read = Math.min(this.read + 3, this.write); + return n; +}; +DataBuffer.prototype.putInt24 = function(n) { + this.accommodate(3); + this.data[this.write] = (n >>> 16) & 0xff; + this.data[this.write + 1] = (n >>> 8) & 0xff; + this.data[this.write + 2] = n & 0xff; + this.write += 3; + return this; +}; + +DataBuffer.prototype.getInt32 = function() { + var n = (this.data[this.read] << 24) ^ + (this.data[this.read + 1] << 16) ^ + (this.data[this.read + 2] << 8) ^ + this.data[this.read + 3]; + this.read = Math.min(this.read + 4, this.write); + return n; +}; +DataBuffer.prototype.putInt32 = function(n) { + this.accommodate(4); + this.data[this.write] = (n >>> 24) & 0xff; + this.data[this.write + 1] = (n >>> 16) & 0xff; + this.data[this.write + 2] = (n >>> 8) & 0xff; + this.data[this.write + 3] = n & 0xff; + this.write += 4; + return this; +}; + +DataBuffer.prototype.getInt16Le = function() { + var n = (this.data[this.read + 1] << 8) ^ + this.data[this.read]; + this.read = Math.min(this.read + 2, this.write); + return n; +}; +DataBuffer.prototype.putInt16Le = function(n) { + this.accommodate(2); + this.data[this.write + 1] = (n >>> 8) & 0xff; + this.data[this.write] = n & 0xff; + this.write += 2; + return this; +}; + +DataBuffer.prototype.getInt24Le = function() { + var n = (this.data[this.read + 2] << 16) ^ + (this.data[this.read + 1] << 8) ^ + this.data[this.read]; + this.read = Math.min(this.read + 3, this.write); + return n; +}; +DataBuffer.prototype.putInt24Le = function(n) { + this.accommodate(3); + this.data[this.write + 2] = (n >>> 16) & 0xff; + this.data[this.write + 1] = (n >>> 8) & 0xff; + this.data[this.write] = n & 0xff; + this.write += 3; + return this; +}; +DataBuffer.prototype.getInt32Le = function() { + var n = (this.data[this.read + 3] << 24) ^ + (this.data[this.read + 2] << 16) ^ + (this.data[this.read + 1] << 8) ^ + this.data[this.read]; + this.read = Math.min(this.read + 4, this.write); + return n; +}; +DataBuffer.prototype.putInt32Le = function(n) { + this.accommodate(4); + this.data[this.write + 3] = (n >>> 24) & 0xff; + this.data[this.write + 2] = (n >>> 16) & 0xff; + this.data[this.write + 1] = (n >>> 8) & 0xff; + this.data[this.write] = n & 0xff; + this.write += 4; + return this; +}; + +DataBuffer.prototype.getInt = function(bits) { + var rval = 0; + do { + rval = (rval << 8) | this.getByte(); + bits -= 8; + } while (bits > 0); + return rval; +}; +DataBuffer.prototype.putInt = function(n, bits) { + this.accommodate(Math.ceil(bits / 8)); + do { + bits -= 8; + this.putByte((n >> bits) & 0xff); + } while (bits > 0); + return this; +}; + +DataBuffer.prototype.putSignedInt = function(n, bits) { + if (n < 0) { + n += 2 << (bits - 1); + } + return this.putInt(n, bits); +}; + +DataBuffer.prototype.putString = function(str) { + return this.putBytes(str, "utf16"); +}; + +DataBuffer.isBuffer = function(test) { + return (test instanceof DataBuffer); +}; +DataBuffer.asBuffer = function(orig) { + return DataBuffer.isBuffer(orig) ? + orig : + orig ? + new DataBuffer(orig) : + new DataBuffer(); +}; + +module.exports = forge.util.ByteBuffer = DataBuffer; diff --git a/lib/util/index.js b/lib/util/index.js new file mode 100644 index 0000000..7250f0f --- /dev/null +++ b/lib/util/index.js @@ -0,0 +1,59 @@ +/*! + * util/index.js - Utilities Entry Point + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var forge = require("../deps/forge.js"); + +var util; + +function asBuffer(input, encoding) { + if (Buffer.isBuffer(input)) { + return input; + } + + if ("string" === typeof input) { + encoding = encoding || "binary"; + if ("base64url" === encoding) { + return util.base64url.decode(input); + } + return new Buffer(input, encoding); + } + + // assume input is an Array, ArrayBuffer, or ArrayBufferView + if (forge.util.isArrayBufferView(input)) { + input = (input instanceof Uint8Array) ? + input : + new Uint8Array(input.buffer, input.byteOffset, input.byteOffset + input.byteLength); + } else if (forge.util.isArrayBuffer(input)) { + input = new Uint8Array(input); + } + + var output; + if ("function" === typeof Buffer._augment) { + // web environments -- try to be efficient with memory + if (!(input instanceof Uint8Array)) { + input = new Uint8Array(input); + } + output = Buffer._augment(input); + } else { + // node.js -- don't care about being that efficient + output = new Buffer(input); + } + + return output; +} + +function randomBytes(len) { + return new Buffer(forge.random.getBytes(len), "binary"); +} + +util = { + base64url: require("./base64url.js"), + utf8: require("./utf8.js"), + asBuffer: asBuffer, + randomBytes: randomBytes +}; +module.exports = util; diff --git a/lib/util/merge.js b/lib/util/merge.js new file mode 100644 index 0000000..8bb0edc --- /dev/null +++ b/lib/util/merge.js @@ -0,0 +1,62 @@ +/*! + * util/utf8.js - Implementation of UTF-8 Encoder/Decoder + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var partialRight = require("lodash.partialright"), + merge = require("lodash.merge"); + +var typedArrayCtors = (function() { + var ctors = []; + if ("undefined" !== typeof Uint8Array) { + ctors.push(Uint8Array); + } + if ("undefined" !== typeof Uint8ClampedArray) { + ctors.push(Uint8ClampedArray); + } + if ("undefined" !== typeof Uint16Array) { + ctors.push(Uint16Array); + } + if ("undefined" !== typeof Uint32Array) { + ctors.push(Uint32Array); + } + if ("undefined" !== typeof Float32Array) { + ctors.push(Float32Array); + } + if ("undefined" !== typeof Float64Array) { + ctors.push(Float64Array); + } + return ctors; +})(); + +function findTypedArrayFor(ta) { + var ctor; + for (var idx = 0; !ctor && typedArrayCtors.length > idx; idx++) { + if (ta instanceof typedArrayCtors[idx]) { + ctor = typedArrayCtors[idx]; + } + } + return ctor; +} + +function mergeBuffer(a, b) { + // TODO: should this be a copy, or the reference itself? + if (Buffer.isBuffer(b)) { + b = new Buffer(b); + } else { + var Ctor = findTypedArrayFor(b); + b = Ctor ? + new Ctor(b, b.byteOffset, b.byteLength) : + undefined; + } + + // TODO: QUESTION: create a merged ?? + // for now, a is b + a = b; + + return b; +} + +module.exports = partialRight(merge, mergeBuffer); diff --git a/lib/util/utf8.js b/lib/util/utf8.js new file mode 100644 index 0000000..3c65825 --- /dev/null +++ b/lib/util/utf8.js @@ -0,0 +1,27 @@ +/*! + * util/utf8.js - Implementation of UTF-8 Encoder/Decoder + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var utf8 = exports; + +utf8.encode = function(input) { + var output = encodeURIComponent(input || ""); + output = output.replace(/\%([0-9a-fA-F]{2})/g, function(m, code) { + code = parseInt(code, 16); + return String.fromCharCode(code); + }); + + return output; +}; +utf8.decode = function(input) { + var output = (input || "").replace(/[\u0080-\u00ff]/g, function(m) { + var code = (0x100 | m.charCodeAt(0)).toString(16).substring(1); + return "%" + code; + }); + output = decodeURIComponent(output); + + return output; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0401ab1 --- /dev/null +++ b/package.json @@ -0,0 +1,80 @@ +{ + "name": "node-jose", + "version": "0.3.0", + "description": "A JavaScript implementation of the JSON Object Signing and Encryption (JOSE) for current web browsers and node.js-based servers", + "main": "lib/index.js", + "scripts": { + "test": "gulp test:nodejs" + }, + "repository": { + "type": "git", + "url": "git@github.com/cisco/node-jose.git" + }, + "keywords": [ + "crypto", + "jose", + "jwk", + "jws", + "jwe", + "jwa" + ], + "author": "Cisco Systems, Inc. ", + "contributors": [ + "Matthew A. Miller ", + "Ian W. Remmel " + ], + "license": "Apache-2.0", + "dependencies": { + "browserify": "^11.0.1", + "es6-promise": "^2.0.1", + "jsbn": "git+https://github.com/andyperlitch/jsbn.git", + "lodash.assign": "^3.2.0", + "lodash.clone": "^3.0.2", + "lodash.fill": "^3.2.2", + "lodash.flatten": "^3.0.2", + "lodash.intersection": "^3.2.0", + "lodash.merge": "^3.3.1", + "lodash.omit": "^3.1.0", + "lodash.partialright": "^3.1.0", + "lodash.pick": "^3.1.0", + "lodash.uniq": "^3.2.1", + "long": "^2.2.3", + "node-forge": "git+https://github.com/linuxwolf/forge.git#master", + "uuid": "^2.0.1" + }, + "devDependencies": { + "browserify": "^8.1.3", + "chai": "^1.10.0", + "conventional-changelog": "^0.4.3", + "del": "^1.1.1", + "gulp": "^3.8.10", + "gulp-eslint": "^0.5.0", + "gulp-istanbul": "^0.6.0", + "gulp-mocha": "^2.0.0", + "gulp-rename": "^1.2.0", + "gulp-sourcemaps": "^1.3.0", + "gulp-uglify": "^1.1.0", + "istanbul": "^0.3.5", + "jose-cookbook": "git+https://github.com/ietf-jose/cookbook.git", + "karma": "^0.12.31", + "karma-browserify": "^3.0.1", + "karma-chrome-launcher": "^0.1.7", + "karma-firefox-launcher": "^0.1.4", + "karma-ie-launcher": "^0.2.0", + "karma-mocha": "^0.1.10", + "karma-mocha-reporter": "^0.3.1", + "karma-safari-launcher": "^0.1.1", + "karma-sauce-launcher": "^0.2.11", + "lodash.bind": "^3.1.0", + "lodash.clonedeep": "^3.0.1", + "lodash.foreach": "^3.0.3", + "mocha": "^2.1.0", + "run-sequence": "^1.0.2", + "vinyl-buffer": "^1.0.0", + "vinyl-source-stream": "^1.0.0", + "yargs": "^3.10.0" + }, + "browser": { + "crypto": false + } +} diff --git a/runas b/runas new file mode 100755 index 0000000..5e9ad51 --- /dev/null +++ b/runas @@ -0,0 +1,94 @@ +#! /usr/bin/env bash +# +# usage: +# assumes bash, homebrew, and nvm for managing node + +## Load NVM +source $(brew --prefix nvm)/nvm.sh + +# CONSTANTS +REFRESH_LIMIT=28800 + +ENVIRON=${1:-default} +shift 1 + +# Retain 'my' prompt +export CLICOLOR=1 +export PS1="RUNAS{$ENVIRON} [\[\033[0;92m\]\D{%F}T\t\[\033[0m\]] \H:\W\[\033[0;91m\]$\[\033[0m\] " +RUN_ARGS=${*:-bash -i} + +if [ ! -d env ] ; then + echo "Creating 'env' directory" + mkdir env +fi + +## Remember next and current environment +NEXT_NODE=env/${ENVIRON} +CURR_NODE= +if [ -h node_modules ]; then + CURR_NODE=$(readlink -n node_modules) +fi + +# Setup "switch to" and "revert to" commands +SWITCH_CMD= +REVERT_CMD= +if [ "${NEXT_NODE}" != "${CURR_NODE}" ] ; then + echo "Preserving existing '${CURR_NODE}' ..." + SWITCH_CMD="ln -sfn ${NEXT_NODE} node_modules" + if [ ! -z "${CURR_NODE}" ] ; then + REVERT_CMD="ln -sfn ${CURR_NODE} node_modules" + else + rm -f node_modules + fi +fi + +## setp environment +RETCODE= + +nvm use ${ENVIRON} +RETCODE=$? +if [ 0 -ne ${RETCODE} ]; then + # environment doesn't exist! + exit ${RETCODE} +fi + +# create/update if needed +UPDATE_CMD="npm update -d" +UPDATE_TIME=$(date +%s) +if [ ! -d ${NEXT_NODE} ] ; then + echo "Creating '${NEXT_NODE}'" + mkdir -p ${NEXT_NODE} + UPDATE_CMD="npm install -d" +elif [ -f ${NEXT_NODE}/.updated ] ; then + UPDATE_LAST=$(cat ${NEXT_NODE}/.updated) + UPDATE_LAST=$(( ${UPDATE_LAST} + ${REFRESH_LIMIT} )) + if [ ${UPDATE_LAST} -gt ${UPDATE_TIME} ] ; then + UPDATE_CMD="true" + fi +fi + +if [ ! -z "${SWITCH_CMD}" ] ; then + ${SWITCH_CMD} +fi + +if [ ! -z "${UPDATE_CMD}" ] ; then + echo "updating '${NEXT_NODE}'" + ${UPDATE_CMD} &> /dev/null && \ + echo ${UPDATE_TIME} > ${NEXT_NODE}/.updated + RETCODE=$? +fi + +# run arguments +if [ 0 -eq ${RETCODE} ] ; then + echo "running '${RUN_ARGS}' under ${ENVIRON}" + ${RUN_ARGS} + RETCODE=$? +fi + +## if the environment was switched, revert it +if [ ! -z "${REVERT_CMD}" ]; then + echo "... reverting to '${CURR_NODE}'" + ${REVERT_CMD} +fi + +exit ${RETCODE} diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..7eeefc3 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/test/algorithms/aes-cbc-hmac-sha2-test.js b/test/algorithms/aes-cbc-hmac-sha2-test.js new file mode 100644 index 0000000..33cc4ee --- /dev/null +++ b/test/algorithms/aes-cbc-hmac-sha2-test.js @@ -0,0 +1,73 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"); + +describe("algorithms/aes-cbc-hmac-sha2", function() { + var vectors = [ + { + alg: "A128CBC-HS256", + desc: "RFC 7518 Appendix B.1. Test Cases for AEAD_AES_128_CBC_HMAC_SHA256", + key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + iv: "1af38c2dc2b96ffdd86694092341bc04", + plaintext: "41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365", + ciphertext: "c80edfa32ddf39d5ef00c0b468834279a2e46a1b8049f792f76bfe54b903a9c9a94ac9b47ad2655c5f10f9aef71427e2fc6f9b3f399a221489f16362c703233609d45ac69864e3321cf82935ac4096c86e133314c54019e8ca7980dfa4b9cf1b384c486f3a54c51078158ee5d79de59fbd34d848b3d69550a67646344427ade54b8851ffb598f7f80074b9473c82e2db", + aad: "546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673", + tag: "652c3fa36b0a7c5b3219fab3a30bc1c4" + }, + { + alg: "A256CBC-HS512", + desc: "RFC 7518 Appendix B.3. Test Cases for AEAD_AES_256_CBC_HMAC_SHA512", + key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + iv: "1af38c2dc2b96ffdd86694092341bc04", + plaintext: "41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365", + ciphertext: "4affaaadb78c31c5da4b1b590d10ffbd3dd8d5d302423526912da037ecbcc7bd822c301dd67c373bccb584ad3e9279c2e6d12a1374b77f077553df829410446b36ebd97066296ae6427ea75c2e0846a11a09ccf5370dc80bfecbad28c73f09b3a3b75e662a2594410ae496b2e2e6609e31e6e02cc837f053d21f37ff4f51950bbe2638d09dd7a4930930806d0703b1f6", + aad: "546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673", + tag: "4dd3b4c088a7f45c216839645b2012bf2e6269a8c56a816dbc1b267761955bc5" + } + ]; + vectors.forEach(function(v) { + var encrunner = function() { + var key = new Buffer(v.key, "hex"), + pdata = new Buffer(v.plaintext, "hex"), + cdata = new Buffer(v.ciphertext, "hex"), + mac = new Buffer(v.tag, "hex"), + props = { + iv: new Buffer(v.iv, "hex"), + aad: new Buffer(v.aad, "hex") + }; + + var promise = algorithms.encrypt(v.alg, key, pdata, props); + promise = promise.then(function(result) { + assert.equal(result.data.toString("hex"), cdata.toString("hex")); + assert.equal(result.tag.toString("hex"), mac.toString("hex")); + }); + return promise; + }; + var decrunner = function() { + var key = new Buffer(v.key, "hex"), + pdata = new Buffer(v.plaintext, "hex"), + cdata = new Buffer(v.ciphertext, "hex"), + props = { + iv: new Buffer(v.iv, "hex"), + aad: new Buffer(v.aad, "hex"), + tag: new Buffer(v.tag, "hex") + }; + + var promise = algorithms.decrypt(v.alg, key, cdata, props); + promise = promise.then(function(result) { + assert.equal(result.toString("hex"), pdata.toString("hex")); + }); + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ") encryption", encrunner); + it("performs " + v.alg + " (" + v.desc + ") decryption", decrunner); + }); +}); diff --git a/test/algorithms/aes-gcm-test.js b/test/algorithms/aes-gcm-test.js new file mode 100644 index 0000000..04d74ab --- /dev/null +++ b/test/algorithms/aes-gcm-test.js @@ -0,0 +1,352 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"); + +describe("algorithms/aes-gcm", function() { + function algSize(alg) { + return parseInt(/A(\d+)GCM/g.exec(alg)[1]) / 8; + } + var fails = [ + "A128GCM", + "A256GCM" + ]; + fails.forEach(function(alg, pos) { + var keyFailer = function(mode) { + var runner = function() { + var size = algSize(alg); + var key, + iv = new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5", "hex"), + tag = new Buffer("ffeeddccbbaa99887766554433221100", "hex"), + plaintext = new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex"); + + var promise = Promise.resolve(); + + promise = promise.then(function() { + // a bit too small + key = new Buffer(size - 1); + return algorithms[mode](alg, key, plaintext, { iv: iv, mac: tag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.equal(err.message, "invalid key size"); + }); + + promise = promise.then(function() { + // a bit too big + key = new Buffer(size + 1); + return algorithms[mode](alg, key, plaintext, { iv: iv, mac: tag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.equal(err.message, "invalid key size"); + }); + + promise = promise.then(function() { + // just right --- for another algorithm + var size = algSize(fails[(pos + 1) % fails.length]); + key = new Buffer(size); + return algorithms[mode](alg, key, plaintext, { iv: iv, mac: tag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.equal(err.message, "invalid key size"); + }); + + return promise; + }; + + it("checks for invalid keysize on " + mode + " " + alg, runner); + }; + var ivFailer = function(mode) { + var runner = function() { + var size = algSize(alg); + var key = new Buffer(size), + iv, + tag = new Buffer("ffeeddccbbaa99887766554433221100", "hex"), + plaintext = new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex"); + + var promise = Promise.resolve(); + + promise = promise.then(function() { + // a bit too small + iv = new Buffer(12 - 1); + return algorithms[mode](alg, key, plaintext, { iv: iv, mac: tag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.equal(err.message, "invalid iv"); + }); + + promise = promise.then(function() { + // a bit too big + iv = new Buffer(12 + 1); + return algorithms[mode](alg, key, plaintext, { iv: iv, mac: tag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.equal(err.message, "invalid iv"); + }); + + promise = promise.then(function() { + // outright missing + iv = undefined; + return algorithms[mode](alg, key, plaintext, { iv: iv, mac: tag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.equal(err.message, "invalid iv"); + }); + + return promise; + }; + + it("checks for invalid iv on " + mode + " " + alg, runner); + }; + var tagFailer = function() { + var runner = function() { + var size = algSize(alg); + var key = new Buffer(size), + iv = new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5", "hex"), + tag, + plaintext = new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex"); + + var promise = Promise.resolve(); + + promise = promise.then(function() { + // a bit too small + tag = new Buffer(16 - 1); + return algorithms.decrypt(alg, key, plaintext, { iv: iv, mac: tag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.equal(err.message, "invalid tag length"); + }); + + promise = promise.then(function() { + // a bit too big + tag = new Buffer(16 + 1); + return algorithms.decrypt(alg, key, plaintext, { iv: iv, mac: tag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.equal(err.message, "invalid tag length"); + }); + + promise = promise.then(function() { + // outright missing + tag = undefined; + return algorithms.decrypt(alg, key, plaintext, { iv: iv, mac: tag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.equal(err.message, "invalid tag length"); + }); + + return promise; + }; + + it("checks for invalid tag on decrypt " + alg, runner); + }; + var decryptFailer = function(){ + var runner = function() { + var size = algSize(alg); + var key = new Buffer(size, "binary"), + iv = new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5", "hex"), + tag, + plaintext = new Buffer("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", "hex"), + ciphertext; + + var promise = Promise.resolve(); + promise = promise.then(function() { + return algorithms.encrypt(alg, key, plaintext, { iv: iv }); + }); + promise = promise.then(function(result) { + ciphertext = result.data; + tag = result.tag; + + // corrupted tag + var badTag = new Buffer(tag.length); + tag.copy(badTag); + for (var idx = 0; idx < badTag.length; idx++) { + badTag[idx] = badTag[idx] ^ 0xa5; + } + return algorithms.decrypt(alg, key, ciphertext, { iv: iv, mac: badTag }); + }); + promise = promise.then(function() { + assert.ok(false, "expected error not thrown"); + }, function(err) { + assert.ok(!!err); + }); + + return promise; + }; + + it("checks for failed decryption on " + alg, runner); + }; + + keyFailer("encrypt"); + keyFailer("decrypt"); + ivFailer("encrypt"); + ivFailer("decrypt"); + tagFailer(); + decryptFailer(); + }); + + var vectors = [ + // 128-bit key, 0-bit AAD + { + alg: "A128GCM", + desc: "NIST-CAVP [128-bit key, 0-bit AAD, 96-bit IV, 128-bit TAG]", + key: "e98b72a9881a84ca6b76e0f43e68647a", + iv: "8b23299fde174053f3d652ba", + plaintext: "28286a321293253c3e0aa2704a278032", + ciphertext: "5a3c1cf1985dbb8bed818036fdd5ab42", + aad: "", + tag: "23c7ab0f952b7091cd324835043b5eb5" + }, + // 128-bit key, 128-bit AAD + { + alg: "A128GCM", + desc: "NIST-CAVP [128-bit key, 128-bit AAD, 96-bit IV, 128-bit TAG]", + key: "816e39070410cf2184904da03ea5075a", + iv: "32c367a3362613b27fc3e67e", + plaintext: "ecafe96c67a1646744f1c891f5e69427", + ciphertext: "552ebe012e7bcf90fcef712f8344e8f1", + aad: "f2a30728ed874ee02983c294435d3c16", + tag: "ecaae9fc68276a45ab0ca3cb9dd9539f" + }, + + /*! NOT SUPPORTED ON CHROME + // 192-bit key, 0-bit AAD + { + alg: "A192GCM", + desc: "NIST-CAVP [192-bit key, 0-bit AAD, 96-bit IV, 128-bit TAG]", + key: "7a7c5b6a8a9ab5acae34a9f6e41f19a971f9c330023c0f0c", + iv: "aa4c38bf587f94f99fee77d5", + plaintext: "99ae6f479b3004354ff18cd86c0b6efb", + ciphertext: "132ae95bd359c44aaefa6348632cafbd", + aad: "", + tag: "19d7c7d5809ad6648110f22f272e7d72" + }, + // 192-bit key, 128-bit AAD + { + alg: "A192GCM", + desc: "NIST-CAVP [192-bit key, 128-bit AAD, 96-bit IV, 128-bit TAG]", + key: "0c44d6c928ee112ce665fe547ebd387298a954b462f695d8", + iv: "18b8f320fef4ae8ccbe8f952", + plaintext: "96ad07f9b628b652cf86cb7317886f51", + ciphertext: "a664078133405eb9094d36f7e070191f", + aad: "7341d43f98cf388221180941970376e8", + tag: "e8f9c317847ce3f3c23994a402f06581" + }, + //*/ + + // 256-bit key, 0-bit AAD + { + alg: "A256GCM", + desc: "NIST-CAVP [256-bit key, 0-bit AAD, 96-bit IV, 128-bit TAG]", + key: "4c8ebfe1444ec1b2d503c6986659af2c94fafe945f72c1e8486a5acfedb8a0f8", + iv: "473360e0ad24889959858995", + plaintext: "7789b41cb3ee548814ca0b388c10b343", + ciphertext: "d2c78110ac7e8f107c0df0570bd7c90c", + aad: "", + tag: "c26a379b6d98ef2852ead8ce83a833a7" + }, + // 256-bit key, 128-bit AAD + { + alg: "A256GCM", + desc: "NIST-CAVP [256-bit key, 128-bit AAD, 96-bit IV, 128-bit TAG]", + key: "54e352ea1d84bfe64a1011096111fbe7668ad2203d902a01458c3bbd85bfce14", + iv: "df7c3bca00396d0c018495d9", + plaintext: "85fc3dfad9b5a8d3258e4fc44571bd3b", + ciphertext: "426e0efc693b7be1f3018db7ddbb7e4d", + aad: "7e968d71b50c1f11fd001f3fef49d045", + tag: "ee8257795be6a1164d7e1d2d6cac77a7" + } + ]; + vectors.forEach(function(v) { + var encrunner = function() { + var key = new Buffer(v.key, "hex"), + pdata = new Buffer(v.plaintext, "hex"), + cdata = new Buffer(v.ciphertext, "hex"), + mac = new Buffer(v.tag, "hex"), + props = { + iv: new Buffer(v.iv, "hex"), + aad: new Buffer(v.aad, "hex") + }; + + var promise = algorithms.encrypt(v.alg, key, pdata, props); + promise = promise.then(function(result) { + assert.equal(result.data.toString("binary"), cdata.toString("binary")); + assert.equal(result.tag.toString("binary"), mac.toString("binary")); + }); + return promise; + }; + var decrunner = function() { + var key = new Buffer(v.key, "hex"), + pdata = new Buffer(v.plaintext, "hex"), + cdata = new Buffer(v.ciphertext, "hex"), + props = { + iv: new Buffer(v.iv, "hex"), + aad: new Buffer(v.aad, "hex"), + tag: new Buffer(v.tag, "hex") + }; + + var promise = algorithms.decrypt(v.alg, key, cdata, props); + promise = promise.then(function(result) { + assert.equal(result.toString("binary"), pdata.toString("binary")); + }); + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ") encryption", encrunner); + it("performs " + v.alg + " (" + v.desc + ") decryption", decrunner); + }); + + it("performs consistently with large data", function() { + var key = new Buffer("00000000000000000000000000000000", "hex"), + iv = new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5", "hex"), + plaintext = new Buffer(1024 * 1024 + 1); + + for (var idx = 0; idx < plaintext.length; idx++) { + plaintext[idx] = idx % 256; + } + var promise = Promise.resolve(plaintext); + promise = promise.then(function(pdata) { + var props = { + iv: iv + }; + return algorithms.encrypt("A128GCM", key, pdata, props); + }); + promise = promise.then(function(result) { + var cdata = result.data, + tag = result.tag; + + var props = { + iv: iv, + tag: tag + }; + return algorithms.decrypt("A128GCM", key, cdata, props); + }); + promise = promise.then(function(result) { + assert.equal(result.toString("binary"), plaintext.toString("binary")); + }); + return promise; + }); +}); diff --git a/test/algorithms/aes-kw-test.js b/test/algorithms/aes-kw-test.js new file mode 100644 index 0000000..2e9c65d --- /dev/null +++ b/test/algorithms/aes-kw-test.js @@ -0,0 +1,71 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"); + +describe("algorithms/aes-kw", function() { + var vectors = [ + { + alg: "A128KW", + desc: "RFC3339 § 4.1 [Wrap 128 bits of Key Data with a 128-bit KEK]", + key: "000102030405060708090A0B0C0D0E0F", + plaintext: "00112233445566778899AABBCCDDEEFF", + ciphertext: "1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5" + }, + { + alg: "A256KW", + desc: "RFC3339 § 4.3 [Wrap 128 bits of Key Data with a 256-bit KEK]", + key: "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + plaintext: "00112233445566778899AABBCCDDEEFF", + ciphertext: "64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7" + }, + { + alg: "A256KW", + desc: "RFC3339 § 4.3 [Wrap 192 bits of Key Data with a 256-bit KEK]", + key: "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + plaintext: "00112233445566778899AABBCCDDEEFF0001020304050607", + ciphertext: "A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1" + }, + { + alg: "A256KW", + desc: "RFC3339 § 4.6 [Wrap 256 bits of Key Data with a 256-bit KEK]", + key: "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + plaintext: "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F", + ciphertext: "28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21" + } + ]; + + vectors.forEach(function(v) { + var encrunner = function() { + var key = new Buffer(v.key, "hex"), + pdata = new Buffer(v.plaintext, "hex"), + cdata = new Buffer(v.ciphertext, "hex"); + + var promise = algorithms.encrypt(v.alg, key, pdata); + promise = promise.then(function(result) { + assert.equal(result.data.toString("hex"), cdata.toString("hex")); + }); + return promise; + }; + var decrunner = function() { + var key = new Buffer(v.key, "hex"), + pdata = new Buffer(v.plaintext, "hex"), + cdata = new Buffer(v.ciphertext, "hex"); + + var promise = algorithms.decrypt(v.alg, key, cdata); + promise = promise.then(function(result) { + assert.equal(result.toString("hex"), pdata.toString("hex")); + }); + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ") encryption", encrunner); + it("performs " + v.alg + " (" + v.desc + ") decryption", decrunner); + }); +}); diff --git a/test/algorithms/concat-test.js b/test/algorithms/concat-test.js new file mode 100644 index 0000000..d377577 --- /dev/null +++ b/test/algorithms/concat-test.js @@ -0,0 +1,45 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"); + +describe("algorithms/concat", function() { + var vectors = [ + { + alg: "CONCAT-SHA-256", + desc: "NIST-CAVP SHA-256", + ikm: "003b84682ef996e462ac04fbf68d19a18fb74f869df87df24cdfcf21e2194784", + otherInfo: "434156536964a1b2c3d4e5f9b260f9b3a465922bb2191dd60c3c691912f7070c0fc2a47e2485963982fdb486dc626b", + keyLength: 16, + okm: "53272dad13352335bc2dc61c7227bdf7" + } + // TODO: tests for other SHA2 algorithms + ]; + + vectors.forEach(function(v) { + var deriverunner = function() { + var ikm = new Buffer(v.ikm, "hex"), + okm = new Buffer(v.okm, "hex"); + + var props = {}; + if (v.otherInfo) { + props.otherInfo = new Buffer(v.otherInfo, "hex"); + } + props.length = v.keyLength; + + var promise = algorithms.derive(v.alg, ikm, props); + promise = promise.then(function(result) { + assert.equal(result.toString("hex"), okm.toString("hex")); + }); + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ") derivation", deriverunner); + }); +}); diff --git a/test/algorithms/dir-test.js b/test/algorithms/dir-test.js new file mode 100644 index 0000000..62decf7 --- /dev/null +++ b/test/algorithms/dir-test.js @@ -0,0 +1,56 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"); + +describe("algorithms/dir", function() { + var vectors = [ + { + alg: "dir/128", + desc: "direct 128-bit key", + key: "e98b72a9881a84ca6b76e0f43e68647a" + }, + { + alg: "dir/192", + desc: "direct 192-bit key", + key: "7a7c5b6a8a9ab5acae34a9f6e41f19a971f9c330023c0f0c" + }, + { + alg: "dir/256", + desc: "direct 256-bit key", + key: "4c8ebfe1444ec1b2d503c6986659af2c94fafe945f72c1e8486a5acfedb8a0f8" + } + ]; + + vectors.forEach(function(v) { + var encRunner = function() { + var key = new Buffer(v.key, "hex"); + + var promise = algorithms.encrypt("dir", key); + promise = promise.then(function(result) { + assert.deepEqual(result.data, key); + assert.equal(result.once, true); + assert.equal(result.direct, true); + }); + return promise; + }; + var decRunner = function() { + var key = new Buffer(v.key, "hex"); + + var promise = algorithms.decrypt("dir", key); + promise = promise.then(function(result) { + assert.deepEqual(result, key); + }); + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ") encryption", encRunner); + it("performs " + v.alg + " (" + v.desc + ") decryption", decRunner); + }); +}); diff --git a/test/algorithms/ecdh-test.js b/test/algorithms/ecdh-test.js new file mode 100644 index 0000000..4cc2a78 --- /dev/null +++ b/test/algorithms/ecdh-test.js @@ -0,0 +1,476 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var omit = require("lodash.omit"), + chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"), + util = require("../../lib/util"); + +describe("algorithms/ecdh", function() { + var deriveVectors = [ + // ECDH Raw + { + alg: "ECDH", + desc: "NIST-CAVP KAS EC-SHA256 #2 [Raw ECDH P-256 to 256-bit secret]", + private: { + kty: "EC", + crv: "P-256", + d: util.base64url.decode("7zbcQ6Fdncg6rLODr7kf8LKLi3gG97w9TDLtb015yu8"), + x: util.base64url.decode("S5n2_0SoNTscyrut1NQGPCpwmWtvHxpZOwqJXkg_s4w"), + y: util.base64url.decode("SS7aA0CHkIeIJNWRDVG3TndlHgGDpBe1itliHNJf0t0") + }, + public: { + kty: "EC", + crv: "P-256", + x: util.base64url.decode("YHgubzHrfUD4iNc1-K0XqtZDtIdviLirkP9Px2sMnFU"), + y: util.base64url.decode("KNJTFg1mCIkpRtTF3a6OgBpjz272RiUtlfNB3vuAfrE") + }, + secret: util.base64url.decode("ADuEaC75luRirAT79o0ZoY-3T4ad-H3yTN_PIeIZR4Q"), + keyLength: 32 + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS EC-SHA256 #2 [Raw ECDH P-256 to 128-bit secret]", + private: { + kty: "EC", + crv: "P-256", + d: util.base64url.decode("7zbcQ6Fdncg6rLODr7kf8LKLi3gG97w9TDLtb015yu8"), + x: util.base64url.decode("S5n2_0SoNTscyrut1NQGPCpwmWtvHxpZOwqJXkg_s4w"), + y: util.base64url.decode("SS7aA0CHkIeIJNWRDVG3TndlHgGDpBe1itliHNJf0t0") + }, + public: { + kty: "EC", + crv: "P-256", + x: util.base64url.decode("YHgubzHrfUD4iNc1-K0XqtZDtIdviLirkP9Px2sMnFU"), + y: util.base64url.decode("KNJTFg1mCIkpRtTF3a6OgBpjz272RiUtlfNB3vuAfrE") + }, + secret: util.base64url.decode("ADuEaC75luRirAT79o0ZoQ"), + keyLength: 16 + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS EC-SHA256 #2 [Raw ECDH P-256 to implied 256-bit secret]", + private: { + kty: "EC", + crv: "P-256", + d: util.base64url.decode("7zbcQ6Fdncg6rLODr7kf8LKLi3gG97w9TDLtb015yu8"), + x: util.base64url.decode("S5n2_0SoNTscyrut1NQGPCpwmWtvHxpZOwqJXkg_s4w"), + y: util.base64url.decode("SS7aA0CHkIeIJNWRDVG3TndlHgGDpBe1itliHNJf0t0") + }, + public: { + kty: "EC", + crv: "P-256", + x: util.base64url.decode("YHgubzHrfUD4iNc1-K0XqtZDtIdviLirkP9Px2sMnFU"), + y: util.base64url.decode("KNJTFg1mCIkpRtTF3a6OgBpjz272RiUtlfNB3vuAfrE") + }, + secret: util.base64url.decode("ADuEaC75luRirAT79o0ZoY-3T4ad-H3yTN_PIeIZR4Q") + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS ED-SHA384 #1 [Raw ECDH P-384 to 384-bit secret]", + private: { + kty: "EC", + crv: "P-384", + d: util.base64url.decode("mFebnPmzDenxEVDpMEM5o_yJrCEX0UkJim1BKrICGuAanfJIjzZ4n5sgj1R780wa"), + x: util.base64url.decode("1NC6nwSAOFxtcLR9FSsx_Q0wLQ336ZP6kNpFqqRvI8Kdod6qdfOi2Ap21JYUWj3f"), + y: util.base64url.decode("LyrO6VSCuek3JCiNm3N3XgUCyiuBAi6fpZuotuInm6WBvnB8zWPUzLUKOkIOsu5z") + }, + public: { + kty: "EC", + crv: "P-384", + x: util.base64url.decode("JGFpvixNv7B1DVspsNQumCFjWBoFEXdn6N8qwZ0j2HMwAzqF8Ohn3OzCGSY9sSjx"), + y: util.base64url.decode("g9CSJXuFh6O30gZO3UqhScYp6D8sHixcpzZZlBBByyu2qjxfx5p6NeTHfjiCkEnx") + }, + secret: util.base64url.decode("uCwsJVPdOdkfXpQz-9motAgAXUEDxsztn3SKXdZPM9bwVDexXXIPH9PIRp_YDi20"), + keyLength: 48 + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS ED-SHA384 #1 [Raw ECDH P-384 to 256-bit secret]", + private: { + kty: "EC", + crv: "P-384", + d: util.base64url.decode("mFebnPmzDenxEVDpMEM5o_yJrCEX0UkJim1BKrICGuAanfJIjzZ4n5sgj1R780wa"), + x: util.base64url.decode("1NC6nwSAOFxtcLR9FSsx_Q0wLQ336ZP6kNpFqqRvI8Kdod6qdfOi2Ap21JYUWj3f"), + y: util.base64url.decode("LyrO6VSCuek3JCiNm3N3XgUCyiuBAi6fpZuotuInm6WBvnB8zWPUzLUKOkIOsu5z") + }, + public: { + kty: "EC", + crv: "P-384", + x: util.base64url.decode("JGFpvixNv7B1DVspsNQumCFjWBoFEXdn6N8qwZ0j2HMwAzqF8Ohn3OzCGSY9sSjx"), + y: util.base64url.decode("g9CSJXuFh6O30gZO3UqhScYp6D8sHixcpzZZlBBByyu2qjxfx5p6NeTHfjiCkEnx") + }, + secret: util.base64url.decode("uCwsJVPdOdkfXpQz-9motAgAXUEDxsztn3SKXdZPM9Y"), + keyLength: 32 + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS ED-SHA384 #1 [Raw ECDH P-384 to 128-bit secret]", + private: { + kty: "EC", + crv: "P-384", + d: util.base64url.decode("mFebnPmzDenxEVDpMEM5o_yJrCEX0UkJim1BKrICGuAanfJIjzZ4n5sgj1R780wa"), + x: util.base64url.decode("1NC6nwSAOFxtcLR9FSsx_Q0wLQ336ZP6kNpFqqRvI8Kdod6qdfOi2Ap21JYUWj3f"), + y: util.base64url.decode("LyrO6VSCuek3JCiNm3N3XgUCyiuBAi6fpZuotuInm6WBvnB8zWPUzLUKOkIOsu5z") + }, + public: { + kty: "EC", + crv: "P-384", + x: util.base64url.decode("JGFpvixNv7B1DVspsNQumCFjWBoFEXdn6N8qwZ0j2HMwAzqF8Ohn3OzCGSY9sSjx"), + y: util.base64url.decode("g9CSJXuFh6O30gZO3UqhScYp6D8sHixcpzZZlBBByyu2qjxfx5p6NeTHfjiCkEnx") + }, + secret: util.base64url.decode("uCwsJVPdOdkfXpQz-9motA"), + keyLength: 16 + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS ED-SHA384 #1 [Raw ECDH P-384 to implicit 384-bit secret]", + private: { + kty: "EC", + crv: "P-384", + d: util.base64url.decode("mFebnPmzDenxEVDpMEM5o_yJrCEX0UkJim1BKrICGuAanfJIjzZ4n5sgj1R780wa"), + x: util.base64url.decode("1NC6nwSAOFxtcLR9FSsx_Q0wLQ336ZP6kNpFqqRvI8Kdod6qdfOi2Ap21JYUWj3f"), + y: util.base64url.decode("LyrO6VSCuek3JCiNm3N3XgUCyiuBAi6fpZuotuInm6WBvnB8zWPUzLUKOkIOsu5z") + }, + public: { + kty: "EC", + crv: "P-384", + x: util.base64url.decode("JGFpvixNv7B1DVspsNQumCFjWBoFEXdn6N8qwZ0j2HMwAzqF8Ohn3OzCGSY9sSjx"), + y: util.base64url.decode("g9CSJXuFh6O30gZO3UqhScYp6D8sHixcpzZZlBBByyu2qjxfx5p6NeTHfjiCkEnx") + }, + secret: util.base64url.decode("uCwsJVPdOdkfXpQz-9motAgAXUEDxsztn3SKXdZPM9bwVDexXXIPH9PIRp_YDi20") + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS EE-SHA512 #3 [Raw ECDH P-521 to 512-bit secret]", + private: { + kty: "EC", + crv: "P-521", + d: util.base64url.decode("AV7JCqhoOnTnX1_T9KSFBlSkZJWbzBaTvkRlNp83SdpbKQS3JgTgPDNtMONaXjyjjk4ec3A5QFLDBBjVrpFEl1vf"), + x: util.base64url.decode("AaETIgjyjW-AXKXKOL_JkwtIgvus4-CQQPBKp5jqf8oDX2r4za7Uq-z6yic7ozGqpz0gtELGyJqU10YLxYK34wMp"), + y: util.base64url.decode("AXBbywfS_ivasfDsuNBbLgxRLnrWJNsHiT1fMivgsMHwYTJcz-8FKYvj1dvJN4bnOWvGvgsRds5u0WIofsTYKKuw") + }, + public: { + kty: "EC", + crv: "P-521", + x: util.base64url.decode("AHo_VxE69e2yc41ejRGgX52I-ZkwBno0x0QB2Japx135jHZ2x-68R_9TYFbE7unj4l7dOz3BznKVErErwTjUbieW"), + y: util.base64url.decode("AHTQuOcIhd6PJo2oyCgxo15xDWsZf6SkSlW5Vt2buNuwqnoErGTPpEU8ptcmUqDsKRKrO5uR6PGyPwDFY3LpKYKH") + }, + secret: util.base64url.decode("ABjsN13mS11qiAJqPR9qnoUMyN1bRdMLthuKNqVYHEoAUhQoYsSaONbmdbaKXWF6Evn-IxD7KZOdCU-4FlRWOA"), + keyLength: 64 + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS EE-SHA512 #3 [Raw ECDH P-521 to 256-bit secret]", + private: { + kty: "EC", + crv: "P-521", + d: util.base64url.decode("AV7JCqhoOnTnX1_T9KSFBlSkZJWbzBaTvkRlNp83SdpbKQS3JgTgPDNtMONaXjyjjk4ec3A5QFLDBBjVrpFEl1vf"), + x: util.base64url.decode("AaETIgjyjW-AXKXKOL_JkwtIgvus4-CQQPBKp5jqf8oDX2r4za7Uq-z6yic7ozGqpz0gtELGyJqU10YLxYK34wMp"), + y: util.base64url.decode("AXBbywfS_ivasfDsuNBbLgxRLnrWJNsHiT1fMivgsMHwYTJcz-8FKYvj1dvJN4bnOWvGvgsRds5u0WIofsTYKKuw") + }, + public: { + kty: "EC", + crv: "P-521", + x: util.base64url.decode("AHo_VxE69e2yc41ejRGgX52I-ZkwBno0x0QB2Japx135jHZ2x-68R_9TYFbE7unj4l7dOz3BznKVErErwTjUbieW"), + y: util.base64url.decode("AHTQuOcIhd6PJo2oyCgxo15xDWsZf6SkSlW5Vt2buNuwqnoErGTPpEU8ptcmUqDsKRKrO5uR6PGyPwDFY3LpKYKH") + }, + secret: util.base64url.decode("ABjsN13mS11qiAJqPR9qnoUMyN1bRdMLthuKNqVYHEo"), + keyLength: 32 + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS EE-SHA512 #3 [Raw ECDH P-521 to 128-bit secret]", + private: { + kty: "EC", + crv: "P-521", + d: util.base64url.decode("AV7JCqhoOnTnX1_T9KSFBlSkZJWbzBaTvkRlNp83SdpbKQS3JgTgPDNtMONaXjyjjk4ec3A5QFLDBBjVrpFEl1vf"), + x: util.base64url.decode("AaETIgjyjW-AXKXKOL_JkwtIgvus4-CQQPBKp5jqf8oDX2r4za7Uq-z6yic7ozGqpz0gtELGyJqU10YLxYK34wMp"), + y: util.base64url.decode("AXBbywfS_ivasfDsuNBbLgxRLnrWJNsHiT1fMivgsMHwYTJcz-8FKYvj1dvJN4bnOWvGvgsRds5u0WIofsTYKKuw") + }, + public: { + kty: "EC", + crv: "P-521", + x: util.base64url.decode("AHo_VxE69e2yc41ejRGgX52I-ZkwBno0x0QB2Japx135jHZ2x-68R_9TYFbE7unj4l7dOz3BznKVErErwTjUbieW"), + y: util.base64url.decode("AHTQuOcIhd6PJo2oyCgxo15xDWsZf6SkSlW5Vt2buNuwqnoErGTPpEU8ptcmUqDsKRKrO5uR6PGyPwDFY3LpKYKH") + }, + secret: util.base64url.decode("ABjsN13mS11qiAJqPR9qng"), + keyLength: 16 + }, + { + alg: "ECDH", + desc: "NIST-CAVP KAS EE-SHA512 #3 [Raw ECDH P-521 to implied 528-bit secret]", + private: { + kty: "EC", + crv: "P-521", + d: util.base64url.decode("AV7JCqhoOnTnX1_T9KSFBlSkZJWbzBaTvkRlNp83SdpbKQS3JgTgPDNtMONaXjyjjk4ec3A5QFLDBBjVrpFEl1vf"), + x: util.base64url.decode("AaETIgjyjW-AXKXKOL_JkwtIgvus4-CQQPBKp5jqf8oDX2r4za7Uq-z6yic7ozGqpz0gtELGyJqU10YLxYK34wMp"), + y: util.base64url.decode("AXBbywfS_ivasfDsuNBbLgxRLnrWJNsHiT1fMivgsMHwYTJcz-8FKYvj1dvJN4bnOWvGvgsRds5u0WIofsTYKKuw") + }, + public: { + kty: "EC", + crv: "P-521", + x: util.base64url.decode("AHo_VxE69e2yc41ejRGgX52I-ZkwBno0x0QB2Japx135jHZ2x-68R_9TYFbE7unj4l7dOz3BznKVErErwTjUbieW"), + y: util.base64url.decode("AHTQuOcIhd6PJo2oyCgxo15xDWsZf6SkSlW5Vt2buNuwqnoErGTPpEU8ptcmUqDsKRKrO5uR6PGyPwDFY3LpKYKH") + }, + secret: util.base64url.decode("ABjsN13mS11qiAJqPR9qnoUMyN1bRdMLthuKNqVYHEoAUhQoYsSaONbmdbaKXWF6Evn-IxD7KZOdCU-4FlRWOMh1") + }, + + // ECDH + Concat + { + alg: "ECDH-CONCAT", + desc: "NIST-CAVP KAS EC-SHA256 #2 [ECDH + Concat KDF P-256 to 128-bit secret]", + private: { + kty: "EC", + crv: "P-256", + d: util.base64url.decode("7zbcQ6Fdncg6rLODr7kf8LKLi3gG97w9TDLtb015yu8"), + x: util.base64url.decode("S5n2_0SoNTscyrut1NQGPCpwmWtvHxpZOwqJXkg_s4w"), + y: util.base64url.decode("SS7aA0CHkIeIJNWRDVG3TndlHgGDpBe1itliHNJf0t0") + }, + public: { + kty: "EC", + crv: "P-256", + x: util.base64url.decode("YHgubzHrfUD4iNc1-K0XqtZDtIdviLirkP9Px2sMnFU"), + y: util.base64url.decode("KNJTFg1mCIkpRtTF3a6OgBpjz272RiUtlfNB3vuAfrE") + }, + otherInfo: util.base64url.decode("Q0FWU2lkobLD1OX5smD5s6RlkiuyGR3WDDxpGRL3BwwPwqR-JIWWOYL9tIbcYms"), + secret: util.base64url.decode("UyctrRM1IzW8LcYccie99w"), + keyLength: 16 + }, + { + alg: "ECDH-CONCAT", + desc: "NIST-CAVP KAS ED-SHA384 #1 [ECDH + Concat P-384 to 192-bit secret]", + private: { + kty: "EC", + crv: "P-384", + d: util.base64url.decode("mFebnPmzDenxEVDpMEM5o_yJrCEX0UkJim1BKrICGuAanfJIjzZ4n5sgj1R780wa"), + x: util.base64url.decode("1NC6nwSAOFxtcLR9FSsx_Q0wLQ336ZP6kNpFqqRvI8Kdod6qdfOi2Ap21JYUWj3f"), + y: util.base64url.decode("LyrO6VSCuek3JCiNm3N3XgUCyiuBAi6fpZuotuInm6WBvnB8zWPUzLUKOkIOsu5z") + }, + public: { + kty: "EC", + crv: "P-384", + x: util.base64url.decode("JGFpvixNv7B1DVspsNQumCFjWBoFEXdn6N8qwZ0j2HMwAzqF8Ohn3OzCGSY9sSjx"), + y: util.base64url.decode("g9CSJXuFh6O30gZO3UqhScYp6D8sHixcpzZZlBBByyu2qjxfx5p6NeTHfjiCkEnx") + }, + otherInfo: util.base64url.decode("Q0FWU2lkobLD1OXjPitYKRnMHxfHmXEulECZpK3D3LYrcaA_TRXUiAVti2K33eo"), + secret: util.base64url.decode("CwsCnGSlB0mg_yRjf8Lif7y0-IOZhVDM"), + keyLength: 24 + }, + { + alg: "ECDH-CONCAT", + desc: "NIST-CAVP KAS EE-SHA512 #3 [ECDH + Concat P-521 to 256-bit secret]", + private: { + kty: "EC", + crv: "P-521", + d: util.base64url.decode("AV7JCqhoOnTnX1_T9KSFBlSkZJWbzBaTvkRlNp83SdpbKQS3JgTgPDNtMONaXjyjjk4ec3A5QFLDBBjVrpFEl1vf"), + x: util.base64url.decode("AaETIgjyjW-AXKXKOL_JkwtIgvus4-CQQPBKp5jqf8oDX2r4za7Uq-z6yic7ozGqpz0gtELGyJqU10YLxYK34wMp"), + y: util.base64url.decode("AXBbywfS_ivasfDsuNBbLgxRLnrWJNsHiT1fMivgsMHwYTJcz-8FKYvj1dvJN4bnOWvGvgsRds5u0WIofsTYKKuw") + }, + public: { + kty: "EC", + crv: "P-521", + x: util.base64url.decode("AHo_VxE69e2yc41ejRGgX52I-ZkwBno0x0QB2Japx135jHZ2x-68R_9TYFbE7unj4l7dOz3BznKVErErwTjUbieW"), + y: util.base64url.decode("AHTQuOcIhd6PJo2oyCgxo15xDWsZf6SkSlW5Vt2buNuwqnoErGTPpEU8ptcmUqDsKRKrO5uR6PGyPwDFY3LpKYKH") + }, + otherInfo: util.base64url.decode("Q0FWU2lkobLD1OVkvTMB9TCliA8WmP78okVMUh2bNRp_hIkxQGqb72z69Af2J0k"), + secret: util.base64url.decode("0ufS0xG13qfXkclzaWbtcrKzDMYTJY6jSpq8omaSCZA"), + keyLength: 32 + }, + + // ECDH + HKDF + { + alg: "ECDH-HKDF", + desc: "Constructed P-256 + HKDF to 256-bit secret", + private: { + kty: "EC", + crv: "P-256", + d: util.base64url.decode("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"), + x: util.base64url.decode("gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0"), + y: util.base64url.decode("SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps") + }, + public: { + kty: "EC", + crv: "P-256", + x: util.base64url.decode("weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ"), + y: util.base64url.decode("e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck") + }, + secret: util.base64url.decode("V7JQ34iR8BffRJir0sZoHuPjFoejiqb2U9_MoVLMVio"), + keyLength: 32 + }, + { + alg: "ECDH-HKDF", + desc: "Constructed P-256 + HKDF to 128-bit secret", + private: { + kty: "EC", + crv: "P-256", + d: util.base64url.decode("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"), + x: util.base64url.decode("gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0"), + y: util.base64url.decode("SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps") + }, + public: { + kty: "EC", + crv: "P-256", + x: util.base64url.decode("weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ"), + y: util.base64url.decode("e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck") + }, + secret: util.base64url.decode("V7JQ34iR8BffRJir0sZoHg"), + keyLength: 16 + }, + { + alg: "ECDH-HKDF", + desc: "Constructed P-256 + HKDF to implied 256-bit secret", + private: { + kty: "EC", + crv: "P-256", + d: util.base64url.decode("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"), + x: util.base64url.decode("gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0"), + y: util.base64url.decode("SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps") + }, + public: { + kty: "EC", + crv: "P-256", + x: util.base64url.decode("weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ"), + y: util.base64url.decode("e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck") + }, + secret: util.base64url.decode("V7JQ34iR8BffRJir0sZoHuPjFoejiqb2U9_MoVLMVio") + } + ]; + deriveVectors.forEach(function(v) { + var deriverunner = function() { + var pubKey = v.public, + privKey = v.private, + secret = v.secret, + keyLen = v.keyLength; + + var props = { + public: pubKey + }; + if (keyLen) { + props.length = keyLen; + } + if (v.otherInfo) { + props.otherInfo = v.otherInfo; + } + + var promise = algorithms.derive(v.alg, privKey, props); + promise = promise.then(function(result) { + assert.equal(result.toString("hex"), secret.toString("hex")); + }); + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ") derivation", deriverunner); + }); + + var encdecVectors = [ + { + alg: "ECDH-ES", + desc: "RFC 7518 Appendix C: Example ECDH-ES Key Agreement Computation", + local: { + "kty": "EC", + "crv": "P-256", + "x": util.base64url.decode("gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0"), + "y": util.base64url.decode("SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps"), + "d": util.base64url.decode("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo") + }, + remote: { + "kty": "EC", + "crv": "P-256", + "x": util.base64url.decode("weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ"), + "y": util.base64url.decode("e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck"), + "d": util.base64url.decode("VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw") + }, + enc: "A128GCM", + apu: util.base64url.decode("QWxpY2U"), + apv: util.base64url.decode("Qm9i"), + secret: util.base64url.decode("VqqN6vgjbSBcIijNcacQGg"), + direct: true, + once: true + }, + { + alg: "ECDH-ES+A128KW", + desc: "Pre-generated ECDH Ephemeral-Static with AES-128-KW Key Wrapping", + local: { + "kty": "EC", + "crv": "P-256", + "x": util.base64url.decode("QAc9AODpp43FtVs0OqmiAE7MI4mTnNZlQikDWVoWE3Y"), + "y": util.base64url.decode("t2fphG7NiaWMqSuc4DnyZtA7rBt5FjOhB-ZOUaF9KNQ"), + "d": util.base64url.decode("6rjahZxevjYQ7UmqSUEQ5fK8YVZ-QCQcVtbtC63xl3w") + }, + remote: { + "kty": "EC", + "crv": "P-256", + "x": util.base64url.decode("wE6lLi8DqbkYrJIhylztpu8OsTetXE4Q5tRAkFemtP8"), + "y": util.base64url.decode("ei1CSptDQkYtzT2ThVc_EO84SbYDI5ZWVX6mxmBzSvY"), + "d": util.base64url.decode("J1yGL-36TcYasGxBS3lSHjgB2yWnfnRAZ-BEcx5voxk") + }, + enc: "A128GCM", + apu: util.base64url.decode("QWxpY2U"), + apv: util.base64url.decode("Qm9i"), + cek: util.base64url.decode("XGBmqfGFXBSUyYQabuznow"), + secret: util.base64url.decode("EPc3n2hbJtUeSAyDhfAIbP7wKRsdE2Gn") + } + ]; + encdecVectors.forEach(function(v) { + var encrunner = function() { + var spk = omit(v.remote, "d"), + epk = v.local, + cek = v.cek, + secret = v.secret; + var props = { + alg: v.alg, + enc: v.enc, + epk: epk, + apu: v.apu, + apv: v.apv + }; + var promise = algorithms.encrypt(v.alg, spk, cek, props); + promise = promise.then(function(result) { + assert.equal(result.data.toString("hex"), secret.toString("hex")); + if ("direct" in v) { + assert.equal(result.direct, v.direct); + } else { + assert.ok(!("direct" in result)); + } + + if ("once" in v) { + assert.equal(result.once, v.once); + } else { + assert.ok(!("once" in result)); + } + }); + return promise; + }; + var decrunner = function() { + var epk = omit(v.remote, "d"), + spk = v.local, + cek = v.cek, + secret = v.secret; + var props = { + alg: v.alg, + enc: v.enc, + epk: epk, + apu: v.apu, + apv: v.apv + }; + var promise = algorithms.decrypt(v.alg, spk, secret, props); + promise = promise.then(function(result) { + if (!cek) { + assert.equal(result.toString("hex"), secret.toString("hex")); + } else { + assert.equal(result.toString("hex"), cek.toString("hex")); + } + }); + return promise; + }; + + it("performs " + v.alg + "(" + v.desc + ") key wrap", encrunner); + it("performs " + v.alg + "(" + v.desc + ") key unwrap", decrunner); + }); +}); diff --git a/test/algorithms/ecdsa-test.js b/test/algorithms/ecdsa-test.js new file mode 100644 index 0000000..47975e7 --- /dev/null +++ b/test/algorithms/ecdsa-test.js @@ -0,0 +1,82 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"), + util = require("../../lib/util"); + +describe("algorithms/ecdsa", function() { + var vectors = [ + { + alg: "ES256", + desc: "ECDSA using P-256 and SHA-256", + key: { + kty: "EC", + crv: "P-256", + x: util.base64url.decode("qE8xuhpJ1HqyRbLg1XR8Wz8Qe_8gU5zmVr9FZqYw6N4"), + y: util.base64url.decode("_fcR8gSgFQvBi2UoodFtFi61Fu_VQIbf0RjN33y6dzU"), + d: util.base64url.decode("ALqZCAHDK3jcCGYZOPcqvWIivb2ph46qBq0L1Mdu3Jc") + }, + msg: util.base64url.decode("d50euE-ihMN9KdXhedADBrQTxEqACgd-WYU_YwX5Lln7f4E") + }, + { + alg: "ES384", + desc: "ECDSA using P-384 and SHA-384", + key: { + kty: "EC", + crv: "P-384", + x: util.base64url.decode("DQgF9mGaYUrhMT5K1y5VZQG_1XAKSGj2G1EdI3aV95DYafNByFqOVeKjSYIFu_0u"), + y: util.base64url.decode("KaZKhidOZF_DhxuReE5_41Lpg1Hj8RTyTJlM1T_rJntmfCZzmUlOUa2coFERkEvc"), + d: util.base64url.decode("zyZc5_XuFQ4uHSwJfWwSI-Uzuay2e25c4G2OikNKHt9HdCkEGTe5PqeG4jYuEg5D") + }, + msg: util.base64url.decode("ISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISE") + }, + { + alg: "ES512", + desc: "ECDSA using P-521 and SHA-512", + key: { + kty: "EC", + crv: "P-521", + x: util.base64url.decode("Ad_BZ3QfGOrkR7UyFeyRnVxYyFm917v1Y5QfJdsTmSVoVqU7kbcZj23VhYyuyMqwRdVeeV7ihYYcl1EGc5Erbk4J"), + y: util.base64url.decode("AKW1CZRP2Wq3cskiWaSU1ci9m-D5EDifKkva3-4WrsBVqavKoBnx6_U42khxLAuclpwRkxIam49zQ_yE5eCUMf-O"), + d: util.base64url.decode("Ae9d4Om78X33TBerpl0Ik5vYXNrcj8kx5GWQ6A2oFTVGuZnLcw5r-CeGx-5qm58Klh8casqxaa9KCdVxMZ03kcPL") + }, + msg: util.base64url.decode("MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTE") + } + ]; + + vectors.forEach(function(v, idx) { + if (0 < idx) { + return; + } + // NOTE: The best we can really do is consistency checks + it("performs " + v.alg + " (" + v.desc + ") sign+verify consistency", function() { + var key = v.key, + msg = v.msg, + sig = null; + + var promise = Promise.resolve(); + promise = promise.then(function() { + return algorithms.sign(v.alg, key, msg); + }); + promise = promise.then(function(result) { + assert.ok(result.mac); + assert.deepEqual(result.data, msg); + sig = result.mac; + + return algorithms.verify(v.alg, key, result.data, result.mac); + }); + promise = promise.then(function(result) { + assert.ok(result.valid); + assert.deepEqual(result.data, msg); + assert.deepEqual(result.mac, sig); + }); + return promise; + }); + }); +}); diff --git a/test/algorithms/hkdf-test.js b/test/algorithms/hkdf-test.js new file mode 100644 index 0000000..27d0041 --- /dev/null +++ b/test/algorithms/hkdf-test.js @@ -0,0 +1,102 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"); + +describe("algorithms/hkdf", function() { + var vectors = [ + // SHA-256 + { + alg: "HKDF-SHA-256", + desc: "RFC 5869 Test Case 1", + salt: "000102030405060708090a0b0c", + info: "f0f1f2f3f4f5f6f7f8f9", + keyLength: 42, + ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + okm: "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865" + }, + { + alg: "HKDF-SHA-256", + desc: "RFC 5869 Test Case 2", + salt: "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + keyLength: 82, + ikm: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", + okm: "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87" + }, + { + alg: "HKDF-SHA-256", + desc: "RFC 5869 Test Case 3", + salt: "", + info: "", + keyLength: 42, + ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + okm: "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8" + }, + { + alg: "HKDF-SHA-1", + desc: "RFC 5869 Test Case 4", + salt: "000102030405060708090a0b0c", + info: "f0f1f2f3f4f5f6f7f8f9", + keyLength: 42, + ikm: "0b0b0b0b0b0b0b0b0b0b0b", + okm: "085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896" + }, + { + alg: "HKDF-SHA-1", + desc: "RFC 5869 Test Case 5", + salt: "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + keyLength: 82, + ikm: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", + okm: "0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4" + }, + { + alg: "HKDF-SHA-1", + desc: "RFC 5869 Test Case 6", + salt: "", + info: "", + keyLength: 42, + ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + okm: "0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918" + }, + { + alg: "HKDF-SHA-1", + desc: "RFC 5869 Test Case 7", + info: "", + keyLength: 42, + ikm: "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c", + okm: "2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48" + } + ]; + + vectors.forEach(function(v) { + var deriverunner = function() { + var ikm = new Buffer(v.ikm, "hex"), + okm = new Buffer(v.okm, "hex"); + + var props = {}; + if (v.salt) { + props.salt = new Buffer(v.salt, "hex"); + } + if (v.info) { + props.info = new Buffer(v.info, "hex"); + } + props.length = v.keyLength; + + var promise = algorithms.derive(v.alg, ikm, props); + promise = promise.then(function(result) { + assert.equal(result.toString("hex"), okm.toString("hex")); + }); + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ") derivation", deriverunner); + }); +}); diff --git a/test/algorithms/hmac-test.js b/test/algorithms/hmac-test.js new file mode 100644 index 0000000..b08f145 --- /dev/null +++ b/test/algorithms/hmac-test.js @@ -0,0 +1,100 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"); + +describe("algorithms/hmac", function() { + var vectors = [ + /* + // ### NOTE: NOT PART OF JOSE!!! + // SHA-1 + { + alg: "SHA-1", + desc: "RFC2202 test #1", + key: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + msg: "4869205468657265", // "Hi There" + mac: "b617318655057264e28bc0b6fb378c8ef146be00", + }, + { + alg: "SHA-1", + desc: "RFC2202 test #3", + key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + msg: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + mac: "125d7342b9ac11cd91a39af48aa17b4f63f175d3", + }, + { + alg: "SHA-1", + desc: "NIST-CAVP v11 test #105", + key: "895868f19695c1f5a26d8ae339c567e5ab43b0fcc8056050e9922ec53010f9ce", + msg: "883e6ca2b19ef54640bb8333f85a9380e17211f6ee3d1dc7dc8f0e7c5d67b73076c3eafc26b93bb248c406ceba5cb4a9bfc939f0a238e1559d0f4d84f87eb85975568050ec1fe13d3365033d405237ec92827dd8cd124b36a4fa89d4fb9de04f4d9f34864cf76f4ec8458168d265a5b02144e596b5f2e0d2b9f9cb54aeeeb67a", + mac: "374c88f4480f5e8aaa9f448b777557c50065e9ac", + }, + //*/ + + // SHA-256 + { + alg: "HS256", + desc: "NIST-CAVP v11 test #30", + key: "9779d9120642797f1747025d5b22b7ac607cab08e1758f2f3a46c8be1e25c53b8c6a8f58ffefa176", + msg: "b1689c2591eaf3c9e66070f8a77954ffb81749f1b00346f9dfe0b2ee905dcc288baf4a92de3f4001dd9f44c468c3d07d6c6ee82faceafc97c2fc0fc0601719d2dcd0aa2aec92d1b0ae933c65eb06a03c9c935c2bad0459810241347ab87e9f11adb30415424c6c7f5f22a003b8ab8de54f6ded0e3ab9245fa79568451dfa258e", + mac: "769f00d3e6a6cc1fb426a14a4f76c6462e6149726e0dee0ec0cf97a16605ac8b" + }, + + // SHA-384 + { + alg: "HS384", + desc: "NIST-CAVP v11 test #45", + key: "5eab0dfa27311260d7bddcf77112b23d8b42eb7a5d72a5a318e1ba7e7927f0079dbb701317b87a3340e156dbcee28ec3a8d9", + msg: "f41380123ccbec4c527b425652641191e90a17d45e2f6206cf01b5edbe932d41cc8a2405c3195617da2f420535eed422ac6040d9cd65314224f023f3ba730d19db9844c71c329c8d9d73d04d8c5f244aea80488292dc803e772402e72d2e9f1baba5a6004f0006d822b0b2d65e9e4a302dd4f776b47a972250051a701fab2b70", + mac: "7cf5a06156ad3de5405a5d261de90275f9bb36de45667f84d08fbcb308ca8f53a419b07deab3b5f8ea231c5b036f8875" + }, + + // SHA-512 + { + alg: "HS512", + desc: "NIST-CAVP v11 test #60", + key: "57c2eb677b5093b9e829ea4babb50bde55d0ad59fec34a618973802b2ad9b78e26b2045dda784df3ff90ae0f2cc51ce39cf54867320ac6f3ba2c6f0d72360480c96614ae66581f266c35fb79fd28774afd113fa5187eff9206d7cbe90dd8bf67c844e202", + msg: "2423dff48b312be864cb3490641f793d2b9fb68a7763b8e298c86f42245e4540eb01ae4d2d4500370b1886f23ca2cf9701704cad5bd21ba87b811daf7a854ea24a56565ced425b35e40e1acbebe03603e35dcf4a100e57218408a1d8dbcc3b99296cfea931efe3ebd8f719a6d9a15487b9ad67eafedf15559ca42445b0f9b42e", + mac: "33c511e9bc2307c62758df61125a980ee64cefebd90931cb91c13742d4714c06de4003faf3c41c06aefc638ad47b21906e6b104816b72de6269e045a1f4429d4" + } + ]; + + vectors.forEach(function(v) { + var signrunner = function() { + var key = new Buffer(v.key, "hex"), + msg = new Buffer(v.msg, "hex"), + mac = new Buffer(v.mac, "hex"); + + var promise = algorithms.sign(v.alg, key, msg); + promise = promise.then(function(result) { + assert.equal(result.data.toString("binary"), msg.toString("binary")); + assert.equal(result.mac.toString("binary"), mac.toString("binary")); + }); + + return promise; + }; + var vfyrunner = function() { + var key = new Buffer(v.key, "hex"), + msg = new Buffer(v.msg, "hex"), + mac = new Buffer(v.mac, "hex"); + + var promise = algorithms.verify(v.alg, key, msg, mac); + promise = promise.then(function(result) { + assert.equal(result.data.toString("binary"), msg.toString("binary")); + assert.equal(result.mac.toString("binary"), mac.toString("binary")); + assert.equal(result.valid, true); + }); + + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ")" + " generation", signrunner); + it("performs " + v.alg + " (" + v.desc + ")" + " verification", vfyrunner); + }); +}); diff --git a/test/algorithms/pbes2-test.js b/test/algorithms/pbes2-test.js new file mode 100644 index 0000000..73f3e61 --- /dev/null +++ b/test/algorithms/pbes2-test.js @@ -0,0 +1,73 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"), + util = require("../../lib/util"); + +describe("algorithms/pbes2", function() { + var vectors = [ + { + alg: "PBES2-HS256+A128KW", + desc: "Password-Based Encryption using HMAC-SHA-256 and AES-128-KW", + password: util.base64url.decode("ZW50cmFwX2_igJNwZXRlcl9sb25n4oCTY3JlZGl0X3R1bg"), + salt: util.base64url.decode("8Q1SzinasR3xchYz6ZZcHA"), + iterations: 8192, + plaintext: util.base64url.decode("pqampqampqampqampqampg"), + ciphertext: util.base64url.decode("walEclCCkwmSDoXll-vLE0DRuOCfXc3N") + }, + /* NOTE: 192-bit AES not supported universally + { + alg: "PBES2-HS384+A192KW", + desc: "Password-Based Encryption using HMAC-SHA-384 and AES-192-KW", + password: util.base64url.decode("cGFzc3dvcmQ"), + salt: util.base64url.decode("c2FsdA"), + iterations: 1, + plaintext: util.base64url.decode("pqampqampqampqampqampg"), + ciphertext: util.base64url.decode("8dZ5AQ31AOhKRUwQ0l3-SxDCpqCnXlmz") + }, + //*/ + { + alg: "PBES2-HS512+A256KW", + desc: "Password-Based Encryption using HMAC-SHA-512 and AES-256-KW", + password: util.base64url.decode("ZW50cmFwX2_igJNwZXRlcl9sb25n4oCTY3JlZGl0X3R1bg"), + salt: util.base64url.decode("8Q1SzinasR3xchYz6ZZcHA"), + iterations: 8192, + plaintext: util.base64url.decode("pqampqampqampqampqampqampqampqampqampqampqY"), + ciphertext: util.base64url.decode("KPq3dK9i8YFo6moWueJlgJ1XAQKRM4u_P1UaQyMs1C8VNTTyDLe9Lw") + } + ]; + + vectors.forEach(function(v) { + var key = v.password, + props = { + p2s: v.salt, + p2c: v.iterations + }, + pdata = v.plaintext, + cdata = v.ciphertext; + + var encrunner = function() { + var promise = algorithms.encrypt(v.alg, key, pdata, props); + promise = promise.then(function(result) { + assert.equal(result.data.toString("hex"), cdata.toString("hex")); + }); + return promise; + }; + var decrunner = function() { + var promise = algorithms.decrypt(v.alg, key, cdata, props); + promise = promise.then(function(result) { + assert.equal(result.toString("hex"), pdata.toString("hex")); + }); + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ") encryption", encrunner); + it("performs " + v.alg + " (" + v.desc + ") decryption", decrunner); + }); +}); diff --git a/test/algorithms/rsaes-test.js b/test/algorithms/rsaes-test.js new file mode 100644 index 0000000..b5e9e2e --- /dev/null +++ b/test/algorithms/rsaes-test.js @@ -0,0 +1,89 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"), + util = require("../../lib/util"); + +describe("algorithms/rsaes", function() { + var vectors = [ + // 2048-bit; RSAES-PKCS-V1_5 + { + alg: "RSA1_5", + desc: "RSAES-PKCS-V1_5", + key: { + "kty": "RSA", + "n": util.base64url.decode("tHxy-fcpeVkvyncpt0xiMRvmKkpLzvd58AFpjHc4fUAqPgIGB7W4TBwTLWFB86p70ZwWYAcZtlxDS5xIfTFSh5sQp9Fw6DK45Jsjqho4WSkx88vrHtHSgIYepyqN88TjOMZnMpArZe1MaWt8jMLq2zia8V07XZx8j33iIKB_cSUcyNaiFYX9xthxiMJT4fAP8mer0N2GMhQnZcucltV9aHN-aMs0srsFpoiWtGFVmW72wF5EAJ-lMConfiImADGnyO6kuhRSsz39_3u3Q7kpcj757-cmCp4pUKn4vROCt3v-Rj_OHmNG5hONtWUeTg7IxAmZtRX_D3OA7eCjzLrb9w"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("SBOa5vApg-h2CWjlI-pBHFOD60eYVqLF827c89d4m6xQMksklVegreRYVDsO13wxzleDJ_4t6oGV7lAPMs_LoZPvZtVhPZlj9QdvirLF5fVpmW7KCpjIc8Mb4q4_2iW6iCXTeIHSkvXdGgxuxNfiaoGEfvc4if3AUJ14_Iab3lbBrsoGcDOgXQZcv2oZbpt4o4NlsesFymrNnUIJbVyQ73nErfjxZOBjJmS8IIeqHNkWM-1cGnWYc4oMBbeVkHq7ZPwGsDg44g9pP9K_ukFeb9umxXwg_NwknLCn2PoPLGu9MoHolyweYU-EDeKtZlLKoKIAnO3tS-HKVS20TrN0YQ"), + "p": util.base64url.decode("_pDg8_8vC80BGbZBUXUZSCPwZmKPAwK_XXOZdvyxEgPsMFJB0wEJeDI1vp6cC_gb-nlsQ9QuvrQJK2tSZ42fiwnVcdgImp7s6g3diVS35UwlnfRtaPCQMoyUGY0Ysr-455kgEmgjFX6JSO41ghun2tRv9NHtk4zTPEhMrcAu9-U"), + "q": util.base64url.decode("tYC8i1xkykvIdTo82BWKYA4LgcoCsISAkf7gRPykOJFhHeM6FHEoYJbLZ7QTW1lhMojgZM6Px_6qyjrDRRQoRz1m8eSFd7v3wq_i94n4sN4MTgDYtj6-VBynYBdF0KXX4z42CzR2rhWXmOKCGCM4KjH9jyTTQq8zAd6BcKpNzqs"), + "dp": util.base64url.decode("8sfrsuixzrhij0oRu4VJalLUSGFA8WciaRcBysgue_b_wAoDOyDnDhocxcJxIr0qudQp2_q15iy__gfp3FbmTO1BAsU9V3Gwk3xLx1jj1aysx5tA6W9cpskJyeCWKIvO5hpUyxlENJCsj8CXiZGkoYAvkjbQNQN-xiRR9PewE70"), + "dq": util.base64url.decode("pbvG7q5QbpSil8C0_E83CpzojvwqVoq3aBjHKtdTEUBW4NazGyV0zDYFyE0be8dyxJVN6V7g1atKwtzDn9lXKi38SZb09K9T_pdi9cwrpT0tGTEWsds7Kkz73PeDTZGSP7N33-VpFW8r_XOffXDzgTwin0nuCq82MVe-9GTeJX8"), + "qi": util.base64url.decode("v9JNqwr4j8nUn3hqFxip2vsn6E0SVvUe29y0LvysXdYjeI3mCAEzZoymycjZ8DPkR9VeKshIMJS92a_Fr4njq98HGjqKJ_NtLgCLglQtiW_NDZvdH930hn80qCSe_6wgP3ZVVAh054MzcxcCoFp1KaOalahf2OW8t9I6eRDF0OQ") + }, + msg: util.base64url.decode("d50euE-ihMN9KdXhedADBrQTxEqACgd-WYU_YwX5Lln7f4E") + }, + // 2048-bit; RSAES-OAEP; SHA-1 + { + alg: "RSA-OAEP", + desc: "RSA-OAEP using SHA-1 for Hash and MGF1", + key: { + "kty": "RSA", + "n": util.base64url.decode("tHxy-fcpeVkvyncpt0xiMRvmKkpLzvd58AFpjHc4fUAqPgIGB7W4TBwTLWFB86p70ZwWYAcZtlxDS5xIfTFSh5sQp9Fw6DK45Jsjqho4WSkx88vrHtHSgIYepyqN88TjOMZnMpArZe1MaWt8jMLq2zia8V07XZx8j33iIKB_cSUcyNaiFYX9xthxiMJT4fAP8mer0N2GMhQnZcucltV9aHN-aMs0srsFpoiWtGFVmW72wF5EAJ-lMConfiImADGnyO6kuhRSsz39_3u3Q7kpcj757-cmCp4pUKn4vROCt3v-Rj_OHmNG5hONtWUeTg7IxAmZtRX_D3OA7eCjzLrb9w"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("SBOa5vApg-h2CWjlI-pBHFOD60eYVqLF827c89d4m6xQMksklVegreRYVDsO13wxzleDJ_4t6oGV7lAPMs_LoZPvZtVhPZlj9QdvirLF5fVpmW7KCpjIc8Mb4q4_2iW6iCXTeIHSkvXdGgxuxNfiaoGEfvc4if3AUJ14_Iab3lbBrsoGcDOgXQZcv2oZbpt4o4NlsesFymrNnUIJbVyQ73nErfjxZOBjJmS8IIeqHNkWM-1cGnWYc4oMBbeVkHq7ZPwGsDg44g9pP9K_ukFeb9umxXwg_NwknLCn2PoPLGu9MoHolyweYU-EDeKtZlLKoKIAnO3tS-HKVS20TrN0YQ"), + "p": util.base64url.decode("_pDg8_8vC80BGbZBUXUZSCPwZmKPAwK_XXOZdvyxEgPsMFJB0wEJeDI1vp6cC_gb-nlsQ9QuvrQJK2tSZ42fiwnVcdgImp7s6g3diVS35UwlnfRtaPCQMoyUGY0Ysr-455kgEmgjFX6JSO41ghun2tRv9NHtk4zTPEhMrcAu9-U"), + "q": util.base64url.decode("tYC8i1xkykvIdTo82BWKYA4LgcoCsISAkf7gRPykOJFhHeM6FHEoYJbLZ7QTW1lhMojgZM6Px_6qyjrDRRQoRz1m8eSFd7v3wq_i94n4sN4MTgDYtj6-VBynYBdF0KXX4z42CzR2rhWXmOKCGCM4KjH9jyTTQq8zAd6BcKpNzqs"), + "dp": util.base64url.decode("8sfrsuixzrhij0oRu4VJalLUSGFA8WciaRcBysgue_b_wAoDOyDnDhocxcJxIr0qudQp2_q15iy__gfp3FbmTO1BAsU9V3Gwk3xLx1jj1aysx5tA6W9cpskJyeCWKIvO5hpUyxlENJCsj8CXiZGkoYAvkjbQNQN-xiRR9PewE70"), + "dq": util.base64url.decode("pbvG7q5QbpSil8C0_E83CpzojvwqVoq3aBjHKtdTEUBW4NazGyV0zDYFyE0be8dyxJVN6V7g1atKwtzDn9lXKi38SZb09K9T_pdi9cwrpT0tGTEWsds7Kkz73PeDTZGSP7N33-VpFW8r_XOffXDzgTwin0nuCq82MVe-9GTeJX8"), + "qi": util.base64url.decode("v9JNqwr4j8nUn3hqFxip2vsn6E0SVvUe29y0LvysXdYjeI3mCAEzZoymycjZ8DPkR9VeKshIMJS92a_Fr4njq98HGjqKJ_NtLgCLglQtiW_NDZvdH930hn80qCSe_6wgP3ZVVAh054MzcxcCoFp1KaOalahf2OW8t9I6eRDF0OQ") + }, + msg: util.base64url.decode("d50euE-ihMN9KdXhedADBrQTxEqACgd-WYU_YwX5Lln7f4E") + }, + // 2048-bit; RSAES-OAEP; SHA-256 + { + alg: "RSA-OAEP-256", + desc: "RSA-OAEP using SHA-256 for Hash and MGF1", + key: { + "kty": "RSA", + "n": util.base64url.decode("tHxy-fcpeVkvyncpt0xiMRvmKkpLzvd58AFpjHc4fUAqPgIGB7W4TBwTLWFB86p70ZwWYAcZtlxDS5xIfTFSh5sQp9Fw6DK45Jsjqho4WSkx88vrHtHSgIYepyqN88TjOMZnMpArZe1MaWt8jMLq2zia8V07XZx8j33iIKB_cSUcyNaiFYX9xthxiMJT4fAP8mer0N2GMhQnZcucltV9aHN-aMs0srsFpoiWtGFVmW72wF5EAJ-lMConfiImADGnyO6kuhRSsz39_3u3Q7kpcj757-cmCp4pUKn4vROCt3v-Rj_OHmNG5hONtWUeTg7IxAmZtRX_D3OA7eCjzLrb9w"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("SBOa5vApg-h2CWjlI-pBHFOD60eYVqLF827c89d4m6xQMksklVegreRYVDsO13wxzleDJ_4t6oGV7lAPMs_LoZPvZtVhPZlj9QdvirLF5fVpmW7KCpjIc8Mb4q4_2iW6iCXTeIHSkvXdGgxuxNfiaoGEfvc4if3AUJ14_Iab3lbBrsoGcDOgXQZcv2oZbpt4o4NlsesFymrNnUIJbVyQ73nErfjxZOBjJmS8IIeqHNkWM-1cGnWYc4oMBbeVkHq7ZPwGsDg44g9pP9K_ukFeb9umxXwg_NwknLCn2PoPLGu9MoHolyweYU-EDeKtZlLKoKIAnO3tS-HKVS20TrN0YQ"), + "p": util.base64url.decode("_pDg8_8vC80BGbZBUXUZSCPwZmKPAwK_XXOZdvyxEgPsMFJB0wEJeDI1vp6cC_gb-nlsQ9QuvrQJK2tSZ42fiwnVcdgImp7s6g3diVS35UwlnfRtaPCQMoyUGY0Ysr-455kgEmgjFX6JSO41ghun2tRv9NHtk4zTPEhMrcAu9-U"), + "q": util.base64url.decode("tYC8i1xkykvIdTo82BWKYA4LgcoCsISAkf7gRPykOJFhHeM6FHEoYJbLZ7QTW1lhMojgZM6Px_6qyjrDRRQoRz1m8eSFd7v3wq_i94n4sN4MTgDYtj6-VBynYBdF0KXX4z42CzR2rhWXmOKCGCM4KjH9jyTTQq8zAd6BcKpNzqs"), + "dp": util.base64url.decode("8sfrsuixzrhij0oRu4VJalLUSGFA8WciaRcBysgue_b_wAoDOyDnDhocxcJxIr0qudQp2_q15iy__gfp3FbmTO1BAsU9V3Gwk3xLx1jj1aysx5tA6W9cpskJyeCWKIvO5hpUyxlENJCsj8CXiZGkoYAvkjbQNQN-xiRR9PewE70"), + "dq": util.base64url.decode("pbvG7q5QbpSil8C0_E83CpzojvwqVoq3aBjHKtdTEUBW4NazGyV0zDYFyE0be8dyxJVN6V7g1atKwtzDn9lXKi38SZb09K9T_pdi9cwrpT0tGTEWsds7Kkz73PeDTZGSP7N33-VpFW8r_XOffXDzgTwin0nuCq82MVe-9GTeJX8"), + "qi": util.base64url.decode("v9JNqwr4j8nUn3hqFxip2vsn6E0SVvUe29y0LvysXdYjeI3mCAEzZoymycjZ8DPkR9VeKshIMJS92a_Fr4njq98HGjqKJ_NtLgCLglQtiW_NDZvdH930hn80qCSe_6wgP3ZVVAh054MzcxcCoFp1KaOalahf2OW8t9I6eRDF0OQ") + }, + msg: util.base64url.decode("ISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISE") + } + ]; + + vectors.forEach(function(v) { + // NOTE: The best we can really do is consistency checks + it("performs " + v.alg + " (" + v.desc + ") encrypt+decrypt consistency", function() { + var key = v.key, + msg = v.msg; + + var promise = Promise.resolve(); + promise = promise.then(function() { + return algorithms.encrypt(v.alg, key, msg); + }); + promise = promise.then(function(result) { + assert.ok(result.data); + + return algorithms.decrypt(v.alg, key, result.data); + }); + promise = promise.then(function(result) { + assert.deepEqual(result, v.msg); + }); + return promise; + }); + }); +}); diff --git a/test/algorithms/rsassa-test.js b/test/algorithms/rsassa-test.js new file mode 100644 index 0000000..2b97f61 --- /dev/null +++ b/test/algorithms/rsassa-test.js @@ -0,0 +1,140 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"), + util = require("../../lib/util"); + +describe("algorithms/rsassa", function() { + var vectors = [ + // 2048-bit; RSASSA-PKCS-V1_5; SHA-256 + { + alg: "RS256", + desc: "RSASSA-PKCS1-V1_5 with SHA-256", + key: { + "kty": "RSA", + "n": util.base64url.decode("tHxy-fcpeVkvyncpt0xiMRvmKkpLzvd58AFpjHc4fUAqPgIGB7W4TBwTLWFB86p70ZwWYAcZtlxDS5xIfTFSh5sQp9Fw6DK45Jsjqho4WSkx88vrHtHSgIYepyqN88TjOMZnMpArZe1MaWt8jMLq2zia8V07XZx8j33iIKB_cSUcyNaiFYX9xthxiMJT4fAP8mer0N2GMhQnZcucltV9aHN-aMs0srsFpoiWtGFVmW72wF5EAJ-lMConfiImADGnyO6kuhRSsz39_3u3Q7kpcj757-cmCp4pUKn4vROCt3v-Rj_OHmNG5hONtWUeTg7IxAmZtRX_D3OA7eCjzLrb9w"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("SBOa5vApg-h2CWjlI-pBHFOD60eYVqLF827c89d4m6xQMksklVegreRYVDsO13wxzleDJ_4t6oGV7lAPMs_LoZPvZtVhPZlj9QdvirLF5fVpmW7KCpjIc8Mb4q4_2iW6iCXTeIHSkvXdGgxuxNfiaoGEfvc4if3AUJ14_Iab3lbBrsoGcDOgXQZcv2oZbpt4o4NlsesFymrNnUIJbVyQ73nErfjxZOBjJmS8IIeqHNkWM-1cGnWYc4oMBbeVkHq7ZPwGsDg44g9pP9K_ukFeb9umxXwg_NwknLCn2PoPLGu9MoHolyweYU-EDeKtZlLKoKIAnO3tS-HKVS20TrN0YQ"), + "p": util.base64url.decode("_pDg8_8vC80BGbZBUXUZSCPwZmKPAwK_XXOZdvyxEgPsMFJB0wEJeDI1vp6cC_gb-nlsQ9QuvrQJK2tSZ42fiwnVcdgImp7s6g3diVS35UwlnfRtaPCQMoyUGY0Ysr-455kgEmgjFX6JSO41ghun2tRv9NHtk4zTPEhMrcAu9-U"), + "q": util.base64url.decode("tYC8i1xkykvIdTo82BWKYA4LgcoCsISAkf7gRPykOJFhHeM6FHEoYJbLZ7QTW1lhMojgZM6Px_6qyjrDRRQoRz1m8eSFd7v3wq_i94n4sN4MTgDYtj6-VBynYBdF0KXX4z42CzR2rhWXmOKCGCM4KjH9jyTTQq8zAd6BcKpNzqs"), + "dp": util.base64url.decode("8sfrsuixzrhij0oRu4VJalLUSGFA8WciaRcBysgue_b_wAoDOyDnDhocxcJxIr0qudQp2_q15iy__gfp3FbmTO1BAsU9V3Gwk3xLx1jj1aysx5tA6W9cpskJyeCWKIvO5hpUyxlENJCsj8CXiZGkoYAvkjbQNQN-xiRR9PewE70"), + "dq": util.base64url.decode("pbvG7q5QbpSil8C0_E83CpzojvwqVoq3aBjHKtdTEUBW4NazGyV0zDYFyE0be8dyxJVN6V7g1atKwtzDn9lXKi38SZb09K9T_pdi9cwrpT0tGTEWsds7Kkz73PeDTZGSP7N33-VpFW8r_XOffXDzgTwin0nuCq82MVe-9GTeJX8"), + "qi": util.base64url.decode("v9JNqwr4j8nUn3hqFxip2vsn6E0SVvUe29y0LvysXdYjeI3mCAEzZoymycjZ8DPkR9VeKshIMJS92a_Fr4njq98HGjqKJ_NtLgCLglQtiW_NDZvdH930hn80qCSe_6wgP3ZVVAh054MzcxcCoFp1KaOalahf2OW8t9I6eRDF0OQ") + }, + msg: util.base64url.decode("d50euE-ihMN9KdXhedADBrQTxEqACgd-WYU_YwX5Lln7f4E") + }, + { + alg: "RS384", + desc: "RSASSA-PKCS1-V_5 with SHA-384", + key: { + "kty": "RSA", + "n": util.base64url.decode("tHxy-fcpeVkvyncpt0xiMRvmKkpLzvd58AFpjHc4fUAqPgIGB7W4TBwTLWFB86p70ZwWYAcZtlxDS5xIfTFSh5sQp9Fw6DK45Jsjqho4WSkx88vrHtHSgIYepyqN88TjOMZnMpArZe1MaWt8jMLq2zia8V07XZx8j33iIKB_cSUcyNaiFYX9xthxiMJT4fAP8mer0N2GMhQnZcucltV9aHN-aMs0srsFpoiWtGFVmW72wF5EAJ-lMConfiImADGnyO6kuhRSsz39_3u3Q7kpcj757-cmCp4pUKn4vROCt3v-Rj_OHmNG5hONtWUeTg7IxAmZtRX_D3OA7eCjzLrb9w"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("SBOa5vApg-h2CWjlI-pBHFOD60eYVqLF827c89d4m6xQMksklVegreRYVDsO13wxzleDJ_4t6oGV7lAPMs_LoZPvZtVhPZlj9QdvirLF5fVpmW7KCpjIc8Mb4q4_2iW6iCXTeIHSkvXdGgxuxNfiaoGEfvc4if3AUJ14_Iab3lbBrsoGcDOgXQZcv2oZbpt4o4NlsesFymrNnUIJbVyQ73nErfjxZOBjJmS8IIeqHNkWM-1cGnWYc4oMBbeVkHq7ZPwGsDg44g9pP9K_ukFeb9umxXwg_NwknLCn2PoPLGu9MoHolyweYU-EDeKtZlLKoKIAnO3tS-HKVS20TrN0YQ"), + "p": util.base64url.decode("_pDg8_8vC80BGbZBUXUZSCPwZmKPAwK_XXOZdvyxEgPsMFJB0wEJeDI1vp6cC_gb-nlsQ9QuvrQJK2tSZ42fiwnVcdgImp7s6g3diVS35UwlnfRtaPCQMoyUGY0Ysr-455kgEmgjFX6JSO41ghun2tRv9NHtk4zTPEhMrcAu9-U"), + "q": util.base64url.decode("tYC8i1xkykvIdTo82BWKYA4LgcoCsISAkf7gRPykOJFhHeM6FHEoYJbLZ7QTW1lhMojgZM6Px_6qyjrDRRQoRz1m8eSFd7v3wq_i94n4sN4MTgDYtj6-VBynYBdF0KXX4z42CzR2rhWXmOKCGCM4KjH9jyTTQq8zAd6BcKpNzqs"), + "dp": util.base64url.decode("8sfrsuixzrhij0oRu4VJalLUSGFA8WciaRcBysgue_b_wAoDOyDnDhocxcJxIr0qudQp2_q15iy__gfp3FbmTO1BAsU9V3Gwk3xLx1jj1aysx5tA6W9cpskJyeCWKIvO5hpUyxlENJCsj8CXiZGkoYAvkjbQNQN-xiRR9PewE70"), + "dq": util.base64url.decode("pbvG7q5QbpSil8C0_E83CpzojvwqVoq3aBjHKtdTEUBW4NazGyV0zDYFyE0be8dyxJVN6V7g1atKwtzDn9lXKi38SZb09K9T_pdi9cwrpT0tGTEWsds7Kkz73PeDTZGSP7N33-VpFW8r_XOffXDzgTwin0nuCq82MVe-9GTeJX8"), + "qi": util.base64url.decode("v9JNqwr4j8nUn3hqFxip2vsn6E0SVvUe29y0LvysXdYjeI3mCAEzZoymycjZ8DPkR9VeKshIMJS92a_Fr4njq98HGjqKJ_NtLgCLglQtiW_NDZvdH930hn80qCSe_6wgP3ZVVAh054MzcxcCoFp1KaOalahf2OW8t9I6eRDF0OQ") + }, + msg: util.base64url.decode("ISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISE") + }, + { + alg: "RS512", + desc: "RSASSA-PKCS1-V_5 with SHA-512", + key: { + "kty": "RSA", + "n": util.base64url.decode("tHxy-fcpeVkvyncpt0xiMRvmKkpLzvd58AFpjHc4fUAqPgIGB7W4TBwTLWFB86p70ZwWYAcZtlxDS5xIfTFSh5sQp9Fw6DK45Jsjqho4WSkx88vrHtHSgIYepyqN88TjOMZnMpArZe1MaWt8jMLq2zia8V07XZx8j33iIKB_cSUcyNaiFYX9xthxiMJT4fAP8mer0N2GMhQnZcucltV9aHN-aMs0srsFpoiWtGFVmW72wF5EAJ-lMConfiImADGnyO6kuhRSsz39_3u3Q7kpcj757-cmCp4pUKn4vROCt3v-Rj_OHmNG5hONtWUeTg7IxAmZtRX_D3OA7eCjzLrb9w"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("SBOa5vApg-h2CWjlI-pBHFOD60eYVqLF827c89d4m6xQMksklVegreRYVDsO13wxzleDJ_4t6oGV7lAPMs_LoZPvZtVhPZlj9QdvirLF5fVpmW7KCpjIc8Mb4q4_2iW6iCXTeIHSkvXdGgxuxNfiaoGEfvc4if3AUJ14_Iab3lbBrsoGcDOgXQZcv2oZbpt4o4NlsesFymrNnUIJbVyQ73nErfjxZOBjJmS8IIeqHNkWM-1cGnWYc4oMBbeVkHq7ZPwGsDg44g9pP9K_ukFeb9umxXwg_NwknLCn2PoPLGu9MoHolyweYU-EDeKtZlLKoKIAnO3tS-HKVS20TrN0YQ"), + "p": util.base64url.decode("_pDg8_8vC80BGbZBUXUZSCPwZmKPAwK_XXOZdvyxEgPsMFJB0wEJeDI1vp6cC_gb-nlsQ9QuvrQJK2tSZ42fiwnVcdgImp7s6g3diVS35UwlnfRtaPCQMoyUGY0Ysr-455kgEmgjFX6JSO41ghun2tRv9NHtk4zTPEhMrcAu9-U"), + "q": util.base64url.decode("tYC8i1xkykvIdTo82BWKYA4LgcoCsISAkf7gRPykOJFhHeM6FHEoYJbLZ7QTW1lhMojgZM6Px_6qyjrDRRQoRz1m8eSFd7v3wq_i94n4sN4MTgDYtj6-VBynYBdF0KXX4z42CzR2rhWXmOKCGCM4KjH9jyTTQq8zAd6BcKpNzqs"), + "dp": util.base64url.decode("8sfrsuixzrhij0oRu4VJalLUSGFA8WciaRcBysgue_b_wAoDOyDnDhocxcJxIr0qudQp2_q15iy__gfp3FbmTO1BAsU9V3Gwk3xLx1jj1aysx5tA6W9cpskJyeCWKIvO5hpUyxlENJCsj8CXiZGkoYAvkjbQNQN-xiRR9PewE70"), + "dq": util.base64url.decode("pbvG7q5QbpSil8C0_E83CpzojvwqVoq3aBjHKtdTEUBW4NazGyV0zDYFyE0be8dyxJVN6V7g1atKwtzDn9lXKi38SZb09K9T_pdi9cwrpT0tGTEWsds7Kkz73PeDTZGSP7N33-VpFW8r_XOffXDzgTwin0nuCq82MVe-9GTeJX8"), + "qi": util.base64url.decode("v9JNqwr4j8nUn3hqFxip2vsn6E0SVvUe29y0LvysXdYjeI3mCAEzZoymycjZ8DPkR9VeKshIMJS92a_Fr4njq98HGjqKJ_NtLgCLglQtiW_NDZvdH930hn80qCSe_6wgP3ZVVAh054MzcxcCoFp1KaOalahf2OW8t9I6eRDF0OQ") + }, + msg: util.base64url.decode("MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTE") + }, + { + alg: "PS256", + desc: "RSASSA-PKCS1-PSS with SHA-256", + key: { + "kty": "RSA", + "n": util.base64url.decode("tHxy-fcpeVkvyncpt0xiMRvmKkpLzvd58AFpjHc4fUAqPgIGB7W4TBwTLWFB86p70ZwWYAcZtlxDS5xIfTFSh5sQp9Fw6DK45Jsjqho4WSkx88vrHtHSgIYepyqN88TjOMZnMpArZe1MaWt8jMLq2zia8V07XZx8j33iIKB_cSUcyNaiFYX9xthxiMJT4fAP8mer0N2GMhQnZcucltV9aHN-aMs0srsFpoiWtGFVmW72wF5EAJ-lMConfiImADGnyO6kuhRSsz39_3u3Q7kpcj757-cmCp4pUKn4vROCt3v-Rj_OHmNG5hONtWUeTg7IxAmZtRX_D3OA7eCjzLrb9w"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("SBOa5vApg-h2CWjlI-pBHFOD60eYVqLF827c89d4m6xQMksklVegreRYVDsO13wxzleDJ_4t6oGV7lAPMs_LoZPvZtVhPZlj9QdvirLF5fVpmW7KCpjIc8Mb4q4_2iW6iCXTeIHSkvXdGgxuxNfiaoGEfvc4if3AUJ14_Iab3lbBrsoGcDOgXQZcv2oZbpt4o4NlsesFymrNnUIJbVyQ73nErfjxZOBjJmS8IIeqHNkWM-1cGnWYc4oMBbeVkHq7ZPwGsDg44g9pP9K_ukFeb9umxXwg_NwknLCn2PoPLGu9MoHolyweYU-EDeKtZlLKoKIAnO3tS-HKVS20TrN0YQ"), + "p": util.base64url.decode("_pDg8_8vC80BGbZBUXUZSCPwZmKPAwK_XXOZdvyxEgPsMFJB0wEJeDI1vp6cC_gb-nlsQ9QuvrQJK2tSZ42fiwnVcdgImp7s6g3diVS35UwlnfRtaPCQMoyUGY0Ysr-455kgEmgjFX6JSO41ghun2tRv9NHtk4zTPEhMrcAu9-U"), + "q": util.base64url.decode("tYC8i1xkykvIdTo82BWKYA4LgcoCsISAkf7gRPykOJFhHeM6FHEoYJbLZ7QTW1lhMojgZM6Px_6qyjrDRRQoRz1m8eSFd7v3wq_i94n4sN4MTgDYtj6-VBynYBdF0KXX4z42CzR2rhWXmOKCGCM4KjH9jyTTQq8zAd6BcKpNzqs"), + "dp": util.base64url.decode("8sfrsuixzrhij0oRu4VJalLUSGFA8WciaRcBysgue_b_wAoDOyDnDhocxcJxIr0qudQp2_q15iy__gfp3FbmTO1BAsU9V3Gwk3xLx1jj1aysx5tA6W9cpskJyeCWKIvO5hpUyxlENJCsj8CXiZGkoYAvkjbQNQN-xiRR9PewE70"), + "dq": util.base64url.decode("pbvG7q5QbpSil8C0_E83CpzojvwqVoq3aBjHKtdTEUBW4NazGyV0zDYFyE0be8dyxJVN6V7g1atKwtzDn9lXKi38SZb09K9T_pdi9cwrpT0tGTEWsds7Kkz73PeDTZGSP7N33-VpFW8r_XOffXDzgTwin0nuCq82MVe-9GTeJX8"), + "qi": util.base64url.decode("v9JNqwr4j8nUn3hqFxip2vsn6E0SVvUe29y0LvysXdYjeI3mCAEzZoymycjZ8DPkR9VeKshIMJS92a_Fr4njq98HGjqKJ_NtLgCLglQtiW_NDZvdH930hn80qCSe_6wgP3ZVVAh054MzcxcCoFp1KaOalahf2OW8t9I6eRDF0OQ") + }, + msg: util.base64url.decode("d50euE-ihMN9KdXhedADBrQTxEqACgd-WYU_YwX5Lln7f4E") + }, + { + alg: "PS384", + desc: "RSASSA-PKCS1-PSS with SHA-384", + key: { + "kty": "RSA", + "n": util.base64url.decode("tHxy-fcpeVkvyncpt0xiMRvmKkpLzvd58AFpjHc4fUAqPgIGB7W4TBwTLWFB86p70ZwWYAcZtlxDS5xIfTFSh5sQp9Fw6DK45Jsjqho4WSkx88vrHtHSgIYepyqN88TjOMZnMpArZe1MaWt8jMLq2zia8V07XZx8j33iIKB_cSUcyNaiFYX9xthxiMJT4fAP8mer0N2GMhQnZcucltV9aHN-aMs0srsFpoiWtGFVmW72wF5EAJ-lMConfiImADGnyO6kuhRSsz39_3u3Q7kpcj757-cmCp4pUKn4vROCt3v-Rj_OHmNG5hONtWUeTg7IxAmZtRX_D3OA7eCjzLrb9w"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("SBOa5vApg-h2CWjlI-pBHFOD60eYVqLF827c89d4m6xQMksklVegreRYVDsO13wxzleDJ_4t6oGV7lAPMs_LoZPvZtVhPZlj9QdvirLF5fVpmW7KCpjIc8Mb4q4_2iW6iCXTeIHSkvXdGgxuxNfiaoGEfvc4if3AUJ14_Iab3lbBrsoGcDOgXQZcv2oZbpt4o4NlsesFymrNnUIJbVyQ73nErfjxZOBjJmS8IIeqHNkWM-1cGnWYc4oMBbeVkHq7ZPwGsDg44g9pP9K_ukFeb9umxXwg_NwknLCn2PoPLGu9MoHolyweYU-EDeKtZlLKoKIAnO3tS-HKVS20TrN0YQ"), + "p": util.base64url.decode("_pDg8_8vC80BGbZBUXUZSCPwZmKPAwK_XXOZdvyxEgPsMFJB0wEJeDI1vp6cC_gb-nlsQ9QuvrQJK2tSZ42fiwnVcdgImp7s6g3diVS35UwlnfRtaPCQMoyUGY0Ysr-455kgEmgjFX6JSO41ghun2tRv9NHtk4zTPEhMrcAu9-U"), + "q": util.base64url.decode("tYC8i1xkykvIdTo82BWKYA4LgcoCsISAkf7gRPykOJFhHeM6FHEoYJbLZ7QTW1lhMojgZM6Px_6qyjrDRRQoRz1m8eSFd7v3wq_i94n4sN4MTgDYtj6-VBynYBdF0KXX4z42CzR2rhWXmOKCGCM4KjH9jyTTQq8zAd6BcKpNzqs"), + "dp": util.base64url.decode("8sfrsuixzrhij0oRu4VJalLUSGFA8WciaRcBysgue_b_wAoDOyDnDhocxcJxIr0qudQp2_q15iy__gfp3FbmTO1BAsU9V3Gwk3xLx1jj1aysx5tA6W9cpskJyeCWKIvO5hpUyxlENJCsj8CXiZGkoYAvkjbQNQN-xiRR9PewE70"), + "dq": util.base64url.decode("pbvG7q5QbpSil8C0_E83CpzojvwqVoq3aBjHKtdTEUBW4NazGyV0zDYFyE0be8dyxJVN6V7g1atKwtzDn9lXKi38SZb09K9T_pdi9cwrpT0tGTEWsds7Kkz73PeDTZGSP7N33-VpFW8r_XOffXDzgTwin0nuCq82MVe-9GTeJX8"), + "qi": util.base64url.decode("v9JNqwr4j8nUn3hqFxip2vsn6E0SVvUe29y0LvysXdYjeI3mCAEzZoymycjZ8DPkR9VeKshIMJS92a_Fr4njq98HGjqKJ_NtLgCLglQtiW_NDZvdH930hn80qCSe_6wgP3ZVVAh054MzcxcCoFp1KaOalahf2OW8t9I6eRDF0OQ") + }, + msg: util.base64url.decode("ISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISE") + }, + { + alg: "PS512", + desc: "RSASSA-PKCS1-PSS with SHA-512", + key: { + "kty": "RSA", + "n": util.base64url.decode("tHxy-fcpeVkvyncpt0xiMRvmKkpLzvd58AFpjHc4fUAqPgIGB7W4TBwTLWFB86p70ZwWYAcZtlxDS5xIfTFSh5sQp9Fw6DK45Jsjqho4WSkx88vrHtHSgIYepyqN88TjOMZnMpArZe1MaWt8jMLq2zia8V07XZx8j33iIKB_cSUcyNaiFYX9xthxiMJT4fAP8mer0N2GMhQnZcucltV9aHN-aMs0srsFpoiWtGFVmW72wF5EAJ-lMConfiImADGnyO6kuhRSsz39_3u3Q7kpcj757-cmCp4pUKn4vROCt3v-Rj_OHmNG5hONtWUeTg7IxAmZtRX_D3OA7eCjzLrb9w"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("SBOa5vApg-h2CWjlI-pBHFOD60eYVqLF827c89d4m6xQMksklVegreRYVDsO13wxzleDJ_4t6oGV7lAPMs_LoZPvZtVhPZlj9QdvirLF5fVpmW7KCpjIc8Mb4q4_2iW6iCXTeIHSkvXdGgxuxNfiaoGEfvc4if3AUJ14_Iab3lbBrsoGcDOgXQZcv2oZbpt4o4NlsesFymrNnUIJbVyQ73nErfjxZOBjJmS8IIeqHNkWM-1cGnWYc4oMBbeVkHq7ZPwGsDg44g9pP9K_ukFeb9umxXwg_NwknLCn2PoPLGu9MoHolyweYU-EDeKtZlLKoKIAnO3tS-HKVS20TrN0YQ"), + "p": util.base64url.decode("_pDg8_8vC80BGbZBUXUZSCPwZmKPAwK_XXOZdvyxEgPsMFJB0wEJeDI1vp6cC_gb-nlsQ9QuvrQJK2tSZ42fiwnVcdgImp7s6g3diVS35UwlnfRtaPCQMoyUGY0Ysr-455kgEmgjFX6JSO41ghun2tRv9NHtk4zTPEhMrcAu9-U"), + "q": util.base64url.decode("tYC8i1xkykvIdTo82BWKYA4LgcoCsISAkf7gRPykOJFhHeM6FHEoYJbLZ7QTW1lhMojgZM6Px_6qyjrDRRQoRz1m8eSFd7v3wq_i94n4sN4MTgDYtj6-VBynYBdF0KXX4z42CzR2rhWXmOKCGCM4KjH9jyTTQq8zAd6BcKpNzqs"), + "dp": util.base64url.decode("8sfrsuixzrhij0oRu4VJalLUSGFA8WciaRcBysgue_b_wAoDOyDnDhocxcJxIr0qudQp2_q15iy__gfp3FbmTO1BAsU9V3Gwk3xLx1jj1aysx5tA6W9cpskJyeCWKIvO5hpUyxlENJCsj8CXiZGkoYAvkjbQNQN-xiRR9PewE70"), + "dq": util.base64url.decode("pbvG7q5QbpSil8C0_E83CpzojvwqVoq3aBjHKtdTEUBW4NazGyV0zDYFyE0be8dyxJVN6V7g1atKwtzDn9lXKi38SZb09K9T_pdi9cwrpT0tGTEWsds7Kkz73PeDTZGSP7N33-VpFW8r_XOffXDzgTwin0nuCq82MVe-9GTeJX8"), + "qi": util.base64url.decode("v9JNqwr4j8nUn3hqFxip2vsn6E0SVvUe29y0LvysXdYjeI3mCAEzZoymycjZ8DPkR9VeKshIMJS92a_Fr4njq98HGjqKJ_NtLgCLglQtiW_NDZvdH930hn80qCSe_6wgP3ZVVAh054MzcxcCoFp1KaOalahf2OW8t9I6eRDF0OQ") + }, + msg: util.base64url.decode("MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTE") + } + ]; + + vectors.forEach(function(v) { + // NOTE: The best we can really do is consistency checks + it("performs " + v.alg + " (" + v.desc + ") sign+verify consistency", function() { + var key = v.key, + msg = v.msg, + sig = null; + + var promise = Promise.resolve(); + promise = promise.then(function() { + return algorithms.sign(v.alg, key, msg); + }); + promise = promise.then(function(result) { + assert.ok(result.mac); + assert.deepEqual(result.data, msg); + sig = result.mac; + + return algorithms.verify(v.alg, key, result.data, result.mac); + }); + promise = promise.then(function(result) { + assert.ok(result.valid); + assert.deepEqual(result.data, msg); + assert.deepEqual(result.mac, sig); + }); + return promise; + }); + }); +}); diff --git a/test/algorithms/sha-test.js b/test/algorithms/sha-test.js new file mode 100644 index 0000000..ef045ee --- /dev/null +++ b/test/algorithms/sha-test.js @@ -0,0 +1,105 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var algorithms = require("../../lib/algorithms/"); + +describe("algorithms/sha", function() { + var vectors = [ + // SHA-1 + { + alg: "SHA-1", + desc: "NIST CAVS SHA-1 80-bit message", + data: "9777cf90dd7c7e863506", + digest: "05c915b5ed4e4c4afffc202961f3174371e90b5c" + }, + { + alg: "SHA-1", + desc: "NIST CAVS SHA-1 160-bit message", + data: "63a3cc83fd1ec1b6680e9974a0514e1a9ecebb6a", + digest: "8bb8c0d815a9c68a1d2910f39d942603d807fbcc" + }, + { + alg: "SHA-1", + desc: "NIST CAVS SHA-1 256-bit message", + data: "0321794b739418c24e7c2e565274791c4be749752ad234ed56cb0a6347430c6b", + digest: "b89962c94d60f6a332fd60f6f07d4f032a586b76" + }, + { + alg: "SHA-1", + desc: "NIST CAVS SHA-1 384-bit message", + data: "57e89659d878f360af6de45a9a5e372ef40c384988e82640a3d5e4b76d2ef181780b9a099ac06ef0f8a7f3f764209720", + digest: "f652f3b1549f16710c7402895911e2b86a9b2aee" + }, + { + alg: "SHA-1", + desc: "NIST CAVS SHA-1 512-bit message", + data: "45927e32ddf801caf35e18e7b5078b7f5435278212ec6bb99df884f49b327c6486feae46ba187dc1cc9145121e1492e6b06e9007394dc33b7748f86ac3207cfe", + digest: "a70cfbfe7563dd0e665c7c6715a96a8d756950c0" + }, + { + alg: "SHA-1", + desc: "NIST CAVS SHA-1 2096-bit message", + data: "6cb70d19c096200f9249d2dbc04299b0085eb068257560be3a307dbd741a3378ebfa03fcca610883b07f7fea563a866571822472dade8a0bec4b98202d47a344312976a7bcb3964427eacb5b0525db22066599b81be41e5adaf157d925fac04b06eb6e01deb753babf33be16162b214e8db017212fafa512cdc8c0d0a15c10f632e8f4f47792c64d3f026004d173df50cf0aa7976066a79a8d78deeeec951dab7cc90f68d16f786671feba0b7d269d92941c4f02f432aa5ce2aab6194dcc6fd3ae36c8433274ef6b1bd0d314636be47ba38d1948343a38bf9406523a0b2a8cd78ed6266ee3c9b5c60620b308cc6b3a73c6060d5268a7d82b6a33b93a6fd6fe1de55231d12c97", + digest: "4a75a406f4de5f9e1132069d66717fc424376388" + }, + // SHA-256 + { + alg: "SHA-256", + desc: "NIST CAVS SHA-256 80-bit message", + data: "74cb9381d89f5aa73368", + digest: "73d6fad1caaa75b43b21733561fd3958bdc555194a037c2addec19dc2d7a52bd" + }, + { + alg: "SHA-256", + desc: "NIST CAVS SHA-256 160-bit message", + data: "c1ef39cee58e78f6fcdc12e058b7f902acd1a93b", + digest: "6dd52b0d8b48cc8146cebd0216fbf5f6ef7eeafc0ff2ff9d1422d6345555a142" + }, + { + alg: "SHA-256", + desc: "NIST CAVS SHA-256 256-bit message", + data: "09fc1accc230a205e4a208e64a8f204291f581a12756392da4b8c0cf5ef02b95", + digest: "4f44c1c7fbebb6f9601829f3897bfd650c56fa07844be76489076356ac1886a4" + }, + { + alg: "SHA-256", + desc: "NIST CAVS SHA-256 384-bit message", + data: "4eef5107459bddf8f24fc7656fd4896da8711db50400c0164847f692b886ce8d7f4d67395090b3534efd7b0d298da34b", + digest: "7c5d14ed83dab875ac25ce7feed6ef837d58e79dc601fb3c1fca48d4464e8b83" + }, + { + alg: "SHA-256", + desc: "NIST CAVS SHA-256 512-bit message", + data: "5a86b737eaea8ee976a0a24da63e7ed7eefad18a101c1211e2b3650c5187c2a8a650547208251f6d4237e661c7bf4c77f335390394c37fa1a9f9be836ac28509", + digest: "42e61e174fbb3897d6dd6cef3dd2802fe67b331953b06114a65c772859dfc1aa" + }, + { + alg: "SHA-256", + desc: "NIST CAVS SHA-256 2096-bit message", + data: "6b918fb1a5ad1f9c5e5dbdf10a93a9c8f6bca89f37e79c9fe12a57227941b173ac79d8d440cde8c64c4ebc84a4c803d198a296f3de060900cc427f58ca6ec373084f95dd6c7c427ecfbf781f68be572a88dbcbb188581ab200bfb99a3a816407e7dd6dd21003554d4f7a99c93ebfce5c302ff0e11f26f83fe669acefb0c1bbb8b1e909bd14aa48ba3445c88b0e1190eef765ad898ab8ca2fe507015f1578f10dce3c11a55fb9434ee6e9ad6cc0fdc4684447a9b3b156b908646360f24fec2d8fa69e2c93db78708fcd2eef743dcb9353819b8d667c48ed54cd436fb1476598c4a1d7028e6f2ff50751db36ab6bc32435152a00abd3d58d9a8770d9a3e52d5a3628ae3c9e0325", + digest: "46500b6ae1ab40bde097ef168b0f3199049b55545a1588792d39d594f493dca7" + } + ]; + + vectors.forEach(function(v) { + var runner = function() { + var data = new Buffer(v.data, "hex"), + expected = new Buffer(v.digest, "hex"); + + var promise = algorithms.digest(v.alg, data); + promise = promise.then(function(result) { + assert.deepEqual(result, expected); + }); + + return promise; + }; + + it("performs " + v.alg + " (" + v.desc + ") digest", runner); + }); +}); diff --git a/test/index-test.js b/test/index-test.js new file mode 100644 index 0000000..6d5abd8 --- /dev/null +++ b/test/index-test.js @@ -0,0 +1,70 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +if (typeof Promise === "undefined") { + require("es6-promise").polyfill(); +} + +var chai = require("chai"); +var assert = chai.assert; + +var jose = require("../"); + +describe("Public API", function() { + it("exports JWK", function() { + var JWK = jose.JWK; + + assert.ok(JWK); + assert.ok(JWK.isKey); + assert.ok(JWK.asKey); + assert.ok(JWK.isKeyStore); + assert.ok(JWK.asKeyStore); + assert.ok(JWK.createKeyStore); + + assert.equal(JWK.MODE_SIGN, "sign"); + assert.equal(JWK.MODE_VERIFY, "verify"); + assert.equal(JWK.MODE_ENCRYPT, "encrypt"); + assert.equal(JWK.MODE_DECRYPT, "decrypt"); + assert.equal(JWK.MODE_WRAP, "wrap"); + assert.equal(JWK.MODE_UNWRAP, "unwrap"); + }); + it("exports JWS", function() { + var JWS = jose.JWS; + + assert.ok(JWS); + assert.ok(JWS.createSign); + assert.ok(JWS.createVerify); + }); + it("exports JWE", function() { + var JWE = jose.JWE; + + assert.ok(JWE); + assert.ok(JWE.createEncrypt); + assert.ok(JWE.createDecrypt); + }); + it("exports JWA", function() { + var JWA = jose.JWA; + + assert.ok(JWA); + assert.ok(JWA.digest); + assert.ok(JWA.encrypt); + assert.ok(JWA.decrypt); + assert.ok(JWA.sign); + assert.ok(JWA.verify); + }); + + it("exports util", function() { + var util = jose.util; + + assert.ok(util); + assert.ok(util.base64url); + assert.ok(util.base64url.decode); + assert.ok(util.base64url.encode); + assert.ok(util.utf8); + assert.ok(util.utf8.decode); + assert.ok(util.utf8.encode); + }); +}); diff --git a/test/jwe/jwe-test.js b/test/jwe/jwe-test.js new file mode 100644 index 0000000..f4761b8 --- /dev/null +++ b/test/jwe/jwe-test.js @@ -0,0 +1,208 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var cloneDeep = require("lodash.clonedeep"); +var forEach = require("lodash.foreach"); +var chai = require("chai"); + +var JWE = require("../../lib/jwe"), + JWK = require("../../lib/jwk"), + util = require("../../lib/util"); + +var assert = chai.assert; + +var fixtures = { + "5_2.key_encryption_using_rsa-oaep_with_aes-gcm": cloneDeep(require("jose-cookbook/jwe/5_2.key_encryption_using_rsa-oaep_with_aes-gcm.json")), + "5_3.key_wrap_using_pbes2-aes-keywrap_with-aes-cbc-hmac-sha2": cloneDeep(require("jose-cookbook/jwe/5_3.key_wrap_using_pbes2-aes-keywrap_with-aes-cbc-hmac-sha2.json")), + "5_4.key_agreement_with_key_wrapping_using_ecdh-es_and_aes-keywrap_with_aes-gcm": cloneDeep(require("jose-cookbook/jwe/5_4.key_agreement_with_key_wrapping_using_ecdh-es_and_aes-keywrap_with_aes-gcm.json")), + "5_6.direct_encryption_using_aes-gcm": cloneDeep(require("jose-cookbook/jwe/5_6.direct_encryption_using_aes-gcm.json")), + "5_8.key_wrap_using_aes-keywrap_with_aes-gcm": cloneDeep(require("jose-cookbook/jwe/5_8.key_wrap_using_aes-keywrap_with_aes-gcm.json")), + "5_9.compressed_content": cloneDeep(require("jose-cookbook/jwe/5_9.compressed_content.json")), + "5_10.including_additional_authentication_data": cloneDeep(require("jose-cookbook/jwe/5_10.including_additional_authentication_data.json")) +}; + +describe("jwe", function() { + forEach(fixtures, function(fixture) { + var input = fixture.input; + var generated = fixture.generated; + var encrypting = fixture.encrypting_content; + var output = fixture.output; + + describe(fixture.title, function() { + before(function keyToJWK() { + var prep = [], + promise; + + if (input.key) { + // Coerce the key object to a JWK object + promise = JWK.asKey(input.key); + promise = promise.then(function(key) { + input.key = key; + assert(JWK.isKey(input.key)); + }); + prep.push(promise); + } + if (input.pwd) { + // Coerce password to JWK object + promise = JWK.asKey({ + kty: "oct", + k: util.base64url.encode(input.pwd, "utf8") + }); + promise = promise.then(function(key) { + input.key = key; + assert(JWK.isKey(input.key)); + }); + } + // Coerce the CEK to a JWK object + if (generated.cek) { + promise = JWK.asKey({ + kty: "oct", + k: generated.cek + }); + promise = promise.then(function(key) { + generated.cek = key; + assert(JWK.isKey(generated.cek)); + }); + prep.push(promise); + } + + return Promise.all(prep); + }); + + // encrypting + if (fixture.reproducible) { + if (output.compact) { + it("encrypts to a compact JWE", function() { + var options = { + compact: true, + contentAlg: input.enc, + protect: "*", + iv: generated.iv, + fields: encrypting.protected + }; + if (generated.cek) { + options.cek = generated.cek; + } + if (input.aad) { + options.aad = input.aad; + } + + var encrypter = JWE.createEncrypt(options, { + key: input.key, + reference: false + }); + return encrypter.final(input.plaintext, "utf8") + .then(function(ciphertext) { + assert.deepEqual(ciphertext, output.compact); + }); + }); + } + if (output.json) { + it("encrypts to a general JSON JWE", function() { + var options = { + compact: false, + contentAlg: input.enc, + protect: "*", + iv: generated.iv, + fields: encrypting.protected + }; + if (generated.cek) { + options.cek = generated.cek; + } + if (input.aad) { + options.aad = input.aad; + } + + var encrypter = JWE.createEncrypt(options, { + key: input.key, + reference: false + }); + return encrypter.final(input.plaintext, "utf8") + .then(function(ciphertext) { + assert.deepEqual(ciphertext, output.json); + }); + }); + } + if (output.json_flat) { + it("encrypts to a flattened JSON JWE", function() { + var options = { + format: "flattened", + contentAlg: input.enc, + protect: "*", + iv: generated.iv, + fields: encrypting.protected + }; + if (generated.cek) { + options.cek = generated.cek; + } + if (input.aad) { + options.aad = input.aad; + } + + var encrypter = JWE.createEncrypt(options, { + key: input.key, + reference: false + }); + return encrypter.final(input.plaintext, "utf8") + .then(function(ciphertext) { + assert.deepEqual(ciphertext, output.json_flat); + }); + }); + } + } + + if (output.compact) { + it("decrypts from a compact JWE", function() { + var decrypter = JWE.createDecrypt(input.key); + return decrypter.decrypt(output.compact) + .then(function(result) { + // result.plaintext is a buffer, assert.equal will invoke its + // toString() method implicitly + assert.equal(result.plaintext, input.plaintext); + + // But let's make it clear that result.plaintext needs to be + // converted before actually being a string. + var plaintext = result.plaintext.toString(); + assert.deepEqual(plaintext, input.plaintext); + }); + }); + } + if (output.json) { + it("decrypts from a general JSON JWE", function() { + var decrypter = JWE.createDecrypt(input.key); + return decrypter.decrypt(output.json) + .then(function(result) { + // result.plaintext is a buffer, assert.equal will invoke its + // toString() method implicitly + assert.equal(result.plaintext, input.plaintext); + + // But let's make it clear that result.plaintext needs to be + // converted before actually being a string. + var plaintext = result.plaintext.toString(); + assert.deepEqual(plaintext, input.plaintext); + }); + }); + } + if (output.json_flat) { + it("decrypts from a flattened JSON JWE", function() { + var decrypter = JWE.createDecrypt(input.key); + return decrypter.decrypt(output.json_flat) + .then(function(result) { + // result.plaintext is a buffer, assert.equal will invoke its + // toString() method implicitly + assert.equal(result.plaintext, input.plaintext); + + // But let's make it clear that result.plaintext needs to be + // converted before actually being a string. + var plaintext = result.plaintext.toString(); + assert.deepEqual(plaintext, input.plaintext); + }); + }); + } + }); + + }); +}); diff --git a/test/jwk/basekey-test.js b/test/jwk/basekey-test.js new file mode 100644 index 0000000..0d18d4c --- /dev/null +++ b/test/jwk/basekey-test.js @@ -0,0 +1,1441 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var JWK = { + BaseKey: require("../../lib/jwk/basekey.js"), + helpers: require("../../lib/jwk/helpers.js"), + CONSTANTS: require("../../lib/jwk/constants.js") +}; +var util = require("../../lib/util"); + +describe("jwk/basekey", function() { + var GENERIC_CFG = { + publicKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + {name: "pub", type: "binary"} + ]); + + var pk = JWK.helpers.unpackProps(props, fields); + if (pk.pub) { + pk.length = pk.pub.length * 8; + } + + return pk; + }, + privateKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + {name: "prv", type: "binary"} + ]); + + var pk = JWK.helpers.unpackProps(props, fields); + if (pk.prv) { + pk.length = pk.prv.length * 8; + } + + return pk; + }, + algorithms: function(keys, mode) { + var supported; + switch (mode) { + case JWK.CONSTANTS.MODE_SIGN: + supported = keys.private && + keys.private.prv && + ["HS256", "HS384", "HS512"]; + break; + case JWK.CONSTANTS.MODE_VERIFY: + supported = keys.public && + keys.public.pub && + ["HS256", "HS384", "HS512"]; + break; + case JWK.CONSTANTS.MODE_ENCRYPT: + supported = keys.public && + keys.public.pub && + ["A128GCM", "A192GCM", "A256GCM"]; + break; + case JWK.CONSTANTS.MODE_DECRYPT: + supported = keys.private && + keys.private.prv && + ["A128GCM", "A192GCM", "A256GCM"]; + break; + case JWK.CONSTANTS.MODE_WRAP: + supported = keys.public && + keys.public.pub && + ["A128KW", "A192KW", "A256KW"]; + break; + case JWK.CONSTANTS.MODE_UNWRAP: + supported = keys.private && + keys.private.prv && + ["A128KW", "A192KW", "A256KW"]; + break; + } + + return supported || []; + } + }; + function createInstance(props) { + var store = {}; + return new JWK.BaseKey("DUMMY", store, props, GENERIC_CFG); + } + + describe("ctor", function() { + it("creates a generic BaseKey", function() { + var keystore = new Date(), + props, + inst; + + props = { + kid: "somevalue", + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + }; + inst = new JWK.BaseKey("DUMMY", keystore, props, GENERIC_CFG); + // built-in properties + assert.strictEqual(inst.keystore, keystore); + assert.equal(inst.length, 128); + assert.equal(inst.kty, "DUMMY"); + assert.equal(inst.kid, props.kid); + assert.equal(inst.use, "enc"); + assert.equal(inst.alg, "A128GCM"); + + // public-only properties + assert.ok(inst.has("kty")); + assert.equal(inst.get("kty"), "DUMMY"); + assert.ok(inst.has("kid")); + assert.equal(inst.get("kid"), "somevalue"); + assert.ok(inst.has("use")); + assert.equal(inst.get("use"), "enc"); + assert.ok(inst.has("alg")); + assert.equal(inst.get("alg"), "A128GCM"); + assert.ok(inst.has("pub")); + assert.equal(util.base64url.encode(inst.get("pub")), + props.pub); + assert.notOk(inst.has("prv")); + assert.isNull(inst.get("prv")); + assert.deepEqual(inst.toObject(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM", + "use": "enc" + }); + assert.deepEqual(inst.toJSON(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM", + "use": "enc" + }); + + // private properties + assert.ok(inst.has("prv", true)); + assert.equal(util.base64url.encode(inst.get("prv", true)), + props.prv); + assert.deepEqual(inst.toObject(true), { + "kty": "DUMMY", + "kid": "somevalue", + "prv": util.base64url.decode("SBh6LBt1DBTeyHTvwDgSjg"), + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM", + "use": "enc" + }); + assert.deepEqual(inst.toJSON(true), { + "kty": "DUMMY", + "kid": "somevalue", + "prv": "SBh6LBt1DBTeyHTvwDgSjg", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM", + "use": "enc" + }); + }); + it("creates a generic BaseKey with extras", function() { + var keystore = new Date(), + props, + inst; + + props = { + kid: "somevalue", + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM", + exp: "2015-01-29T11:15:01-07:00", + iss: "nobody@nowhere" + }; + inst = new JWK.BaseKey("DUMMY", keystore, props, GENERIC_CFG); + // built-in properties + assert.strictEqual(inst.keystore, keystore); + assert.equal(inst.length, 128); + assert.equal(inst.kty, "DUMMY"); + assert.equal(inst.kid, props.kid); + assert.equal(inst.use, "enc"); + assert.equal(inst.alg, "A128GCM"); + + // public-only properties + assert.ok(inst.has("kty")); + assert.equal(inst.get("kty"), "DUMMY"); + assert.ok(inst.has("kid")); + assert.equal(inst.get("kid"), "somevalue"); + assert.ok(inst.has("use")); + assert.equal(inst.get("use"), "enc"); + assert.ok(inst.has("alg")); + assert.equal(inst.get("alg"), "A128GCM"); + assert.ok(inst.has("pub")); + assert.equal(util.base64url.encode(inst.get("pub")), + props.pub); + assert.notOk(inst.has("prv")); + assert.isNull(inst.get("prv")); + assert.ok(inst.has("exp")); + assert.equal(inst.get("exp"), "2015-01-29T11:15:01-07:00"); + assert.ok(inst.has("iss")); + assert.equal(inst.get("iss"), "nobody@nowhere"); + assert.deepEqual(inst.toObject(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM", + "use": "enc", + "exp": "2015-01-29T11:15:01-07:00", + "iss": "nobody@nowhere" + }); + assert.deepEqual(inst.toJSON(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM", + "use": "enc", + "exp": "2015-01-29T11:15:01-07:00", + "iss": "nobody@nowhere" + }); + + // private properties + assert.ok(inst.has("prv", true)); + assert.equal(util.base64url.encode(inst.get("prv", true)), + props.prv); + assert.deepEqual(inst.toObject(true), { + "kty": "DUMMY", + "kid": "somevalue", + "prv": util.base64url.decode("SBh6LBt1DBTeyHTvwDgSjg"), + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM", + "use": "enc", + "exp": "2015-01-29T11:15:01-07:00", + "iss": "nobody@nowhere" + }); + assert.deepEqual(inst.toJSON(true), { + "kty": "DUMMY", + "kid": "somevalue", + "prv": "SBh6LBt1DBTeyHTvwDgSjg", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM", + "use": "enc", + "exp": "2015-01-29T11:15:01-07:00", + "iss": "nobody@nowhere" + }); + }); + it("creates a generic BaseKey with a props string", function() { + var keystore = new Date(), + props, + inst; + + props = { + kid: "somevalue", + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + }; + inst = new JWK.BaseKey("DUMMY", + keystore, + JSON.stringify(props), + GENERIC_CFG); + // built-in properties + assert.strictEqual(inst.keystore, keystore); + assert.equal(inst.length, 128); + assert.equal(inst.kty, "DUMMY"); + assert.equal(inst.kid, props.kid); + assert.equal(inst.use, "enc"); + assert.equal(inst.alg, "A128GCM"); + + // public-only properties + assert.ok(inst.has("kty")); + assert.equal(inst.get("kty"), "DUMMY"); + assert.ok(inst.has("kid")); + assert.equal(inst.get("kid"), "somevalue"); + assert.ok(inst.has("use")); + assert.equal(inst.get("use"), "enc"); + assert.ok(inst.has("alg")); + assert.equal(inst.get("alg"), "A128GCM"); + assert.ok(inst.has("pub")); + assert.equal(util.base64url.encode(inst.get("pub")), + props.pub); + assert.notOk(inst.has("prv")); + assert.isNull(inst.get("prv")); + assert.deepEqual(inst.toObject(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM", + "use": "enc" + }); + assert.deepEqual(inst.toJSON(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM", + "use": "enc" + }); + + // private properties + assert.ok(inst.has("prv", true)); + assert.equal(util.base64url.encode(inst.get("prv", true)), + props.prv); + assert.deepEqual(inst.toObject(true), { + "kty": "DUMMY", + "kid": "somevalue", + "prv": util.base64url.decode("SBh6LBt1DBTeyHTvwDgSjg"), + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM", + "use": "enc" + }); + assert.deepEqual(inst.toJSON(true), { + "kty": "DUMMY", + "kid": "somevalue", + "prv": "SBh6LBt1DBTeyHTvwDgSjg", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM", + "use": "enc" + }); + }); + it("creates a generic BaseKey without a private key", function() { + var keystore = new Date(), + props, + inst; + + props = { + kid: "somevalue", + pub: "Lc3EY3_96tfej0F7Afa0TQ", + use: "enc", + alg: "A128GCM" + }; + inst = new JWK.BaseKey("DUMMY", keystore, props, GENERIC_CFG); + // built-in properties + assert.strictEqual(inst.keystore, keystore); + assert.equal(inst.length, 128); + assert.equal(inst.kty, "DUMMY"); + assert.equal(inst.kid, props.kid); + assert.equal(inst.use, "enc"); + assert.equal(inst.alg, "A128GCM"); + + // public-only properties + assert.ok(inst.has("kty")); + assert.equal(inst.get("kty"), "DUMMY"); + assert.ok(inst.has("kid")); + assert.equal(inst.get("kid"), "somevalue"); + assert.ok(inst.has("use")); + assert.equal(inst.get("use"), "enc"); + assert.ok(inst.has("alg")); + assert.equal(inst.get("alg"), "A128GCM"); + assert.ok(inst.has("pub")); + assert.equal(util.base64url.encode(inst.get("pub")), + props.pub); + assert.notOk(inst.has("prv")); + assert.isNull(inst.get("prv")); + assert.deepEqual(inst.toObject(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM", + "use": "enc" + }); + assert.deepEqual(inst.toJSON(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM", + "use": "enc" + }); + + // private properties + assert.notOk(inst.has("prv", true)); + assert.isNull(inst.get("prv", true)); + assert.deepEqual(inst.toObject(true), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM", + "use": "enc" + }); + assert.deepEqual(inst.toJSON(true), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM", + "use": "enc" + }); + }); + it("creates a generic BaseKey without a public key", function() { + var keystore = new Date(), + props, + inst; + + props = { + kid: "somevalue", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + }; + inst = new JWK.BaseKey("DUMMY", keystore, props, GENERIC_CFG); + // built-in properties + assert.strictEqual(inst.keystore, keystore); + assert.equal(inst.length, 128); + assert.equal(inst.kty, "DUMMY"); + assert.equal(inst.kid, props.kid); + assert.equal(inst.use, "enc"); + assert.equal(inst.alg, "A128GCM"); + + // public-only properties + assert.ok(inst.has("kty")); + assert.equal(inst.get("kty"), "DUMMY"); + assert.ok(inst.has("kid")); + assert.equal(inst.get("kid"), "somevalue"); + assert.ok(inst.has("use")); + assert.equal(inst.get("use"), "enc"); + assert.ok(inst.has("alg")); + assert.equal(inst.get("alg"), "A128GCM"); + assert.notOk(inst.has("pub")); + assert.isNull(inst.get("pub")); + assert.notOk(inst.has("prv")); + assert.isNull(inst.get("prv")); + assert.deepEqual(inst.toObject(), { + "kty": "DUMMY", + "kid": "somevalue", + "alg": "A128GCM", + "use": "enc" + }); + assert.deepEqual(inst.toJSON(), { + "kty": "DUMMY", + "kid": "somevalue", + "alg": "A128GCM", + "use": "enc" + }); + + // private properties + assert.ok(inst.has("prv", true)); + assert.equal(util.base64url.encode(inst.get("prv", true)), + props.prv); + assert.deepEqual(inst.toObject(true), { + "kty": "DUMMY", + "kid": "somevalue", + "prv": util.base64url.decode("SBh6LBt1DBTeyHTvwDgSjg"), + "alg": "A128GCM", + "use": "enc" + }); + assert.deepEqual(inst.toJSON(true), { + "kty": "DUMMY", + "kid": "somevalue", + "prv": "SBh6LBt1DBTeyHTvwDgSjg", + "alg": "A128GCM", + "use": "enc" + }); + }); + it("creates a generic BaseKey with implied values", function() { + var keystore = new Date(), + props, + inst; + + props = { + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg" + }; + inst = new JWK.BaseKey("DUMMY", keystore, props, GENERIC_CFG); + // built-in properties + assert.strictEqual(inst.keystore, keystore); + assert.equal(inst.length, 128); + assert.equal(inst.kty, "DUMMY"); + assert.isString(inst.kid); + assert.equal(inst.use, ""); + assert.equal(inst.alg, ""); + + // public-only properties + assert.ok(inst.has("kty")); + assert.equal(inst.get("kty"), "DUMMY"); + assert.notOk(inst.has("use")); + assert.isNull(inst.get("use")); + assert.notOk(inst.has("alg")); + assert.isNull(inst.get("alg")); + assert.ok(inst.has("pub")); + assert.equal(util.base64url.encode(inst.get("pub")), + props.pub); + assert.notOk(inst.has("prv")); + assert.isNull(inst.get("prv")); + assert.deepEqual(inst.toObject(), { + "kty": "DUMMY", + "kid": inst.kid, + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ") + }); + assert.deepEqual(inst.toJSON(), { + "kty": "DUMMY", + "kid": inst.kid, + "pub": "Lc3EY3_96tfej0F7Afa0TQ" + }); + + // private properties + assert.ok(inst.has("prv", true)); + assert.equal(util.base64url.encode(inst.get("prv", true)), + props.prv); + assert.deepEqual(inst.toJSON(true), { + "kty": "DUMMY", + "kid": inst.kid, + "prv": "SBh6LBt1DBTeyHTvwDgSjg", + "pub": "Lc3EY3_96tfej0F7Afa0TQ" + }); + }); + + it("fails to create with missing arguments", function() { + var keystore = new Date(), + props = { + kid: "somevalue", + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + }; + + /* eslint no-unused-vars: [0] */ + assert.throw(function() { + var key = new JWK.BaseKey(null, keystore, props, GENERIC_CFG); + }, "kty cannot be null"); + assert.throw(function() { + var key = new JWK.BaseKey("DUMMY", null, props, GENERIC_CFG); + }, "keystore cannot be null"); + assert.throw(function() { + var key = new JWK.BaseKey("DUMMY", keystore, null, GENERIC_CFG); + }, "props cannot be null"); + assert.throw(function() { + var key = new JWK.BaseKey("DUMMY", keystore, props, null); + }); + }); + }); + + describe("serialization", function() { + it("serializes no options", function() { + var props = { + kid: "somevalue", + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + }; + var inst = createInstance(props); + + assert.deepEqual(inst.toObject(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "use": "enc", + "alg": "A128GCM" + }); + assert.deepEqual(inst.toJSON(), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "use": "enc", + "alg": "A128GCM" + }); + + assert.deepEqual(inst.toObject(true), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "prv": util.base64url.decode("SBh6LBt1DBTeyHTvwDgSjg"), + "use": "enc", + "alg": "A128GCM" + }); + assert.deepEqual(inst.toJSON(true), { + "kty": "DUMMY", + "kid": "somevalue", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "prv": "SBh6LBt1DBTeyHTvwDgSjg", + "use": "enc", + "alg": "A128GCM" + }); + }); + it("serializes with excluded", function() { + var props = { + kid: "somevalue", + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + }; + var inst = createInstance(props); + + assert.deepEqual(inst.toObject(false, ["kid", "use"]), { + "kty": "DUMMY", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM" + }); + assert.deepEqual(inst.toJSON(false, ["kid", "use"]), { + "kty": "DUMMY", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM" + }); + + assert.deepEqual(inst.toObject(true, ["kid", "use"]), { + "kty": "DUMMY", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "prv": util.base64url.decode("SBh6LBt1DBTeyHTvwDgSjg"), + "alg": "A128GCM" + }); + assert.deepEqual(inst.toJSON(true, ["kid", "use"]), { + "kty": "DUMMY", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "prv": "SBh6LBt1DBTeyHTvwDgSjg", + "alg": "A128GCM" + }); + }); + it("serializes with excluded first", function() { + var props = { + kid: "somevalue", + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + }; + var inst = createInstance(props); + + assert.deepEqual(inst.toObject(["kid", "use"]), { + "kty": "DUMMY", + "pub": util.base64url.decode("Lc3EY3_96tfej0F7Afa0TQ"), + "alg": "A128GCM" + }); + assert.deepEqual(inst.toJSON(["kid", "use"]), { + "kty": "DUMMY", + "pub": "Lc3EY3_96tfej0F7Afa0TQ", + "alg": "A128GCM" + }); + }); + }); + + describe("algorithms and supports", function() { + it("returns all supported algorithms", function() { + var inst; + + // all-all ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg" + }); + assert.deepEqual(inst.algorithms(), [ + "HS256", + "HS384", + "HS512", + "A128GCM", + "A192GCM", + "A256GCM", + "A128KW", + "A192KW", + "A256KW" + ]); + assert.ok(inst.supports("HS256")); + assert.ok(inst.supports("HS384")); + assert.ok(inst.supports("HS512")); + assert.ok(inst.supports("A128GCM")); + assert.ok(inst.supports("A192GCM")); + assert.ok(inst.supports("A256GCM")); + assert.ok(inst.supports("A128KW")); + assert.ok(inst.supports("A192KW")); + assert.ok(inst.supports("A256KW")); + + // public-only ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ" + }); + assert.deepEqual(inst.algorithms(), [ + "HS256", + "HS384", + "HS512", + "A128GCM", + "A192GCM", + "A256GCM", + "A128KW", + "A192KW", + "A256KW" + ]); + assert.ok(inst.supports("HS256")); + assert.ok(inst.supports("HS384")); + assert.ok(inst.supports("HS512")); + assert.ok(inst.supports("A128GCM")); + assert.ok(inst.supports("A192GCM")); + assert.ok(inst.supports("A256GCM")); + assert.ok(inst.supports("A128KW")); + assert.ok(inst.supports("A192KW")); + assert.ok(inst.supports("A256KW")); + + // private-only ops + inst = createInstance({ + prv: "SBh6LBt1DBTeyHTvwDgSjg" + }); + assert.deepEqual(inst.algorithms(), [ + "HS256", + "HS384", + "HS512", + "A128GCM", + "A192GCM", + "A256GCM", + "A128KW", + "A192KW", + "A256KW" + ]); + assert.ok(inst.supports("HS256")); + assert.ok(inst.supports("HS384")); + assert.ok(inst.supports("HS512")); + assert.ok(inst.supports("A128GCM")); + assert.ok(inst.supports("A192GCM")); + assert.ok(inst.supports("A256GCM")); + assert.ok(inst.supports("A128KW")); + assert.ok(inst.supports("A192KW")); + assert.ok(inst.supports("A256KW")); + }); + it("returns a specific mode of supported algorithms", function() { + var inst; + + // all-all ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg" + }); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), [ + "HS256", + "HS384", + "HS512" + ]); + assert.ok(inst.supports("HS256", JWK.CONSTANTS.MODE_SIGN)); + assert.ok(inst.supports("HS384", JWK.CONSTANTS.MODE_SIGN)); + assert.ok(inst.supports("HS512", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_SIGN)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), [ + "HS256", + "HS384", + "HS512" + ]); + assert.ok(inst.supports("HS256", JWK.CONSTANTS.MODE_VERIFY)); + assert.ok(inst.supports("HS384", JWK.CONSTANTS.MODE_VERIFY)); + assert.ok(inst.supports("HS512", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_VERIFY)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), [ + "A128GCM", + "A192GCM", + "A256GCM" + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.ok(inst.supports("A128GCM", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.ok(inst.supports("A192GCM", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.ok(inst.supports("A256GCM", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_ENCRYPT)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), [ + "A128GCM", + "A192GCM", + "A256GCM" + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_DECRYPT)); + assert.ok(inst.supports("A128GCM", JWK.CONSTANTS.MODE_DECRYPT)); + assert.ok(inst.supports("A192GCM", JWK.CONSTANTS.MODE_DECRYPT)); + assert.ok(inst.supports("A256GCM", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_DECRYPT)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), [ + "A128KW", + "A192KW", + "A256KW" + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_WRAP)); + assert.ok(inst.supports("A128KW", JWK.CONSTANTS.MODE_WRAP)); + assert.ok(inst.supports("A192KW", JWK.CONSTANTS.MODE_WRAP)); + assert.ok(inst.supports("A256KW", JWK.CONSTANTS.MODE_WRAP)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), [ + "A128KW", + "A192KW", + "A256KW" + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_UNWRAP)); + assert.ok(inst.supports("A128KW", JWK.CONSTANTS.MODE_UNWRAP)); + assert.ok(inst.supports("A192KW", JWK.CONSTANTS.MODE_UNWRAP)); + assert.ok(inst.supports("A256KW", JWK.CONSTANTS.MODE_UNWRAP)); + + // public-only ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ" + }); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), [ + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_SIGN)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), [ + "HS256", + "HS384", + "HS512" + ]); + assert.ok(inst.supports("HS256", JWK.CONSTANTS.MODE_VERIFY)); + assert.ok(inst.supports("HS384", JWK.CONSTANTS.MODE_VERIFY)); + assert.ok(inst.supports("HS512", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_VERIFY)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), [ + "A128GCM", + "A192GCM", + "A256GCM" + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.ok(inst.supports("A128GCM", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.ok(inst.supports("A192GCM", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.ok(inst.supports("A256GCM", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_ENCRYPT)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), [ + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_DECRYPT)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), [ + "A128KW", + "A192KW", + "A256KW" + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_WRAP)); + assert.ok(inst.supports("A128KW", JWK.CONSTANTS.MODE_WRAP)); + assert.ok(inst.supports("A192KW", JWK.CONSTANTS.MODE_WRAP)); + assert.ok(inst.supports("A256KW", JWK.CONSTANTS.MODE_WRAP)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), [ + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_UNWRAP)); + + // private-only ops + inst = createInstance({ + prv: "SBh6LBt1DBTeyHTvwDgSjg" + }); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), [ + "HS256", + "HS384", + "HS512" + ]); + assert.ok(inst.supports("HS256", JWK.CONSTANTS.MODE_SIGN)); + assert.ok(inst.supports("HS384", JWK.CONSTANTS.MODE_SIGN)); + assert.ok(inst.supports("HS512", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_SIGN)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_SIGN)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), [ + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_VERIFY)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_VERIFY)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), [ + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_ENCRYPT)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_ENCRYPT)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), [ + "A128GCM", + "A192GCM", + "A256GCM" + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_DECRYPT)); + assert.ok(inst.supports("A128GCM", JWK.CONSTANTS.MODE_DECRYPT)); + assert.ok(inst.supports("A192GCM", JWK.CONSTANTS.MODE_DECRYPT)); + assert.ok(inst.supports("A256GCM", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_DECRYPT)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_DECRYPT)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), [ + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A128KW", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A192KW", JWK.CONSTANTS.MODE_WRAP)); + assert.notOk(inst.supports("A256KW", JWK.CONSTANTS.MODE_WRAP)); + + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), [ + "A128KW", + "A192KW", + "A256KW" + ]); + assert.notOk(inst.supports("HS256", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("HS384", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("HS512", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A128GCM", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A192GCM", JWK.CONSTANTS.MODE_UNWRAP)); + assert.notOk(inst.supports("A256GCM", JWK.CONSTANTS.MODE_UNWRAP)); + assert.ok(inst.supports("A128KW", JWK.CONSTANTS.MODE_UNWRAP)); + assert.ok(inst.supports("A192KW", JWK.CONSTANTS.MODE_UNWRAP)); + assert.ok(inst.supports("A256KW", JWK.CONSTANTS.MODE_UNWRAP)); + }); + it("returns supported algorithms based on 'use'", function() { + var inst; + + // 'enc' all-all ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc" + }); + assert.deepEqual(inst.algorithms(), [ + "A128GCM", + "A192GCM", + "A256GCM", + "A128KW", + "A192KW", + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), [ + "A128GCM", + "A192GCM", + "A256GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), [ + "A128GCM", + "A192GCM", + "A256GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), [ + "A128KW", + "A192KW", + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), [ + "A128KW", + "A192KW", + "A256KW" + ]); + assert.notOk(inst.supports("HS256")); + assert.notOk(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.ok(inst.supports("A128GCM")); + assert.ok(inst.supports("A192GCM")); + assert.ok(inst.supports("A256GCM")); + assert.ok(inst.supports("A128KW")); + assert.ok(inst.supports("A192KW")); + assert.ok(inst.supports("A256KW")); + + // 'enc' public-only ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + use: "enc" + }); + assert.deepEqual(inst.algorithms(), [ + "A128GCM", + "A192GCM", + "A256GCM", + "A128KW", + "A192KW", + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), [ + "A128GCM", + "A192GCM", + "A256GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), [ + "A128KW", + "A192KW", + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.notOk(inst.supports("HS256")); + assert.notOk(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.ok(inst.supports("A128GCM")); + assert.ok(inst.supports("A192GCM")); + assert.ok(inst.supports("A256GCM")); + assert.ok(inst.supports("A128KW")); + assert.ok(inst.supports("A192KW")); + assert.ok(inst.supports("A256KW")); + + // 'enc' private-only ops + inst = createInstance({ + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc" + }); + assert.deepEqual(inst.algorithms(), [ + "A128GCM", + "A192GCM", + "A256GCM", + "A128KW", + "A192KW", + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), [ + "A128GCM", + "A192GCM", + "A256GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), [ + "A128KW", + "A192KW", + "A256KW" + ]); + assert.notOk(inst.supports("HS256")); + assert.notOk(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.ok(inst.supports("A128GCM")); + assert.ok(inst.supports("A192GCM")); + assert.ok(inst.supports("A256GCM")); + assert.ok(inst.supports("A128KW")); + assert.ok(inst.supports("A192KW")); + assert.ok(inst.supports("A256KW")); + + // 'sig' all-all ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "sig" + }); + assert.deepEqual(inst.algorithms(), [ + "HS256", + "HS384", + "HS512" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), [ + "HS256", + "HS384", + "HS512" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), [ + "HS256", + "HS384", + "HS512" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.ok(inst.supports("HS256")); + assert.ok(inst.supports("HS384")); + assert.ok(inst.supports("HS512")); + assert.notOk(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.notOk(inst.supports("A256KW")); + + // 'enc' public-only ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + use: "sig" + }); + assert.deepEqual(inst.algorithms(), [ + "HS256", + "HS384", + "HS512" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), [ + "HS256", + "HS384", + "HS512" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.ok(inst.supports("HS256")); + assert.ok(inst.supports("HS384")); + assert.ok(inst.supports("HS512")); + assert.notOk(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.notOk(inst.supports("A256KW")); + + // 'enc' private-only ops + inst = createInstance({ + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "sig" + }); + assert.deepEqual(inst.algorithms(), [ + "HS256", + "HS384", + "HS512" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), [ + "HS256", + "HS384", + "HS512" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.ok(inst.supports("HS256")); + assert.ok(inst.supports("HS384")); + assert.ok(inst.supports("HS512")); + assert.notOk(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.notOk(inst.supports("A256KW")); + }); + it("returns supported algorithms based on 'alg'", function() { + var inst; + + // 'HS384' all-all ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + alg: "HS384" + }); + assert.deepEqual(inst.algorithms(), [ + "HS384" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), [ + "HS384" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), [ + "HS384" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.notOk(inst.supports("HS256")); + assert.ok(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.notOk(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.notOk(inst.supports("A256KW")); + + // 'HS384' public-only ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + alg: "HS384" + }); + assert.deepEqual(inst.algorithms(), [ + "HS384" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), [ + "HS384" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.notOk(inst.supports("HS256")); + assert.ok(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.notOk(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.notOk(inst.supports("A256KW")); + + // 'HS384' private-only ops + inst = createInstance({ + prv: "SBh6LBt1DBTeyHTvwDgSjg", + alg: "HS384" + }); + assert.deepEqual(inst.algorithms(), [ + "HS384" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), [ + "HS384" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.notOk(inst.supports("HS256")); + assert.ok(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.notOk(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.notOk(inst.supports("A256KW")); + + // 'A128GCM' all-all ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + alg: "A128GCM" + }); + assert.deepEqual(inst.algorithms(), [ + "A128GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), [ + "A128GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), [ + "A128GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.notOk(inst.supports("HS256")); + assert.notOk(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.ok(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.notOk(inst.supports("A256KW")); + + // 'A128GCM' public-only ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + alg: "A128GCM" + }); + assert.deepEqual(inst.algorithms(), [ + "A128GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), [ + "A128GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.notOk(inst.supports("HS256")); + assert.notOk(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.ok(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.notOk(inst.supports("A256KW")); + + // 'A128GCM' private-only ops + inst = createInstance({ + prv: "SBh6LBt1DBTeyHTvwDgSjg", + alg: "A128GCM" + }); + assert.deepEqual(inst.algorithms(), [ + "A128GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), [ + "A128GCM" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.notOk(inst.supports("HS256")); + assert.notOk(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.ok(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.notOk(inst.supports("A256KW")); + + // 'A256KW' all-all ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + alg: "A256KW" + }); + assert.deepEqual(inst.algorithms(), [ + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), [ + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), [ + "A256KW" + ]); + assert.notOk(inst.supports("HS256")); + assert.notOk(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.notOk(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.ok(inst.supports("A256KW")); + + // 'A256KW' public-only ops + inst = createInstance({ + pub: "Lc3EY3_96tfej0F7Afa0TQ", + alg: "A256KW" + }); + assert.deepEqual(inst.algorithms(), [ + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), [ + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), []); + assert.notOk(inst.supports("HS256")); + assert.notOk(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.notOk(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.ok(inst.supports("A256KW")); + + // 'A256KW' private-only ops + inst = createInstance({ + prv: "SBh6LBt1DBTeyHTvwDgSjg", + alg: "A256KW" + }); + assert.deepEqual(inst.algorithms(), [ + "A256KW" + ]); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_SIGN), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_VERIFY), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_ENCRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_DECRYPT), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_WRAP), []); + assert.deepEqual(inst.algorithms(JWK.CONSTANTS.MODE_UNWRAP), [ + "A256KW" + ]); + assert.notOk(inst.supports("HS256")); + assert.notOk(inst.supports("HS384")); + assert.notOk(inst.supports("HS512")); + assert.notOk(inst.supports("A128GCM")); + assert.notOk(inst.supports("A192GCM")); + assert.notOk(inst.supports("A256GCM")); + assert.notOk(inst.supports("A128KW")); + assert.notOk(inst.supports("A192KW")); + assert.ok(inst.supports("A256KW")); + }); + }); +}); diff --git a/test/jwk/eckey-test.js b/test/jwk/eckey-test.js new file mode 100644 index 0000000..8c5abb5 --- /dev/null +++ b/test/jwk/eckey-test.js @@ -0,0 +1,336 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"), + bind = require("lodash.bind"), + clone = require("lodash.clone"), + merge = require("../../lib/util/merge"), + omit = require("lodash.omit"), + pick = require("lodash.pick"); +var assert = chai.assert; + +var JWK = { + EC: require("../../lib/jwk/eckey.js"), + BaseKey: require("../../lib/jwk/basekey.js"), + store: require("../../lib/jwk/keystore.js"), + helpers: require("../../lib/jwk/helpers.js"), + CONSTANTS: require("../../lib/jwk/constants.js"), + ecutil: require("../../lib/algorithms/ec-util.js") +}; +var util = require("../../lib/util"); + +describe("jwk/EC", function() { + var keyProps = { + "kty": "EC", + "crv": "P-256", + "x": "uiOfViX69jYwnygrkPkuM0XqUlvW65WEs_7rgT3eaak", + "y": "v8S-ifVFkNLoe1TSUrNFQVj6jRbK1L8V-eZa-ngsZLM", + "d": "dI5TRpZrVLpTr_xxYK-n8FgTBpe5Uer-8QgHu5gx9Ds" + }; + var keyPair; + + function formatKeyPair(json) { + function convert(src, name) { + this[name] = util.base64url.decode(src[name]); + } + var result = { + public: {}, + private: {} + }; + + result.public.kty = result.private.kty = "EC"; + result.public.crv = result.private.crv = json.crv; + ["x", "y"].forEach(bind(convert, result.public, json)); + ["x", "y", "d"].forEach(bind(convert, result.private, json)); + result.public.length = result.private.length = JWK.ecutil.curveSize(json.crv); + + return result; + } + + beforeEach(function() { + keyPair = formatKeyPair(keyProps); + }); + + describe("#publicKey", function() { + it("prepares a publicKey", function() { + var props = clone(keyProps), + actual, + expected; + actual = JWK.EC.config.publicKey(props); + expected = { + "kty": "EC", + "crv": "P-256", + "x": util.base64url.decode("uiOfViX69jYwnygrkPkuM0XqUlvW65WEs_7rgT3eaak"), + "y": util.base64url.decode("v8S-ifVFkNLoe1TSUrNFQVj6jRbK1L8V-eZa-ngsZLM"), + length: 256 + }; + assert.deepEqual(actual, expected); + }); + it("prepares a publicKey with missing values", function() { + var props, + actual, + expected; + + props = omit(keyProps, "crv", "x", "y"); + actual = JWK.EC.config.publicKey(props); + expected = { + "kty": "EC" + }; + assert.deepEqual(actual, expected); + + props = omit(keyProps, "x"); + actual = JWK.EC.config.publicKey(props); + expected = { + "kty": "EC" + }; + assert.deepEqual(actual, expected); + + props = omit(keyProps, "y"); + actual = JWK.EC.config.publicKey(props); + expected = { + "kty": "EC" + }; + assert.deepEqual(actual, expected); + }); + }); + describe("#privateKey", function() { + it("prepares a privateKey", function() { + var props = clone(keyProps), + actual, + expected; + actual = JWK.EC.config.privateKey(props); + expected = { + "kty": "EC", + "crv": "P-256", + "x": util.base64url.decode("uiOfViX69jYwnygrkPkuM0XqUlvW65WEs_7rgT3eaak"), + "y": util.base64url.decode("v8S-ifVFkNLoe1TSUrNFQVj6jRbK1L8V-eZa-ngsZLM"), + "d": util.base64url.decode("dI5TRpZrVLpTr_xxYK-n8FgTBpe5Uer-8QgHu5gx9Ds"), + length: 256 + }; + assert.deepEqual(actual, expected); + }); + it("prepares a privateKey with missing values", function() { + var props, + actual; + + props = omit(keyProps, "crv", "x", "y", "d"); + actual = JWK.EC.config.privateKey(props); + assert.deepEqual(actual, undefined); + + props = omit(keyProps, "d"); + actual = JWK.EC.config.privateKey(props); + assert.deepEqual(actual, undefined); + + props = omit(keyProps, "d"); + actual = JWK.EC.config.privateKey(props); + assert.deepEqual(actual, undefined); + + props = omit(keyProps, "x", "y"); + actual = JWK.EC.config.privateKey(props); + assert.deepEqual(actual, undefined); + }); + }); + + describe("#wrapKey", function() { + it("returns key value", function() { + var keys = clone(keyPair); + + var result = JWK.EC.config.wrapKey("ECDH-ES", keys); + assert.strictEqual(result, keys.public); + }); + it("returns undefined for missing keys.public", function() { + var keys = omit(keyPair, "public"); + + var result = JWK.EC.config.wrapKey("ECDH-ES", keys); + assert.isUndefined(result); + }); + }); + describe("#unwrapKey", function() { + it("returns key value", function() { + var keys = clone(keyPair); + + var result = JWK.EC.config.unwrapKey("ECDH-ES", keys); + assert.strictEqual(result, keys.private); + }); + it("returns undefined for missing keys.private", function() { + var keys = omit(keyPair, "private"); + + var result = JWK.EC.config.unwrapKey("ECDH-ES", keys); + assert.isUndefined(result); + }); + }); + describe("#signKey", function() { + it("returns key value", function() { + var keys = clone(keyPair); + + var result = JWK.EC.config.signKey("ES256", keys); + assert.strictEqual(result, keys.private); + }); + it("returns undefined for missing keys.private", function() { + var keys = omit(keyPair, "private"); + + var result = JWK.EC.config.signKey("ES256", keys); + assert.isUndefined(result); + }); + }); + describe("#verifyKey", function() { + it("returns key value", function() { + var keys = clone(keyPair); + + var result = JWK.EC.config.verifyKey("ES256", keys); + assert.strictEqual(result, keys.public); + }); + it("returns undefined for missing keys.public", function() { + var keys = omit(keyPair, "public"); + + var result = JWK.EC.config.verifyKey("ES256", keys); + assert.isUndefined(result); + }); + }); + describe("#algorithms", function() { + it("returns suite for public key", function() { + var keys = pick(keyPair, "public"); + var algs; + + algs = JWK.EC.config.algorithms(keys, "encrypt"); + assert.deepEqual(algs, []); + algs = JWK.EC.config.algorithms(keys, "decrypt"); + assert.deepEqual(algs, []); + + algs = JWK.EC.config.algorithms(keys, "wrap"); + assert.deepEqual(algs, ["ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"]); + + algs = JWK.EC.config.algorithms(keys, "unwrap"); + assert.deepEqual(algs, []); + + algs = JWK.EC.config.algorithms(keys, "sign"); + assert.deepEqual(algs, []); + + algs = JWK.EC.config.algorithms(keys, "verify"); + assert.deepEqual(algs, ["ES256"]); + }); + it("returns suite for private key", function() { + var keys = pick(keyPair, "private"); + var algs; + + algs = JWK.EC.config.algorithms(keys, "encrypt"); + assert.deepEqual(algs, []); + algs = JWK.EC.config.algorithms(keys, "decrypt"); + assert.deepEqual(algs, []); + + algs = JWK.EC.config.algorithms(keys, "wrap"); + assert.deepEqual(algs, []); + + algs = JWK.EC.config.algorithms(keys, "unwrap"); + assert.deepEqual(algs, ["ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW"]); + + algs = JWK.EC.config.algorithms(keys, "sign"); + assert.deepEqual(algs, ["ES256"]); + + algs = JWK.EC.config.algorithms(keys, "verify"); + assert.deepEqual(algs, []); + }); + }); + describe("keystore integration", function() { + it("generates a 'EC' JWK", function() { + var keystore = JWK.store.KeyStore.createKeyStore(); + + var promise = keystore.generate("EC", "P-256"); + promise = promise.then(function(key) { + assert.equal(key.kty, "EC"); + assert.equal(key.length, 256); + assert.equal(key.get("crv"), "P-256"); + assert.ok(!!key.get("x")); + assert.ok(!!key.get("y")); + assert.ok(!!key.get("d", true)); + }); + + return promise; + }); + it("generates a 'EC' JWK with props", function() { + var keystore = JWK.store.KeyStore.createKeyStore(); + + var props = { + kid: "someid", + use: "sig", + alg: "ES256" + }; + + var promise = keystore.generate("EC", "P-256", props); + promise = promise.then(function(key) { + assert.equal(key.kty, "EC"); + assert.equal(key.length, 256); + assert.equal(key.kid, "someid"); + assert.equal(key.get("use"), "sig"); + assert.equal(key.get("alg"), "ES256"); + assert.equal(key.get("crv"), "P-256"); + assert.ok(!!key.get("x")); + assert.ok(!!key.get("y")); + assert.ok(!!key.get("d", true)); + }); + + return promise; + }); + + function setupSigKey() { + var keystore = JWK.store.KeyStore.createKeyStore(); + var jwk = merge({}, keyProps, { + kid: "someid", + use: "sig", + alg: "ES256" + }); + + return keystore.add(jwk); + } + function setupWrapKey() { + var keystore = JWK.store.KeyStore.createKeyStore(); + var jwk = merge({}, keyProps, { + kid: "someid", + use: "enc", + alg: "ECDH-ES" + }); + + return keystore.add(jwk); + } + + it("imports a 'EC' signing JWK", function() { + var promise = Promise.resolve(); + + promise = promise.then(setupSigKey); + promise = promise.then(function(key) { + assert.equal(key.kty, "EC"); + assert.equal(key.length, 256); + assert.equal(key.kid, "someid"); + + var json = merge({}, keyProps, { + use: "sig", + alg: "ES256", + kid: "someid" + }); + assert.deepEqual(key.toJSON(true), json); + }); + + return promise; + }); + it("imports a 'EC' wrapping JWK", function() { + var promise = Promise.resolve(); + + promise = promise.then(setupWrapKey); + promise = promise.then(function(key) { + assert.equal(key.kty, "EC"); + assert.equal(key.length, 256); + assert.equal(key.kid, "someid"); + + var json = merge({}, keyProps, { + use: "enc", + alg: "ECDH-ES", + kid: "someid" + }); + assert.deepEqual(key.toJSON(true), json); + }); + }); + }); +}); diff --git a/test/jwk/keystore-test.js b/test/jwk/keystore-test.js new file mode 100644 index 0000000..fde20ac --- /dev/null +++ b/test/jwk/keystore-test.js @@ -0,0 +1,606 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"), + forge = require("node-forge"); +var assert = chai.assert; + +var JWK = { + store: require("../../lib/jwk/keystore.js"), + BaseKey: require("../../lib/jwk/basekey.js"), + helpers: require("../../lib/jwk/helpers.js"), + CONSTANTS: require("../../lib/jwk/constants.js") +}; +var util = require("../../lib/util"); + +var DUMMY_FACTORY = { + kty: "DUMMY", + config: { + publicKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + {name: "pub", type: "binary"} + ]); + + var pk; + pk = JWK.helpers.unpackProps(props, fields); + pk.length = (pk.pub && pk.pub.length || 0) * 8; + + return pk; + }, + privateKey: function(props) { + var fields = JWK.helpers.COMMON_PROPS.concat([ + {name: "prv", type: "binary"} + ]); + + var pk; + pk = JWK.helpers.unpackProps(props, fields); + pk.length = (pk.prv && pk.prv.length || 0) * 8; + + return pk; + } + }, + generate: function(size) { + if ((size !== 128) && + (size !== 192) && + (size !== 256)) { + Promise.reject(new Error("invalid key size")); + } + + // NOT A REAL KEY + var props = { + pub: new Buffer(forge.random.getBytes(size / 8), "binary"), + prv: new Buffer(forge.random.getBytes(size / 8), "binary") + }; + + return Promise.resolve(props); + }, + prepare: function() { + return Promise.resolve(DUMMY_FACTORY.config); + } +}; + +describe("jwk/registry", function() { + it("registers and unregisters a factory", function() { + var registry = new JWK.store.KeyRegistry(); + + var type = registry.get("DUMMY"); + assert.isUndefined(type); + assert.isUndefined(registry.get("")); + assert.isUndefined(registry.get()); + + assert.strictEqual(registry.register(DUMMY_FACTORY), registry); + type = registry.get("DUMMY"); + assert.strictEqual(type, DUMMY_FACTORY); + assert.isUndefined(registry.get("")); + assert.isUndefined(registry.get()); + + assert.strictEqual(registry.unregister(DUMMY_FACTORY), registry); + type = registry.get("DUMMY"); + assert.isUndefined(type); + assert.isUndefined(registry.get("")); + assert.isUndefined(registry.get()); + + assert.strictEqual(registry.unregister(DUMMY_FACTORY), registry); + type = registry.get("DUMMY"); + assert.isUndefined(type); + assert.isUndefined(registry.get("")); + assert.isUndefined(registry.get()); + }); + it("rejects an invalid factory when registering", function() { + var registry = new JWK.store.KeyRegistry(); + + assert.throw(function() { + registry.register(null); + }, "invalid Key factory"); + assert.throw(function() { + registry.register({}); + }, "invalid Key factory"); + }); + it("rejects an invalid factory when registering", function() { + var registry = new JWK.store.KeyRegistry(); + + assert.throw(function() { + registry.unregister(null); + }, "invalid Key factory"); + assert.throw(function() { + registry.unregister({}); + }, "invalid Key factory"); + }); +}); +describe("jwk/keystore", function() { + var REGISTRY = new JWK.store.KeyRegistry(); + before(function() { + REGISTRY.register(DUMMY_FACTORY); + }); + + function createInstance() { + return new JWK.store.KeyStore(REGISTRY); + } + + describe("add/remove", function() { + it("adds/removes a key as JSON", function() { + var jwk = { + "kty": "DUMMY", + "kid": "someid", + "use": "sig", + "alg": "HS256", + "pub": util.base64url.encode(new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", "hex")), + "prv": util.base64url.encode(new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex")) + }; + var keystore = createInstance(); + + var promise = keystore.add(jwk); + assert.ok("function" === typeof promise.then); + promise = promise.then(function(key) { + // is a key ... + assert.ok(JWK.store.KeyStore.isKey(key)); + assert.deepEqual(key.toObject(true), { + "kty": "DUMMY", + "kid": "someid", + "use": "sig", + "alg": "HS256", + "pub": new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", "hex"), + "prv": new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex") + }); + + // ... and exists in the keystore + assert.deepEqual(keystore.all(), [key]); + + return key; + }); + + promise = promise.then(function(key) { + keystore.remove(key); + + assert.deepEqual(keystore.all(), []); + }); + + return promise; + }); + it("adds/removes a key as a string", function() { + var jwk = JSON.stringify({ + "kty": "DUMMY", + "kid": "someid", + "use": "sig", + "alg": "HS256", + "pub": util.base64url.encode(new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", "hex")), + "prv": util.base64url.encode(new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex")) + }); + var keystore = createInstance(); + + var promise = keystore.add(jwk); + assert.ok("function" === typeof promise.then); + promise = promise.then(function(key) { + // is a key ... + assert.ok(JWK.store.KeyStore.isKey(key)); + assert.deepEqual(key.toObject(true), { + "kty": "DUMMY", + "kid": "someid", + "use": "sig", + "alg": "HS256", + "pub": new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", "hex"), + "prv": new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex") + }); + + // ... and exists in the keystore + assert.deepEqual(keystore.all(), [key]); + + return key; + }); + + promise = promise.then(function(key) { + keystore.remove(key); + + assert.deepEqual(keystore.all(), []); + }); + + return promise; + }); + it("adds/removes a key as a JWK.Key object", function() { + var jwk = { + "kty": "DUMMY", + "kid": "someid", + "use": "sig", + "alg": "HS256", + "pub": util.base64url.encode(new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", "hex")), + "prv": util.base64url.encode(new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex")) + }; + var keystore = createInstance(); + + jwk = new JWK.BaseKey(jwk.kty, keystore, jwk, DUMMY_FACTORY.config); + + var promise = keystore.add(jwk); + assert.ok("function" === typeof promise.then); + promise = promise.then(function(key) { + // is a key ... + assert.ok(JWK.store.KeyStore.isKey(key)); + assert.deepEqual(key.toObject(true), { + "kty": "DUMMY", + "kid": "someid", + "use": "sig", + "alg": "HS256", + "pub": new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", "hex"), + "prv": new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex") + }); + + // ... and exists in the keystore + assert.deepEqual(keystore.all(), [key]); + + return key; + }); + + promise = promise.then(function(key) { + keystore.remove(key); + + assert.deepEqual(keystore.all(), []); + }); + + return promise; + }); + it("adds/removes a key as a JWK.Key object", function() { + var jwk = { + "kty": "DUMMY", + "kid": "someid", + "use": "sig", + "alg": "HS256", + "pub": util.base64url.encode(new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", "hex")), + "prv": util.base64url.encode(new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex")) + }; + var keystore = createInstance(); + + jwk = new JWK.BaseKey(jwk.kty, keystore, jwk, DUMMY_FACTORY.config); + + var promise = keystore.add(jwk); + assert.ok("function" === typeof promise.then); + promise = promise.then(function(key) { + // is a key ... + assert.ok(JWK.store.KeyStore.isKey(key)); + assert.deepEqual(key.toObject(true), { + "kty": "DUMMY", + "kid": "someid", + "use": "sig", + "alg": "HS256", + "pub": new Buffer("a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", "hex"), + "prv": new Buffer("bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc", "hex") + }); + + // ... and exists in the keystore + assert.deepEqual(keystore.all(), [key]); + + return key; + }); + + promise = promise.then(function(key) { + keystore.remove(key); + + assert.deepEqual(keystore.all(), []); + }); + + return promise; + }); + + it("fails with unknown 'kty'", function() { + var keystore = createInstance(); + var jwk = { + kty: "BOGUS", + kid: "someid", + pub: new Buffer(forge.random.getBytes(16), "binary") + }; + + var promise = keystore.add(jwk); + promise = promise.then(function() { + assert.ok(false, "promise unexpectedly resolved"); + }, function(err) { + assert.equal(err.message, "unsupported key type"); + }); + + return promise; + }); + }); + + describe("generation", function() { + it("generates a key with properties", function() { + var keystore = createInstance(); + var props = { + kid: "someid", + use: "enc" + }; + var promise = keystore.generate("DUMMY", 128, props); + promise = promise.then(function(key) { + assert.strictEqual(key.keystore, keystore); + assert.equal(key.kty, "DUMMY"); + assert.equal(key.kid, "someid"); + assert.equal(key.get("use"), "enc"); + assert.ok(!!key.get("pub")); + assert.ok(!!key.get("prv", true)); + assert.deepEqual(keystore.all(), [key]); + }); + + return promise; + }); + it("generates a key simple", function() { + var keystore = createInstance(); + var promise = keystore.generate("DUMMY", 128); + promise = promise.then(function(key) { + assert.strictEqual(key.keystore, keystore); + assert.equal(key.kty, "DUMMY"); + assert.equal(typeof key.kid, "string"); + assert.ok(!!key.get("pub")); + assert.ok(!!key.get("prv", true)); + assert.deepEqual(keystore.all(), [key]); + }); + + return promise; + }); + it("fails with unknown 'kty'", function() { + var keystore = createInstance(); + + var promise = keystore.generate("BOGUS", 256); + promise = promise.then(function() { + assert.ok(false, "promise unexpectedly resolved"); + }, function(err) { + assert.equal(err.message, "unsupported key type"); + }); + + return promise; + }); + }); + + describe("query", function() { + + }); + + describe("export", function() { + // ensure there's registred key types + require("../../lib/jwk/rsakey.js"); + require("../../lib/jwk/octkey.js"); + + var inst = JWK.store.KeyStore.createKeyStore(), + keys = [ + { + "kty": "RSA", + "kid": "somekey", + "use": "sig", + "n": "i8exGrwNz69aIKhpblb7DrouMBJJohuzhGPezA1EWZ8klqNt7kxKhd3fA1MN1Nn9QTIX4NefJxmOwzXhq6qLtfOa7ZnXUS-pWrs3pm9oFOf1qF1DbeEDTaTbkvxlO7AFUs_LR1-wHyztH0O-Rl17WqUUjcoA07QYwHCKm1cP_kE4yCkyT0EPNkreCnwQEs-1xvZkyAo_zLjESN8y_Ck9FTTTAWmuEbUhtE1_QmlCfFaoUsBJ5OJG6eCTmr1MQ47T4flKDq6-PFr4JCFyMrmnungxpsg4lp-s1sUgg5qRUyga6ze854pmAgQKzj61lhs8g7k1J5HR6S0PL7xQl5pW6Q", + "e": "AQAB", + "d": "TgcjLjFb5FuWjDR25klXzFjR_7O1tvCPvY-ih3XAeecEnbKNY0DjOOcp3sk2J2OopAQ6oCC9jy4NK5ugZhvF8cQS8B-4unFIsIViA16dU05JK7skMOoy1dz5VYvfVvpjfl7Qsv8PadfCZnmCdfUpLuiIGL5yx7r5NjOcrCplmyApr5KJ53Qk8q76WVbZvH4bxmzoK2sOhzjH_4I4bcdlueeUj6VNZXiReY2VpldsoIgmntEy1z7DMVRjqwvBFLM7yD4P9Dlk3pfHtSMtDMyAsnaco7cAA95t1Yk60Om9RlCf_CjbmempzQ_P6Ned9VJYUvtcadqIE0lhe4Pp4POgAQ", + "p": "y-7A4AGF8dkUjHKVXmPQ65ymWO2ACP8jR9LVrkRXd0uPXQyJqTa-233H0pqXRuMiTCnbYTr3oz4ePX53SDUsusDtWgGsrzTRozoHQoxA6cjLX-1VcjlgPpW2gzltlQWv7MBK-LaxGwlS3iuc9tuTE2vwvyPWBO3orQIF21ZoDGk", + "q": "r3fcBfzoik-U1cL_eNb3dFCnnCt3KsBSQj0zgSKhxcdQYXkWrUgx2F0nzN98T7zjBqDcuVtdRVe8JL1HN_J5fqdjNdfCyvT5asS-MccY-fZwMsdcsT1LLLqZDWd6TsMLMpzW_dTkPTWIKWJB27-DFS61mYafvah2HfWxt8ggpoE", + "dp": "lbOdOJNFrWTKldMjXRfu7Jag8lTeETyhvH7Dx1p5zqPUCN1ETMhYUK3DuxEqjan8qmZrmbN8yAO4lTG6BHKsdCdd1R23kyI15hmZ7Lsih7uTt8Z0XBZMVYT3ZtsIW0XCgAwkvPD3j75Ha7oeToSfMbmiD94RpKq0jBQZEosadEk", + "dq": "OcG2RrJMyNoRH5ukA96ebUbvJNSZ0RSk_vCuN19y6GsG5k65TChrX9Cp_SHDBWwjPldM0CZmuSB76Yv0GVJS84GdgmeW0r94KdDA2hmy-vRHUi-VLzIBwKNbJbJd6_b_hJVjnwGobw1j2FtjWjXbq-lIFVTe18rPtmTdLqVNOgE", + "qi": "YYCsHYc8qLJ1aIWnVJ9srXBC3VPWhB98tjOdK-xafhi19TeDL3OxazFV0f0FuxEGOmYeHyF4nh72wK3kRBrcosNQkAlK8oMH3Cg_AnMYehFRmDSKUFjDjXH5bVBfFk72FkmEywEaQgOiYs34P4RAEBdZohh6UTZm0-bajOkVEOE" + }, + { + kty: "oct", + kid: "somevalue", + k: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + } + ]; + + before(function() { + keys = keys.map(function(k) { + return inst.add(k); + }); + return Promise.all(keys). + then(function(results) { + keys = results; + }); + }); + + it("toJSON() exports the keys (public fields only)", function() { + var actual = inst.toJSON(); + var expected = { + keys: keys.map(function(k) { + return k.toJSON(); + }) + }; + assert.deepEqual(actual, expected); + }); + it("toJSON() exports the keys (with private fields)", function() { + var actual = inst.toJSON(true); + var expected = { + keys: keys.map(function(k) { + return k.toJSON(true); + }) + }; + assert.deepEqual(actual, expected); + }); + }); + + describe("static", function() { + // ensure there's a registered key type + require("../../lib/jwk/octkey.js"); + + describe("KeyStore.isKey", function() { + it("tests for JWK.Key instances", function() { + var props = { + kty: "oct", + kid: "somevalue", + k: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + }; + var ks = createInstance(), + inst = new JWK.BaseKey("DUMMY", ks, {}, DUMMY_FACTORY.config); + + assert.equal(JWK.store.KeyStore.isKey(inst), true); + assert.equal(JWK.store.KeyStore.isKey(props), false); + assert.equal(JWK.store.KeyStore.isKey(42), false); + assert.equal(JWK.store.KeyStore.isKey("hello"), false); + assert.equal(JWK.store.KeyStore.isKey(null), false); + assert.equal(JWK.store.KeyStore.isKey(), false); + }); + }); + describe("KeyStore.asKey", function() { + var props = { + kty: "oct", + kid: "somevalue", + pub: "Lc3EY3_96tfej0F7Afa0TQ", + prv: "SBh6LBt1DBTeyHTvwDgSjg", + use: "enc", + alg: "A128GCM" + }; + + it("coerces JSON Object to JWK.Key instance", function() { + var promise = Promise.resolve(props); + promise = promise.then(function(json) { + return JWK.store.KeyStore.asKey(json); + }); + promise = promise.then(function(jwk) { + assert.ok(JWK.store.KeyStore.isKey(jwk)); + }); + + return promise; + }); + + it("coerces JSON String to JWK.Key instance", function() { + var promise = Promise.resolve(JSON.stringify(props)); + promise = promise.then(function(json) { + return JWK.store.KeyStore.asKey(json); + }); + promise = promise.then(function(jwk) { + assert.ok(JWK.store.KeyStore.isKey(jwk)); + }); + + return promise; + }); + + it("returns the provided JWK.Key instance", function() { + var ks = createInstance(), + key = new JWK.BaseKey(props.kty, ks, props, DUMMY_FACTORY.config), + promise = Promise.resolve(key); + promise = promise.then(function(json) { + return JWK.store.KeyStore.asKey(json); + }); + promise = promise.then(function(jwk) { + assert.ok(JWK.store.KeyStore.isKey(jwk)); + assert.strictEqual(jwk, key); + }); + + return promise; + }); + }); + + describe("KeyStore.isKeyStore", function() { + it("tests for JWK.KeyStore instances", function() { + var inst = createInstance(); + + assert.equal(JWK.store.KeyStore.isKeyStore(inst), true); + assert.equal(JWK.store.KeyStore.isKeyStore({}), false); + assert.equal(JWK.store.KeyStore.isKeyStore(42), false); + assert.equal(JWK.store.KeyStore.isKeyStore("hello"), false); + assert.equal(JWK.store.KeyStore.isKeyStore(null), false); + assert.equal(JWK.store.KeyStore.isKeyStore(), false); + }); + }); + describe("KeyStore.createKeyStore", function() { + it("creates an empty KeyStore", function() { + var keystore = JWK.store.KeyStore.createKeyStore(); + assert.equal(JWK.store.KeyStore.isKeyStore(keystore), true); + assert.deepEqual(keystore.all(), []); + }); + }); + describe("KeyStore.asKeyStore", function() { + var props = { + keys: [ + { + kty: "oct", + kid: "onevalue", + k: "Lc3EY3_96tfej0F7Afa0TQ", + use: "enc", + alg: "A128GCM" + }, + { + kty: "oct", + kid: "twovalue", + k: "TI3C3LsvhIexA3aYg6B6ZMMIhJjLfmddHa_zMyOkZjU", + use: "enc", + alg: "A256GCM" + } + ] + }; + + it("coerces a JSON Object to a JWK.KeyStore instance", function() { + var promise = Promise.resolve(props); + promise = promise.then(function(json) { + return JWK.store.KeyStore.asKeyStore(json); + }); + promise = promise.then(function(ks) { + assert.ok(JWK.store.KeyStore.isKeyStore(ks)); + + var key; + key = ks.get("onevalue"); + assert.ok(JWK.store.KeyStore.isKey(key)); + key = ks.get("twovalue"); + assert.ok(JWK.store.KeyStore.isKey(key)); + }); + return promise; + }); + it("coerces a JSON String (of a JSON Object) to a JWK.KeyStore instance", function() { + var promise = Promise.resolve(JSON.stringify(props)); + promise = promise.then(function(json) { + return JWK.store.KeyStore.asKeyStore(json); + }); + promise = promise.then(function(ks) { + assert.ok(JWK.store.KeyStore.isKeyStore(ks)); + + var key; + key = ks.get("onevalue"); + assert.ok(JWK.store.KeyStore.isKey(key)); + key = ks.get("twovalue"); + assert.ok(JWK.store.KeyStore.isKey(key)); + }); + return promise; + }); + it("coerces a JSON Array to a JWK.KeyStore instance", function() { + var promise = Promise.resolve(props.keys); + promise = promise.then(function(json) { + return JWK.store.KeyStore.asKeyStore(json); + }); + promise = promise.then(function(ks) { + assert.ok(JWK.store.KeyStore.isKeyStore(ks)); + + var key; + key = ks.get("onevalue"); + assert.ok(JWK.store.KeyStore.isKey(key)); + key = ks.get("twovalue"); + assert.ok(JWK.store.KeyStore.isKey(key)); + }); + return promise; + }); + it("coerces a JSON String (of a JSON Array) to a JWK.KeyStore instance", function() { + var promise = Promise.resolve(JSON.stringify(props.keys)); + promise = promise.then(function(json) { + return JWK.store.KeyStore.asKeyStore(json); + }); + promise = promise.then(function(ks) { + assert.ok(JWK.store.KeyStore.isKeyStore(ks)); + + var key; + key = ks.get("onevalue"); + assert.ok(JWK.store.KeyStore.isKey(key)); + key = ks.get("twovalue"); + assert.ok(JWK.store.KeyStore.isKey(key)); + }); + return promise; + }); + it("returns the provided JWK.KeyStore instance", function() { + var inst = createInstance(), + promise = Promise.resolve(inst); + promise = promise.then(function(json) { + return JWK.store.KeyStore.asKeyStore(json); + }); + promise = promise.then(function(ks) { + assert.ok(JWK.store.KeyStore.isKeyStore(ks)); + assert.strictEqual(ks, inst); + }); + return promise; + }); + }); + }); +}); diff --git a/test/jwk/octkey-test.js b/test/jwk/octkey-test.js new file mode 100644 index 0000000..ed6e1ea --- /dev/null +++ b/test/jwk/octkey-test.js @@ -0,0 +1,794 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"), + forge = require("node-forge"), + clone = require("lodash.clone"), + merge = require("../../lib/util/merge"); +var assert = chai.assert; + +var JWK = { + OCTET: require("../../lib/jwk/octkey.js"), + BaseKey: require("../../lib/jwk/basekey.js"), + store: require("../../lib/jwk/keystore.js"), + helpers: require("../../lib/jwk/helpers.js"), + CONSTANTS: require("../../lib/jwk/constants.js") +}; +var util = require("../../lib/util"); + +describe("jwk/oct", function() { + describe("#publicKey", function() { + it("prepares a publicKey", function() { + var props, + actual, + expected; + + props = { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM", + "k": "Obdi7uR-5mc3Zbo0HtI-CQ", + "exp": "2025-01-26T00:00:00Z" + }; + actual = JWK.OCTET.config.publicKey(props); + expected = { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM" + }; + assert.deepEqual(actual, expected); + }); + it("prepares a publicKey with missing key value", function() { + var props, + actual, + expected; + + props = { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM", + "exp": "2025-01-26T00:00:00Z" + }; + actual = JWK.OCTET.config.publicKey(props); + expected = { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM" + }; + assert.deepEqual(actual, expected); + }); + }); + describe("#privateKey", function() { + it("prepares a privateKey", function() { + var props, + actual, + expected; + + props = { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM", + "k": "Obdi7uR-5mc3Zbo0HtI-CQ", + "exp": "2025-01-26T00:00:00Z" + }; + actual = JWK.OCTET.config.privateKey(props); + expected = { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM", + "k": util.base64url.decode("Obdi7uR-5mc3Zbo0HtI-CQ"), + "length": 128 + }; + assert.deepEqual(actual, expected); + }); + it("returns undefined for missing key value", function() { + var props, + actual; + + props = { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM", + "exp": "2025-01-26T00:00:00Z" + }; + actual = JWK.OCTET.config.privateKey(props); + assert.isUndefined(actual); + }); + }); + + describe("#encryptKey", function() { + it("returns key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM", + "k": util.base64url.decode("Obdi7uR-5mc3Zbo0HtI-CQ"), + "length": 128 + } + }; + + var result = JWK.OCTET.config.encryptKey("A128GCM", keys); + assert.equal(result, keys.private.k); + }); + it("returns undefined for missing key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM" + } + }; + + var result = JWK.OCTET.config.encryptKey("A128GCM", keys); + assert.isUndefined(result); + }); + it("returns undefined for missing keys.private", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM" + } + }; + + var result = JWK.OCTET.config.encryptKey("A128GCM", keys); + assert.isUndefined(result); + }); + }); + describe("#encryptProps", function() { + it("prepares string properties", function() { + var props, + adjusted; + + props = { + iv: "f0uE_M5yFBbwGhHy", + stuff: "hello" + }; + adjusted = JWK.OCTET.config.encryptProps("A128GCM", props); + assert.ok(Buffer.isBuffer(adjusted.iv)); + assert.equal(util.base64url.encode(adjusted.iv), "f0uE_M5yFBbwGhHy"); + assert.equal(adjusted.stuff, "hello"); + }); + + it("prepares Buffer properties", function() { + var props, + adjusted; + + props = { + iv: util.base64url.decode("f0uE_M5yFBbwGhHy"), + stuff: "hello" + }; + adjusted = JWK.OCTET.config.encryptProps("A128GCM", props); + assert.ok(Buffer.isBuffer(adjusted.iv)); + assert.equal(util.base64url.encode(adjusted.iv), "f0uE_M5yFBbwGhHy"); + assert.equal(adjusted.stuff, "hello"); + }); + }); + + describe("#decryptKey", function() { + it("returns key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM", + "k": util.base64url.decode("Obdi7uR-5mc3Zbo0HtI-CQ"), + "length": 128 + } + }; + + var result = JWK.OCTET.config.decryptKey("A128GCM", keys); + assert.equal(result, keys.private.k); + }); + it("returns undefined for missing key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCM" + } + }; + + var result = JWK.OCTET.config.decryptKey("A128GCM", keys); + assert.isUndefined(result); + }); + }); + describe("#decryptProps", function() { + it("prepares string properties", function() { + var props, + adjusted; + + props = { + iv: "f0uE_M5yFBbwGhHy", + mac: "ZnNSux6e4oz6r85VHTE8jw" + }; + adjusted = JWK.OCTET.config.decryptProps("A128GCM", props); + assert.ok(Buffer.isBuffer(adjusted.iv)); + assert.equal(util.base64url.encode(adjusted.iv), "f0uE_M5yFBbwGhHy"); + assert.ok(Buffer.isBuffer(adjusted.mac)); + assert.equal(util.base64url.encode(adjusted.mac), "ZnNSux6e4oz6r85VHTE8jw"); + }); + it("prepares Buffer properties", function() { + var props, + adjusted; + + props = { + iv: util.base64url.decode("f0uE_M5yFBbwGhHy"), + mac: util.base64url.decode("ZnNSux6e4oz6r85VHTE8jw") + }; + adjusted = JWK.OCTET.config.decryptProps("A128GCM", props); + assert.ok(Buffer.isBuffer(adjusted.iv)); + assert.equal(util.base64url.encode(adjusted.iv), "f0uE_M5yFBbwGhHy"); + assert.ok(Buffer.isBuffer(adjusted.mac)); + assert.equal(util.base64url.encode(adjusted.mac), "ZnNSux6e4oz6r85VHTE8jw"); + }); + }); + + describe("#wrapKey", function() { + it("returns key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCMKW" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCMKW", + "k": util.base64url.decode("Obdi7uR-5mc3Zbo0HtI-CQ"), + "length": 128 + } + }; + + var result = JWK.OCTET.config.wrapKey("A128GCMKW", keys); + assert.equal(result, keys.private.k); + }); + it("returns undefined for missing key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCMKW" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCMKW" + } + }; + + var result = JWK.OCTET.config.wrapKey("A128GCMKW", keys); + assert.isUndefined(result); + }); + }); + describe("#wrapProps", function() { + it("prepares string properties", function() { + var props, + adjusted; + + props = { + iv: "f0uE_M5yFBbwGhHy", + stuff: "hello" + }; + adjusted = JWK.OCTET.config.wrapProps("A128GCMKW", props); + assert.ok(Buffer.isBuffer(adjusted.iv)); + assert.equal(util.base64url.encode(adjusted.iv), "f0uE_M5yFBbwGhHy"); + assert.equal(adjusted.stuff, "hello"); + }); + + it("prepares Buffer properties", function() { + var props, + adjusted; + + props = { + iv: util.base64url.decode("f0uE_M5yFBbwGhHy"), + stuff: "hello" + }; + adjusted = JWK.OCTET.config.wrapProps("A128GCMKW", props); + assert.ok(Buffer.isBuffer(adjusted.iv)); + assert.equal(util.base64url.encode(adjusted.iv), "f0uE_M5yFBbwGhHy"); + assert.equal(adjusted.stuff, "hello"); + }); + }); + + describe("#unwrapKey", function() { + it("returns key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCMKW" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCMKW", + "k": util.base64url.decode("Obdi7uR-5mc3Zbo0HtI-CQ"), + "length": 128 + } + }; + + var result = JWK.OCTET.config.unwrapKey("A128GCMKW", keys); + assert.equal(result, keys.private.k); + }); + it("returns undefined for missing key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCMKW" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "A128GCMKW" + } + }; + + var result = JWK.OCTET.config.unwrapKey("A128GCMKW", keys); + assert.isUndefined(result); + }); + }); + describe("#unwrapProps", function() { + it("prepares string properties", function() { + var props, + adjusted; + + props = { + iv: "f0uE_M5yFBbwGhHy", + tag: "ZnNSux6e4oz6r85VHTE8jw" + }; + adjusted = JWK.OCTET.config.unwrapProps("A128GCMKW", props); + assert.ok(Buffer.isBuffer(adjusted.iv)); + assert.equal(util.base64url.encode(adjusted.iv), "f0uE_M5yFBbwGhHy"); + assert.ok(Buffer.isBuffer(adjusted.tag)); + assert.equal(util.base64url.encode(adjusted.tag), "ZnNSux6e4oz6r85VHTE8jw"); + }); + it("prepares Buffer properties", function() { + var props, + adjusted; + + props = { + iv: util.base64url.decode("f0uE_M5yFBbwGhHy"), + tag: util.base64url.decode("ZnNSux6e4oz6r85VHTE8jw") + }; + adjusted = JWK.OCTET.config.unwrapProps("A128GCMKW", props); + assert.ok(Buffer.isBuffer(adjusted.iv)); + assert.equal(util.base64url.encode(adjusted.iv), "f0uE_M5yFBbwGhHy"); + assert.ok(Buffer.isBuffer(adjusted.tag)); + assert.equal(util.base64url.encode(adjusted.tag), "ZnNSux6e4oz6r85VHTE8jw"); + }); + }); + + describe("#signKey", function() { + it("returns key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "sig", + "alg": "HS256" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "HS256", + "k": util.base64url.decode("FSvoH87JhcZHRloyWOgODO_Y-XkK4w0eyFMxO4JE-Yo"), + "length": 256 + } + }; + + var result = JWK.OCTET.config.signKey("HS256", keys); + assert.equal(result, keys.private.k); + }); + it("returns undefined for missing key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "HS256" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "HS256" + } + }; + + var result = JWK.OCTET.config.signKey("HS256", keys); + assert.isUndefined(result); + }); + }); + + + describe("#verifyKey", function() { + it("returns key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "sig", + "alg": "HS256" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "HS256", + "k": util.base64url.decode("FSvoH87JhcZHRloyWOgODO_Y-XkK4w0eyFMxO4JE-Yo"), + "length": 256 + } + }; + + var result = JWK.OCTET.config.verifyKey("HS256", keys); + assert.equal(result, keys.private.k); + }); + it("returns undefined for missing key value", function() { + var keys = { + public: { + "kid": "somekey", + "use": "enc", + "alg": "HS256" + }, + private: { + "kid": "somekey", + "use": "enc", + "alg": "HS256" + } + }; + + var result = JWK.OCTET.config.verifyKey("HS256", keys); + assert.isUndefined(result); + }); + }); + + describe("#algorithms", function() { + function generateKeys(size, props) { + props = merge({}, props || {}, { + "k": new Buffer(forge.random.getBytes(size / 8), "binary") + }); + var keys = {}; + keys.public = JWK.OCTET.config.publicKey(clone(props)); + keys.private = JWK.OCTET.config.privateKey(clone(props)); + + return keys; + } + it("returns the suite for 128-bit", function() { + var keys = generateKeys(128); + var algs; + + algs = JWK.OCTET.config.algorithms(keys, "sign"); + assert.deepEqual(algs, []); + algs = JWK.OCTET.config.algorithms(keys, "verify"); + assert.deepEqual(algs, []); + + algs = JWK.OCTET.config.algorithms(keys, "encrypt"); + assert.deepEqual(algs, ["A128GCM"]); + algs = JWK.OCTET.config.algorithms(keys, "decrypt"); + assert.deepEqual(algs, ["A128GCM"]); + + algs = JWK.OCTET.config.algorithms(keys, "wrap"); + assert.deepEqual(algs, ["A128KW", "A128GCMKW", "PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + algs = JWK.OCTET.config.algorithms(keys, "unwrap"); + assert.deepEqual(algs, ["A128KW", "A128GCMKW", "PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + }); + it("returns the suite for 192-bit", function() { + var keys = generateKeys(192); + var algs; + + algs = JWK.OCTET.config.algorithms(keys, "sign"); + assert.deepEqual(algs, []); + algs = JWK.OCTET.config.algorithms(keys, "verify"); + assert.deepEqual(algs, []); + + algs = JWK.OCTET.config.algorithms(keys, "encrypt"); + assert.deepEqual(algs, ["A192GCM"]); + algs = JWK.OCTET.config.algorithms(keys, "decrypt"); + assert.deepEqual(algs, ["A192GCM"]); + + algs = JWK.OCTET.config.algorithms(keys, "wrap"); + assert.deepEqual(algs, ["A192KW", "A192GCMKW", "PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + algs = JWK.OCTET.config.algorithms(keys, "unwrap"); + assert.deepEqual(algs, ["A192KW", "A192GCMKW", "PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + }); + it("returns the suite for 256-bit", function() { + var keys = generateKeys(256); + var algs; + + algs = JWK.OCTET.config.algorithms(keys, "sign"); + assert.deepEqual(algs, ["HS256"]); + algs = JWK.OCTET.config.algorithms(keys, "verify"); + assert.deepEqual(algs, ["HS256"]); + + algs = JWK.OCTET.config.algorithms(keys, "encrypt"); + assert.deepEqual(algs, ["A256GCM", "A128CBC-HS256"]); + algs = JWK.OCTET.config.algorithms(keys, "decrypt"); + assert.deepEqual(algs, ["A256GCM", "A128CBC-HS256"]); + + algs = JWK.OCTET.config.algorithms(keys, "wrap"); + assert.deepEqual(algs, ["A256KW", "A256GCMKW", "PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + algs = JWK.OCTET.config.algorithms(keys, "unwrap"); + assert.deepEqual(algs, ["A256KW", "A256GCMKW", "PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + }); + it("returns the suite for 384-bit", function() { + var keys = generateKeys(384); + var algs; + + algs = JWK.OCTET.config.algorithms(keys, "sign"); + assert.deepEqual(algs, ["HS256", "HS384"]); + algs = JWK.OCTET.config.algorithms(keys, "verify"); + assert.deepEqual(algs, ["HS256", "HS384"]); + + algs = JWK.OCTET.config.algorithms(keys, "encrypt"); + assert.deepEqual(algs, ["A192CBC-HS384"]); + algs = JWK.OCTET.config.algorithms(keys, "decrypt"); + assert.deepEqual(algs, ["A192CBC-HS384"]); + + algs = JWK.OCTET.config.algorithms(keys, "wrap"); + assert.deepEqual(algs, ["PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + algs = JWK.OCTET.config.algorithms(keys, "unwrap"); + assert.deepEqual(algs, ["PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + }); + it("returns the suite for 512-bit", function() { + var keys = generateKeys(512); + var algs; + + algs = JWK.OCTET.config.algorithms(keys, "sign"); + assert.deepEqual(algs, ["HS256", "HS384", "HS512"]); + algs = JWK.OCTET.config.algorithms(keys, "verify"); + assert.deepEqual(algs, ["HS256", "HS384", "HS512"]); + + algs = JWK.OCTET.config.algorithms(keys, "encrypt"); + assert.deepEqual(algs, ["A256CBC-HS512"]); + algs = JWK.OCTET.config.algorithms(keys, "decrypt"); + assert.deepEqual(algs, ["A256CBC-HS512"]); + + algs = JWK.OCTET.config.algorithms(keys, "wrap"); + assert.deepEqual(algs, ["PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + algs = JWK.OCTET.config.algorithms(keys, "unwrap"); + assert.deepEqual(algs, ["PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]); + }); + }); + + describe("keystore integration", function() { + it("generates a 'oct' JWK", function() { + var keystore = JWK.store.KeyStore.createKeyStore(); + + var promise = keystore.generate("oct", 128); + promise = promise.then(function(key) { + assert.equal(key.kty, "oct"); + assert.equal(key.length, 128); + assert.ok(!!key.get("k", true)); + + assert.deepEqual(keystore.all(), [key]); + }); + + return promise; + }); + it("generates a 'oct' JWK with props", function() { + var keystore = JWK.store.KeyStore.createKeyStore(); + + var props = { + kid: "someid", + use: "enc", + alg: "A128GCM" + }; + var promise = keystore.generate("oct", 128, props); + promise = promise.then(function(key) { + assert.equal(key.kty, "oct"); + assert.equal(key.length, 128); + assert.equal(key.kid, "someid"); + assert.equal(key.get("use"), "enc"); + assert.equal(key.get("alg"), "A128GCM"); + assert.ok(!!key.get("k", true)); + + assert.deepEqual(keystore.all(), [key]); + }); + + return promise; + }); + + function setupEncKey() { + var keystore = JWK.store.KeyStore.createKeyStore(); + var jwk = { + kty: "oct", + kid: "someid", + use: "enc", + alg: "A128GCM", + k: util.base64url.encode("816e39070410cf2184904da03ea5075a", "hex") + }; + + return keystore.add(jwk); + } + function setupSigKey() { + var keystore = JWK.store.KeyStore.createKeyStore(); + var jwk = { + kty: "oct", + kid: "someid", + use: "sig", + alg: "HS256", + k: util.base64url.encode("9779d9120642797f1747025d5b22b7ac607cab08e1758f2f3a46c8be1e25c53b8c6a8f58ffefa176", "hex") + }; + + return keystore.add(jwk); + } + function setupWrapKey() { + var keystore = JWK.store.KeyStore.createKeyStore(); + var jwk = { + kty: "oct", + kid: "someid", + use: "enc", + alg: "A128GCMKW", + k: util.base64url.encode("e98b72a9881a84ca6b76e0f43e68647a", "hex") + }; + return keystore.add(jwk); + } + + it("imports a 'oct' encryption JWK", function() { + var promise = Promise.resolve(); + + promise = promise.then(setupEncKey); + promise = promise.then(function(key) { + assert.equal(key.kty, "oct"); + assert.equal(key.length, 128); + assert.equal(key.kid, "someid"); + assert.equal(key.get("k", true).toString("hex"), "816e39070410cf2184904da03ea5075a"); + assert.deepEqual(key.toJSON(true), { + kty: "oct", + kid: "someid", + k: util.base64url.encode("816e39070410cf2184904da03ea5075a", "hex"), + use: "enc", + alg: "A128GCM" + }); + }); + + return promise; + }); + + it("imports a 'oct' signing JWK", function() { + var promise = Promise.resolve(); + + promise = promise.then(setupSigKey); + promise = promise.then(function(key) { + assert.equal(key.kty, "oct"); + assert.equal(key.length, 320); + assert.equal(key.kid, "someid"); + assert.equal(key.get("k", true).toString("hex"), "9779d9120642797f1747025d5b22b7ac607cab08e1758f2f3a46c8be1e25c53b8c6a8f58ffefa176"); + assert.deepEqual(key.toJSON(true), { + kty: "oct", + kid: "someid", + k: util.base64url.encode("9779d9120642797f1747025d5b22b7ac607cab08e1758f2f3a46c8be1e25c53b8c6a8f58ffefa176", "hex"), + use: "sig", + alg: "HS256" + }); + }); + + return promise; + }); + + it("imports a 'oct' wrapping JWK", function() { + var promise = Promise.resolve(); + + promise = promise.then(setupWrapKey); + promise = promise.then(function(key) { + assert.equal(key.kty, "oct"); + assert.equal(key.length, 128); + assert.equal(key.kid, "someid"); + assert.equal(key.get("k", true).toString("hex"), "e98b72a9881a84ca6b76e0f43e68647a"); + assert.deepEqual(key.toJSON(true), { + kty: "oct", + kid: "someid", + k: util.base64url.encode("e98b72a9881a84ca6b76e0f43e68647a", "hex"), + use: "enc", + alg: "A128GCMKW" + }); + }); + + return promise; + }); + + it("encrypts via JWK", function() { + var promise = setupEncKey(); + promise = promise.then(function(jwk) { + var props = { + iv: new Buffer("32c367a3362613b27fc3e67e", "hex"), + aad: new Buffer("f2a30728ed874ee02983c294435d3c16", "hex") + }; + + var pdata = new Buffer("ecafe96c67a1646744f1c891f5e69427", "hex"); + return jwk.encrypt("A128GCM", pdata, props); + }); + promise = promise.then(function(result) { + assert.equal(result.data.toString("hex"), "552ebe012e7bcf90fcef712f8344e8f1"); + assert.equal(result.tag.toString("hex"), "ecaae9fc68276a45ab0ca3cb9dd9539f"); + }); + + return promise; + }); + it("decrypts via JWK", function() { + var promise = setupEncKey(); + promise = promise.then(function(jwk) { + var props = { + iv: new Buffer("32c367a3362613b27fc3e67e", "hex"), + aad: new Buffer("f2a30728ed874ee02983c294435d3c16", "hex"), + tag: new Buffer("ecaae9fc68276a45ab0ca3cb9dd9539f", "hex") + }; + + var cdata = new Buffer("552ebe012e7bcf90fcef712f8344e8f1", "hex"); + return jwk.decrypt("A128GCM", cdata, props); + }); + promise = promise.then(function(result) { + assert.equal(result.toString("hex"), "ecafe96c67a1646744f1c891f5e69427"); + }); + + return promise; + }); + it("signs via JWK", function() { + var promise = setupSigKey(); + promise = promise.then(function(jwk) { + var pdata = new Buffer("b1689c2591eaf3c9e66070f8a77954ffb81749f1b00346f9dfe0b2ee905dcc288baf4a92de3f4001dd9f44c468c3d07d6c6ee82faceafc97c2fc0fc0601719d2dcd0aa2aec92d1b0ae933c65eb06a03c9c935c2bad0459810241347ab87e9f11adb30415424c6c7f5f22a003b8ab8de54f6ded0e3ab9245fa79568451dfa258e", "hex"); + return jwk.sign("HS256", pdata); + }); + promise = promise.then(function(result) { + assert.equal(result.data.toString("hex"), "b1689c2591eaf3c9e66070f8a77954ffb81749f1b00346f9dfe0b2ee905dcc288baf4a92de3f4001dd9f44c468c3d07d6c6ee82faceafc97c2fc0fc0601719d2dcd0aa2aec92d1b0ae933c65eb06a03c9c935c2bad0459810241347ab87e9f11adb30415424c6c7f5f22a003b8ab8de54f6ded0e3ab9245fa79568451dfa258e"); + assert.equal(result.mac.toString("hex"), "769f00d3e6a6cc1fb426a14a4f76c6462e6149726e0dee0ec0cf97a16605ac8b"); + }); + + return promise; + }); + it("verifies via JWK", function() { + var promise = setupSigKey(); + promise = promise.then(function(jwk) { + var pdata = new Buffer("b1689c2591eaf3c9e66070f8a77954ffb81749f1b00346f9dfe0b2ee905dcc288baf4a92de3f4001dd9f44c468c3d07d6c6ee82faceafc97c2fc0fc0601719d2dcd0aa2aec92d1b0ae933c65eb06a03c9c935c2bad0459810241347ab87e9f11adb30415424c6c7f5f22a003b8ab8de54f6ded0e3ab9245fa79568451dfa258e", "hex"); + var mac = new Buffer("769f00d3e6a6cc1fb426a14a4f76c6462e6149726e0dee0ec0cf97a16605ac8b", "hex"); + return jwk.verify("HS256", pdata, mac); + }); + promise = promise.then(function(result) { + assert.equal(result.data.toString("hex"), "b1689c2591eaf3c9e66070f8a77954ffb81749f1b00346f9dfe0b2ee905dcc288baf4a92de3f4001dd9f44c468c3d07d6c6ee82faceafc97c2fc0fc0601719d2dcd0aa2aec92d1b0ae933c65eb06a03c9c935c2bad0459810241347ab87e9f11adb30415424c6c7f5f22a003b8ab8de54f6ded0e3ab9245fa79568451dfa258e"); + assert.equal(result.mac.toString("hex"), "769f00d3e6a6cc1fb426a14a4f76c6462e6149726e0dee0ec0cf97a16605ac8b"); + assert.equal(result.valid, true); + }); + + return promise; + }); + it("wraps via JWK", function() { + var promise = setupWrapKey(); + promise = promise.then(function(jwk) { + var props = { + iv: new Buffer("8b23299fde174053f3d652ba", "hex") + }; + + var pdata = new Buffer("28286a321293253c3e0aa2704a278032", "hex"); + return jwk.wrap("A128GCMKW", pdata, props); + }); + promise = promise.then(function(result) { + assert.equal(result.data.toString("hex"), "5a3c1cf1985dbb8bed818036fdd5ab42"); + assert.equal(result.tag.toString("hex"), "23c7ab0f952b7091cd324835043b5eb5"); + }); + + return promise; + }); + }); +}); diff --git a/test/jwk/rsakey-test.js b/test/jwk/rsakey-test.js new file mode 100644 index 0000000..bae46e1 --- /dev/null +++ b/test/jwk/rsakey-test.js @@ -0,0 +1,355 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var bind = require("lodash.bind"); +var clone = require("lodash.clone"); +var merge = require("../../lib/util/merge"); +var omit = require("lodash.omit"); +var pick = require("lodash.pick"); +var assert = chai.assert; + +var JWK = { + RSA: require("../../lib/jwk/rsakey.js"), + BaseKey: require("../../lib/jwk/basekey.js"), + store: require("../../lib/jwk/keystore.js"), + helpers: require("../../lib/jwk/helpers.js"), + CONSTANTS: require("../../lib/jwk/constants.js") +}; +var util = require("../../lib/util"); + +describe("jwk/RSA", function() { + var keyProps = { + "kty": "RSA", + "n": "i8exGrwNz69aIKhpblb7DrouMBJJohuzhGPezA1EWZ8klqNt7kxKhd3fA1MN1Nn9QTIX4NefJxmOwzXhq6qLtfOa7ZnXUS-pWrs3pm9oFOf1qF1DbeEDTaTbkvxlO7AFUs_LR1-wHyztH0O-Rl17WqUUjcoA07QYwHCKm1cP_kE4yCkyT0EPNkreCnwQEs-1xvZkyAo_zLjESN8y_Ck9FTTTAWmuEbUhtE1_QmlCfFaoUsBJ5OJG6eCTmr1MQ47T4flKDq6-PFr4JCFyMrmnungxpsg4lp-s1sUgg5qRUyga6ze854pmAgQKzj61lhs8g7k1J5HR6S0PL7xQl5pW6Q", + "e": "AQAB", + "d": "TgcjLjFb5FuWjDR25klXzFjR_7O1tvCPvY-ih3XAeecEnbKNY0DjOOcp3sk2J2OopAQ6oCC9jy4NK5ugZhvF8cQS8B-4unFIsIViA16dU05JK7skMOoy1dz5VYvfVvpjfl7Qsv8PadfCZnmCdfUpLuiIGL5yx7r5NjOcrCplmyApr5KJ53Qk8q76WVbZvH4bxmzoK2sOhzjH_4I4bcdlueeUj6VNZXiReY2VpldsoIgmntEy1z7DMVRjqwvBFLM7yD4P9Dlk3pfHtSMtDMyAsnaco7cAA95t1Yk60Om9RlCf_CjbmempzQ_P6Ned9VJYUvtcadqIE0lhe4Pp4POgAQ", + "p": "y-7A4AGF8dkUjHKVXmPQ65ymWO2ACP8jR9LVrkRXd0uPXQyJqTa-233H0pqXRuMiTCnbYTr3oz4ePX53SDUsusDtWgGsrzTRozoHQoxA6cjLX-1VcjlgPpW2gzltlQWv7MBK-LaxGwlS3iuc9tuTE2vwvyPWBO3orQIF21ZoDGk", + "q": "r3fcBfzoik-U1cL_eNb3dFCnnCt3KsBSQj0zgSKhxcdQYXkWrUgx2F0nzN98T7zjBqDcuVtdRVe8JL1HN_J5fqdjNdfCyvT5asS-MccY-fZwMsdcsT1LLLqZDWd6TsMLMpzW_dTkPTWIKWJB27-DFS61mYafvah2HfWxt8ggpoE", + "dp": "lbOdOJNFrWTKldMjXRfu7Jag8lTeETyhvH7Dx1p5zqPUCN1ETMhYUK3DuxEqjan8qmZrmbN8yAO4lTG6BHKsdCdd1R23kyI15hmZ7Lsih7uTt8Z0XBZMVYT3ZtsIW0XCgAwkvPD3j75Ha7oeToSfMbmiD94RpKq0jBQZEosadEk", + "dq": "OcG2RrJMyNoRH5ukA96ebUbvJNSZ0RSk_vCuN19y6GsG5k65TChrX9Cp_SHDBWwjPldM0CZmuSB76Yv0GVJS84GdgmeW0r94KdDA2hmy-vRHUi-VLzIBwKNbJbJd6_b_hJVjnwGobw1j2FtjWjXbq-lIFVTe18rPtmTdLqVNOgE", + "qi": "YYCsHYc8qLJ1aIWnVJ9srXBC3VPWhB98tjOdK-xafhi19TeDL3OxazFV0f0FuxEGOmYeHyF4nh72wK3kRBrcosNQkAlK8oMH3Cg_AnMYehFRmDSKUFjDjXH5bVBfFk72FkmEywEaQgOiYs34P4RAEBdZohh6UTZm0-bajOkVEOE" + }; + var keyPair; + + function formatKeyPair(json) { + function convert(src, name) { + this[name] = util.base64url.decode(src[name]); + } + var result = { + public: {}, + private: {} + }; + + ["n", "e"].forEach(bind(convert, result.public, json)); + result.public.length = result.public.n.length * 8; + + ["n", "e", "d", "p", "q", "dp", "dq", "qi"].forEach(bind(convert, result.private, json)); + result.public.kty = result.private.kty = "RSA"; + result.public.length = result.private.length = result.public.n.length * 8; + + return result; + } + + beforeEach(function() { + keyPair = formatKeyPair(keyProps); + }); + + describe("#publicKey", function() { + it("prepares a publicKey", function() { + var props = clone(keyProps), + actual, + expected; + + actual = JWK.RSA.config.publicKey(props); + expected = { + "kty": "RSA", + "n": util.base64url.decode("i8exGrwNz69aIKhpblb7DrouMBJJohuzhGPezA1EWZ8klqNt7kxKhd3fA1MN1Nn9QTIX4NefJxmOwzXhq6qLtfOa7ZnXUS-pWrs3pm9oFOf1qF1DbeEDTaTbkvxlO7AFUs_LR1-wHyztH0O-Rl17WqUUjcoA07QYwHCKm1cP_kE4yCkyT0EPNkreCnwQEs-1xvZkyAo_zLjESN8y_Ck9FTTTAWmuEbUhtE1_QmlCfFaoUsBJ5OJG6eCTmr1MQ47T4flKDq6-PFr4JCFyMrmnungxpsg4lp-s1sUgg5qRUyga6ze854pmAgQKzj61lhs8g7k1J5HR6S0PL7xQl5pW6Q"), + "e": util.base64url.decode("AQAB"), + length: 2048 + }; + assert.deepEqual(actual, expected); + }); + it("prepares a publicKey with missing values", function() { + var props, + actual, + expected; + + props = omit(keyProps, "n", "e"); + actual = JWK.RSA.config.publicKey(props); + expected = { + "kty": "RSA" + }; + assert.deepEqual(actual, expected); + + props = omit(keyProps, "n"); + actual = JWK.RSA.config.publicKey(props); + expected = { + "kty": "RSA" + }; + assert.deepEqual(actual, expected); + + props = omit(keyProps, "e"); + actual = JWK.RSA.config.publicKey(props); + expected = { + "kty": "RSA" + }; + assert.deepEqual(actual, expected); + }); + }); + describe("#privateKey", function() { + it("prepares a privateKey", function() { + var props = clone(keyProps), + actual, + expected; + + actual = JWK.RSA.config.privateKey(props); + expected = { + "kty": "RSA", + "n": util.base64url.decode("i8exGrwNz69aIKhpblb7DrouMBJJohuzhGPezA1EWZ8klqNt7kxKhd3fA1MN1Nn9QTIX4NefJxmOwzXhq6qLtfOa7ZnXUS-pWrs3pm9oFOf1qF1DbeEDTaTbkvxlO7AFUs_LR1-wHyztH0O-Rl17WqUUjcoA07QYwHCKm1cP_kE4yCkyT0EPNkreCnwQEs-1xvZkyAo_zLjESN8y_Ck9FTTTAWmuEbUhtE1_QmlCfFaoUsBJ5OJG6eCTmr1MQ47T4flKDq6-PFr4JCFyMrmnungxpsg4lp-s1sUgg5qRUyga6ze854pmAgQKzj61lhs8g7k1J5HR6S0PL7xQl5pW6Q"), + "e": util.base64url.decode("AQAB"), + "d": util.base64url.decode("TgcjLjFb5FuWjDR25klXzFjR_7O1tvCPvY-ih3XAeecEnbKNY0DjOOcp3sk2J2OopAQ6oCC9jy4NK5ugZhvF8cQS8B-4unFIsIViA16dU05JK7skMOoy1dz5VYvfVvpjfl7Qsv8PadfCZnmCdfUpLuiIGL5yx7r5NjOcrCplmyApr5KJ53Qk8q76WVbZvH4bxmzoK2sOhzjH_4I4bcdlueeUj6VNZXiReY2VpldsoIgmntEy1z7DMVRjqwvBFLM7yD4P9Dlk3pfHtSMtDMyAsnaco7cAA95t1Yk60Om9RlCf_CjbmempzQ_P6Ned9VJYUvtcadqIE0lhe4Pp4POgAQ"), + "p": util.base64url.decode("y-7A4AGF8dkUjHKVXmPQ65ymWO2ACP8jR9LVrkRXd0uPXQyJqTa-233H0pqXRuMiTCnbYTr3oz4ePX53SDUsusDtWgGsrzTRozoHQoxA6cjLX-1VcjlgPpW2gzltlQWv7MBK-LaxGwlS3iuc9tuTE2vwvyPWBO3orQIF21ZoDGk"), + "q": util.base64url.decode("r3fcBfzoik-U1cL_eNb3dFCnnCt3KsBSQj0zgSKhxcdQYXkWrUgx2F0nzN98T7zjBqDcuVtdRVe8JL1HN_J5fqdjNdfCyvT5asS-MccY-fZwMsdcsT1LLLqZDWd6TsMLMpzW_dTkPTWIKWJB27-DFS61mYafvah2HfWxt8ggpoE"), + "dp": util.base64url.decode("lbOdOJNFrWTKldMjXRfu7Jag8lTeETyhvH7Dx1p5zqPUCN1ETMhYUK3DuxEqjan8qmZrmbN8yAO4lTG6BHKsdCdd1R23kyI15hmZ7Lsih7uTt8Z0XBZMVYT3ZtsIW0XCgAwkvPD3j75Ha7oeToSfMbmiD94RpKq0jBQZEosadEk"), + "dq": util.base64url.decode("OcG2RrJMyNoRH5ukA96ebUbvJNSZ0RSk_vCuN19y6GsG5k65TChrX9Cp_SHDBWwjPldM0CZmuSB76Yv0GVJS84GdgmeW0r94KdDA2hmy-vRHUi-VLzIBwKNbJbJd6_b_hJVjnwGobw1j2FtjWjXbq-lIFVTe18rPtmTdLqVNOgE"), + "qi": util.base64url.decode("YYCsHYc8qLJ1aIWnVJ9srXBC3VPWhB98tjOdK-xafhi19TeDL3OxazFV0f0FuxEGOmYeHyF4nh72wK3kRBrcosNQkAlK8oMH3Cg_AnMYehFRmDSKUFjDjXH5bVBfFk72FkmEywEaQgOiYs34P4RAEBdZohh6UTZm0-bajOkVEOE"), + length: 2048 + }; + assert.deepEqual(actual, expected); + }); + it("returns undefined for missing values", function() { + var props, + actual; + + props = omit(keyProps, "n", "e", "d", "p", "q", "dp", "dq", "qi"); + actual = JWK.RSA.config.privateKey(props); + assert.deepEqual(actual, undefined); + + props = omit(keyProps, "d"); + actual = JWK.RSA.config.privateKey(props); + assert.deepEqual(actual, undefined); + + props = omit(keyProps, "n", "e"); + actual = JWK.RSA.config.privateKey(props); + assert.deepEqual(actual, undefined); + + props = omit(keyProps, "p", "q"); + actual = JWK.RSA.config.privateKey(props); + assert.deepEqual(actual, undefined); + }); + }); + describe("#wrapKey", function() { + it("returns key value", function() { + var keys = clone(keyPair); + + var result = JWK.RSA.config.wrapKey("RSA-OAEP", keys); + assert.strictEqual(result, keys.public); + }); + it("returns undefined for missing keys.public", function() { + var keys = omit(keyPair, "public"); + + var result = JWK.RSA.config.wrapKey("RSA-OAEP", keys); + assert.isUndefined(result); + }); + }); + describe("#unwrapKey", function() { + it("returns key value", function() { + var keys = clone(keyPair); + + var result = JWK.RSA.config.unwrapKey("RSA-OAEP", keys); + assert.strictEqual(result, keys.private); + }); + it("returns undefined for missing keys.private", function() { + var keys = omit(keyPair, "private"); + + var result = JWK.RSA.config.unwrapKey("RSA-OAEP", keys); + assert.isUndefined(result); + }); + }); + describe("#signKey", function() { + it("returns key value", function() { + var keys = clone(keyPair); + + var result = JWK.RSA.config.signKey("PS256", keys); + assert.strictEqual(result, keys.private); + }); + it("returns undefined for missing keys.private", function() { + var keys = omit(keyPair, "private"); + + var result = JWK.RSA.config.signKey("PS256", keys); + assert.isUndefined(result); + }); + }); + describe("#verifyKey", function() { + it("returns key value", function() { + var keys = clone(keyPair); + + var result = JWK.RSA.config.verifyKey("PS256", keys); + assert.strictEqual(result, keys.public); + }); + it("returns undefined for missing keys.public", function() { + var keys = omit(keyPair, "public"); + + var result = JWK.RSA.config.verifyKey("PS256", keys); + assert.isUndefined(result); + }); + }); + describe("#algorithms", function() { + it("returns suite for public key", function() { + var keys = pick(keyPair, "public"); + var algs; + + algs = JWK.RSA.config.algorithms(keys, "encrypt"); + assert.deepEqual(algs, []); + algs = JWK.RSA.config.algorithms(keys, "decrypt"); + assert.deepEqual(algs, []); + + algs = JWK.RSA.config.algorithms(keys, "wrap"); + assert.deepEqual(algs, ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"]); + + algs = JWK.RSA.config.algorithms(keys, "unwrap"); + assert.deepEqual(algs, []); + + algs = JWK.RSA.config.algorithms(keys, "sign"); + assert.deepEqual(algs, []); + + algs = JWK.RSA.config.algorithms(keys, "verify"); + assert.deepEqual(algs, ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"]); + }); + it("returns suite for private key", function() { + var keys = pick(keyPair, "private"); + var algs; + + algs = JWK.RSA.config.algorithms(keys, "encrypt"); + assert.deepEqual(algs, []); + algs = JWK.RSA.config.algorithms(keys, "decrypt"); + assert.deepEqual(algs, []); + + algs = JWK.RSA.config.algorithms(keys, "wrap"); + assert.deepEqual(algs, []); + + algs = JWK.RSA.config.algorithms(keys, "unwrap"); + assert.deepEqual(algs, ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"]); + + algs = JWK.RSA.config.algorithms(keys, "sign"); + assert.deepEqual(algs, ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"]); + + algs = JWK.RSA.config.algorithms(keys, "verify"); + assert.deepEqual(algs, []); + }); + }); + describe("keystore integration", function() { + it("generates a 'RSA' JWK", function() { + var keystore = JWK.store.KeyStore.createKeyStore(); + + var promise = keystore.generate("RSA", 2048); + promise = promise.then(function(key) { + assert.equal(key.kty, "RSA"); + assert.equal(key.length, 2048); + assert.ok(!!key.get("n")); + assert.ok(!!key.get("e")); + assert.ok(!!key.get("d", true)); + assert.ok(!!key.get("p", true)); + assert.ok(!!key.get("q", true)); + assert.ok(!!key.get("dp", true)); + assert.ok(!!key.get("dq", true)); + assert.ok(!!key.get("qi", true)); + }); + + return promise; + }); + it("generates a 'RSA' JWK with props", function() { + var keystore = JWK.store.KeyStore.createKeyStore(); + + var props = { + kid: "someid", + use: "sig", + alg: "PS256" + }; + + var promise = keystore.generate("RSA", 2048, props); + promise = promise.then(function(key) { + assert.equal(key.kty, "RSA"); + assert.equal(key.length, 2048); + assert.equal(key.kid, "someid"); + assert.equal(key.get("use"), "sig"); + assert.equal(key.get("alg"), "PS256"); + assert.ok(!!key.get("n")); + assert.ok(!!key.get("e")); + assert.ok(!!key.get("d", true)); + assert.ok(!!key.get("p", true)); + assert.ok(!!key.get("q", true)); + assert.ok(!!key.get("dp", true)); + assert.ok(!!key.get("dq", true)); + assert.ok(!!key.get("qi", true)); + }); + + return promise; + }); + + function setupSigKey() { + var keystore = JWK.store.KeyStore.createKeyStore(); + var jwk = merge({}, keyProps, { + kid: "someid", + use: "sig", + alg: "PS256" + }); + + return keystore.add(jwk); + } + function setupWrapKey() { + var keystore = JWK.store.KeyStore.createKeyStore(); + var jwk = merge({}, keyProps, { + kid: "someid", + use: "enc", + alg: "RSA-OAEP" + }); + + return keystore.add(jwk); + } + + it("imports a 'RSA' signing JWK", function() { + var promise = Promise.resolve(); + + promise = promise.then(setupSigKey); + promise = promise.then(function(key) { + assert.equal(key.kty, "RSA"); + assert.equal(key.length, 2048); + assert.equal(key.kid, "someid"); + + var json = merge({}, keyProps, { + use: "sig", + alg: "PS256", + kid: "someid" + }); + assert.deepEqual(key.toJSON(true), json); + }); + + return promise; + }); + + it("imports a 'RSA' wrapping JWK", function() { + var promise = Promise.resolve(); + + promise = promise.then(setupWrapKey); + promise = promise.then(function(key) { + assert.equal(key.kty, "RSA"); + assert.equal(key.length, 2048); + assert.equal(key.kid, "someid"); + + var json = merge({}, keyProps, { + use: "enc", + alg: "RSA-OAEP", + kid: "someid" + }); + assert.deepEqual(key.toJSON(true), json); + }); + + return promise; + }); + }); +}); diff --git a/test/jws/jws-test.js b/test/jws/jws-test.js new file mode 100644 index 0000000..95e92ff --- /dev/null +++ b/test/jws/jws-test.js @@ -0,0 +1,125 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var cloneDeep = require("lodash.clonedeep"); +var forEach = require("lodash.foreach"); +var chai = require("chai"); + +var JWS = require("../../lib/jws"); + +var JWK = require("../../lib/jwk"); + +var assert = chai.assert; + +var fixtures = { + "4_1.rsa_v15_signature": cloneDeep(require("jose-cookbook/jws/4_1.rsa_v15_signature.json")), + "4_2.rsa-pss_signature": cloneDeep(require("jose-cookbook/jws/4_2.rsa-pss_signature.json")), + "4_3.ecdsa_signature": cloneDeep(require("jose-cookbook/jws/4_3.ecdsa_signature.json")), + "4_4.hmac-sha2_integrity_protection": cloneDeep(require("jose-cookbook/jws/4_4.hmac-sha2_integrity_protection.json")) +}; + +describe("jws", function() { + forEach(fixtures, function(fixture) { + var input = fixture.input; + var output = fixture.output; + + // TODO figure out how to generate description from fixture values + describe(fixture.title, function() { + before(function keyToJWK() { + // coerse the key object ot a JWK object + return JWK.asKey(input.key) + .then(function(key) { + input.key = key; + assert(JWK.isKey(input.key)); + }); + }); + + // signing + if (fixture.reproducible) { + it("signs to a compact JWS", function() { + var options = { + compact: true, + protect: "*" + }; + + var signer = JWS.createSign(options, input.key); + return signer.final(input.payload, "utf8"). + then(function(result) { + assert.deepEqual(result, output.compact); + }); + }); + it("signs to a general JSON JWS", function() { + var options = { + compact: false, + protect: "*" + }; + + var signer = JWS.createSign(options, input.key); + return signer.final(input.payload, "utf8"). + then(function(result) { + assert.deepEqual(result, output.json); + }); + }); + it("signs to a flattened JSON JWS", function() { + var options = { + format: "flattened", + protect: "*" + }; + + var signer = JWS.createSign(options, input.key); + return signer.final(input.payload, "utf8"). + then(function(result) { + assert.deepEqual(result, output.json_flat); + }); + }); + } + + // verifying + it("verifies from a compact JWS", function() { + var verifier = JWS.createVerify(input.key); + return verifier.verify(output.compact). + then(function(result) { + // result.payload is a buffer, assert.equal will invoke its + // toString() method implicitly + assert.equal(result.payload, input.payload); + + // But let's make it clear that result.payload needs to be + // converted before actually being a string. + var payload = result.payload.toString(); + assert.deepEqual(payload, input.payload); + }); + }); + it("verifies from a general JSON JWS", function() { + var verifier = JWS.createVerify(input.key); + return verifier.verify(output.json). + then(function(result) { + // result.payload is a buffer, assert.equal will invoke its + // toString() method implicitly + assert.equal(result.payload, input.payload); + + // But let's make it clear that result.payload needs to be + // converted before actually being a string. + var payload = result.payload.toString(); + assert.deepEqual(payload, input.payload); + }); + }); + it("verifies from a flattened JSON JWS", function() { + var verifier = JWS.createVerify(input.key); + return verifier.verify(output.json_flat). + then(function(result) { + // result.payload is a buffer, assert.equal will invoke its + // toString() method implicitly + assert.equal(result.payload, input.payload); + + // But let's make it clear that result.payload needs to be + // converted before actually being a string. + var payload = result.payload.toString(); + assert.deepEqual(payload, input.payload); + }); + }); + }); + }); +}); diff --git a/test/util/base64url-test.js b/test/util/base64url-test.js new file mode 100644 index 0000000..83eb392 --- /dev/null +++ b/test/util/base64url-test.js @@ -0,0 +1,110 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var utils = { + base64url: require("../../lib/util/base64url.js") +}; + +describe("util/base64url", function() { + it("should encode a node.js Buffer", function() { + var input = new Buffer("‹hello world!›", "utf8"); + var output = utils.base64url.encode(input); + assert.equal(output, "4oC5aGVsbG8gd29ybGQh4oC6"); + }); + + it("should encode an ArrayBuffer", function() { + var input = new Uint8Array([226, 128, 185, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 226, 128, 186]). + buffer; + var output = utils.base64url.encode(input); + assert.equal(output, "4oC5aGVsbG8gd29ybGQh4oC6"); + }); + + it("should encode an ArrayBufferView", function() { + var input = new Uint8Array([226, 128, 185, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 226, 128, 186]); + var output = utils.base64url.encode(input); + assert.equal(output, "4oC5aGVsbG8gd29ybGQh4oC6"); + }); + + it("should encode an array", function() { + var input = [226, 128, 185, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 226, 128, 186]; + var output = utils.base64url.encode(input); + assert.equal(output, "4oC5aGVsbG8gd29ybGQh4oC6"); + }); + + it("should encode a (binary) string", function() { + var input = "\xe2\x80\xb9hello world!\xe2\x80\xba"; + var output = utils.base64url.encode(input); + assert.equal(output, "4oC5aGVsbG8gd29ybGQh4oC6"); + }); + + it("should encode a string with a specified encoding", function() { + var input, output; + + input = "‹hello world!›"; + output = utils.base64url.encode(input, "utf8"); + assert.equal(output, "4oC5aGVsbG8gd29ybGQh4oC6"); + + input = "\xe2\x80\xb9hello world!\xe2\x80\xba"; + output = utils.base64url.encode(input, "binary"); + assert.equal(output, "4oC5aGVsbG8gd29ybGQh4oC6"); + + input = "e280b968656c6c6f20776f726c6421e280ba"; + output = utils.base64url.encode(input, "hex"); + assert.equal(output, "4oC5aGVsbG8gd29ybGQh4oC6"); + }); + + it("should encode the rainbow!", function() { + var input, output; + + input = "3dfbff39ebbe35db7d31cb3c2dbafb29aaba259a79218a381d79f71969b61559751149340d38f30928b2051871010830"; + output = utils.base64url.encode(input, "hex"); + assert.equal(output, "Pfv_Oeu-Ndt9Mcs8Lbr7Kaq6JZp5IYo4HXn3GWm2FVl1EUk0DTjzCSiyBRhxAQgw"); + }); + + it("should encode without padding", function() { + var input, output; + input = "hello!!"; + output = utils.base64url.encode(input, "utf8"); + assert.equal(output, "aGVsbG8hIQ"); + }); + + it("should decode a string to a node.js Buffer", function() { + var input, output; + + input = "4oC5aGVsbG8gd29ybGQh4oC6"; + output = utils.base64url.decode(input); + + var expected = new Buffer([226, 128, 185, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 226, 128, 186]); + assert.deepEqual(output, expected); + }); + + it("should decode a string to a string", function() { + var input, output; + + input = "4oC5aGVsbG8gd29ybGQh4oC6"; + output = utils.base64url.decode(input, "binary"); + assert.equal(output, "\xe2\x80\xb9hello world!\xe2\x80\xba"); + + input = "4oC5aGVsbG8gd29ybGQh4oC6"; + output = utils.base64url.decode(input, "utf8"); + assert.equal(output, "‹hello world!›"); + + input = "4oC5aGVsbG8gd29ybGQh4oC6"; + output = utils.base64url.decode(input, "hex"); + assert.equal(output, "e280b968656c6c6f20776f726c6421e280ba"); + }); + + it("should decode the rainbow!", function() { + var input, output; + + input = "Pfv_Oeu-Ndt9Mcs8Lbr7Kaq6JZp5IYo4HXn3GWm2FVl1EUk0DTjzCSiyBRhxAQgw"; + output = utils.base64url.decode(input, "hex"); + assert.equal(output, "3dfbff39ebbe35db7d31cb3c2dbafb29aaba259a79218a381d79f71969b61559751149340d38f30928b2051871010830"); + }); +}); diff --git a/test/util/databuffer-test.js b/test/util/databuffer-test.js new file mode 100644 index 0000000..1751597 --- /dev/null +++ b/test/util/databuffer-test.js @@ -0,0 +1,631 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var utils = { + DataBuffer: require("../../lib/util/databuffer.js") +}; + +function assertArrayEqual(a1, a2) { + assert.equal(a1.length, a2.length); + for (var idx = 0; idx < a2.length; idx++) { + assert.equal(a1[idx], a2[idx], "element " + idx + " not equal"); + } +} + +describe("util/DataBuffer", function() { + it("creates a default empty DataBuffer", function() { + var buffer; + buffer = new utils.DataBuffer(); + assert.equal(buffer.growSize, 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 0); + assert.ok(Buffer.isBuffer(buffer.data)); + assert.equal(buffer.length(), 0); + assert.equal(buffer.available(), 16); + assert.ok(buffer.isEmpty()); + }); + + it("creates a DataBuffer from ArrayBuffer", function() { + var input = new Uint8Array(31); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + + var buffer; + buffer = new utils.DataBuffer(input.buffer); + assert.equal(buffer.growSize, 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 31); + assert.ok(Buffer.isBuffer(buffer.data)); + assert.equal(buffer.length(), 31); + assert.equal(buffer.available(), 0); + assertArrayEqual(buffer.data, input); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from ArrayBufferView", function() { + var input = new Uint8Array(31); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + + var buffer; + buffer = new utils.DataBuffer(input); + assert.equal(buffer.growSize, 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 31); + assert.ok(Buffer.isBuffer(buffer.data)); + assert.equal(buffer.length(), 31); + assert.equal(buffer.available(), 0); + assertArrayEqual(buffer.data, input); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from nodejs Buffer", function() { + var input = new Buffer(31); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + + var buffer; + buffer = new utils.DataBuffer(input); + assert.equal(buffer.growSize, 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 31); + assert.ok(Buffer.isBuffer(buffer.data)); + assert.equal(buffer.length(), 31); + assert.equal(buffer.available(), 0); + assert.strictEqual(buffer.data, input); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from an array", function() { + var input = [104, 101, 108, 108, 111, 32, 116, 104, 101, 114, 101]; + + var buffer = new utils.DataBuffer(input); + assert.equal(buffer.growSize, 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, input.length); + assert.equal(buffer.length(), input.length); + assert.equal(buffer.available(), 16 - input.length); + assertArrayEqual(buffer.data.slice(0, input.length), input); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from string", function() { + var input = "hello there"; + + var buffer = new utils.DataBuffer(input); + assert.equal(buffer.growSize, 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, input.length); + assert.equal(buffer.length(), input.length); + assert.equal(buffer.available(), 16 - input.length); + + var expected = []; + for (var idx = 0; idx < input.length; idx++) { + expected[idx] = input.charCodeAt(idx); + } + assertArrayEqual(buffer.data.slice(0, expected.length), expected); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from another DataBuffer", function() { + var expected = [104, 101, 108, 108, 111, 32, 116, 104, 101, 114, 101]; + var input = new utils.DataBuffer(expected); + + var buffer = new utils.DataBuffer(input); + assert.equal(buffer.growSize, 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, expected.length); + assert.notStrictEqual(buffer.data, input.data); + assert.equal(buffer.length(), expected.length); + assert.equal(buffer.available(), 16 - expected.length); + assertArrayEqual(buffer.data.slice(0, expected.length), expected); + assert.ok(!buffer.isEmpty()); + }); + + it("creates an empty DataBuffer with options", function() { + var buffer = new utils.DataBuffer(null, { + readOffset: 5, + writeOffset: 10, + growSize: 1024 + }); + assert.equal(buffer.growSize, 1024); + assert.equal(buffer.read, 5); + assert.equal(buffer.write, 10); + assert.ok(Buffer.isBuffer(buffer.data)); + assert.equal(buffer.length(), 5); + assert.equal(buffer.available(), 1014); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from ArrayBuffer with options", function() { + var input = new Uint8Array(31); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + + var buffer; + buffer = new utils.DataBuffer(input.buffer, { + readOffset: 5, + writeOffset: 10, + growSize: 1024 + }); + assert.equal(buffer.growSize, 1024); + assert.equal(buffer.read, 5); + assert.equal(buffer.write, 10); + assert.ok(Buffer.isBuffer(buffer.data)); + assert.equal(buffer.length(), 5); + assert.equal(buffer.available(), input.length - 10); + assertArrayEqual(buffer.data.slice(0, input.length), input); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from ArrayBufferView with options", function() { + var input = new Uint8Array(31); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + + var buffer; + buffer = new utils.DataBuffer(input, { + readOffset: 5, + writeOffset: 10, + growSize: 1024 + }); + assert.equal(buffer.growSize, 1024); + assert.equal(buffer.read, 5); + assert.equal(buffer.write, 10); + assert.ok(Buffer.isBuffer(buffer.data)); + assert.equal(buffer.length(), 5); + assert.equal(buffer.available(), input.length - 10); + assertArrayEqual(buffer.data.slice(0, input.length), input); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from nodejs Buffer with options", function() { + var input = new Buffer(31); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + + var buffer; + buffer = new utils.DataBuffer(input, { + readOffset: 5, + writeOffset: 10, + growSize: 1024 + }); + assert.equal(buffer.growSize, 1024); + assert.equal(buffer.read, 5); + assert.equal(buffer.write, 10); + assert.ok(Buffer.isBuffer(buffer.data)); + assert.equal(buffer.length(), 5); + assert.equal(buffer.available(), input.length - 10); + assert.strictEqual(buffer.data, input); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from an array with options", function() { + var input = [104, 101, 108, 108, 111, 32, 116, 104, 101, 114, 101]; + + var buffer = new utils.DataBuffer(input, { + readOffset: 5, + writeOffset: 10, + growSize: 1024 + }); + assert.equal(buffer.growSize, 1024); + assert.equal(buffer.read, 5); + assert.equal(buffer.write, 10); + assert.equal(buffer.length(), 5); + assert.equal(buffer.available(), 1014); + assertArrayEqual(buffer.data.slice(0, input.length), input); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from string with options", function() { + var input = "hello there"; + + var buffer = new utils.DataBuffer(input, { + readOffset: 5, + writeOffset: 10, + growSize: 1024 + }); + assert.equal(buffer.growSize, 1024); + assert.equal(buffer.read, 5); + assert.equal(buffer.write, 10); + assert.equal(buffer.length(), 5); + assert.equal(buffer.available(), 1014); + + var expected = []; + for (var idx = 0; idx < input.length; idx++) { + expected[idx] = input.charCodeAt(idx); + } + assertArrayEqual(buffer.data.slice(0, expected.length), expected); + assert.ok(!buffer.isEmpty()); + }); + + it("creates a DataBuffer from another DataBuffer with options", function() { + var expected = [104, 101, 108, 108, 111, 32, 116, 104, 101, 114, 101]; + var input = new utils.DataBuffer(expected); + + var buffer = new utils.DataBuffer(input, { + readOffset: 5, + writeOffset: 10, + growSize: 1024 + }); + assert.equal(buffer.growSize, 1024); + assert.equal(buffer.read, 5); + assert.equal(buffer.write, 10); + assert.notStrictEqual(buffer.data, input.data); + assert.equal(buffer.length(), 5); + assert.equal(buffer.available(), 1014); + assertArrayEqual(buffer.data.slice(0, expected.length), expected); + assert.ok(!buffer.isEmpty()); + }); + + it("returns the correct encoding for toString()", function() { + var buffer = new utils.DataBuffer([194, 171, 104, 101, 108, 108, 111, 32, 116, 104, 101, 114, 101, 194, 187]); + + assert.equal(buffer.toString("binary"), "\xc2\xabhello there\xc2\xbb"); + assert.equal(buffer.toString("raw"), "\xc2\xabhello there\xc2\xbb"); + assert.equal(buffer.toString("hex"), "c2ab68656c6c6f207468657265c2bb"); + assert.equal(buffer.toString("utf8"), "«hello there»"); + assert.equal(buffer.toString("base64"), "wqtoZWxsbyB0aGVyZcK7"); + assert.equal(buffer.toString("base64url"), "wqtoZWxsbyB0aGVyZcK7"); + }); + + it("puts strings of various encodings", function() { + var buffer; + + buffer = new utils.DataBuffer(); + buffer.putBytes("\xc2\xabhello there\xc2\xbb", "binary"); + assert.equal(buffer.toString("binary"), "\xc2\xabhello there\xc2\xbb"); + + buffer = new utils.DataBuffer(); + buffer.putBytes("\xc2\xabhello there\xc2\xbb", "raw"); + assert.equal(buffer.toString("binary"), "\xc2\xabhello there\xc2\xbb"); + + buffer = new utils.DataBuffer(); + buffer.putBytes("c2ab68656c6c6f207468657265c2bb", "hex"); + assert.equal(buffer.toString("binary"), "\xc2\xabhello there\xc2\xbb"); + + buffer = new utils.DataBuffer(); + buffer.putBytes("«hello there»", "utf8"); + assert.equal(buffer.toString("binary"), "\xc2\xabhello there\xc2\xbb"); + + buffer = new utils.DataBuffer(); + buffer.putBytes("wqtoZWxsbyB0aGVyZcK7", "base64"); + assert.equal(buffer.toString("binary"), "\xc2\xabhello there\xc2\xbb"); + + buffer = new utils.DataBuffer(); + buffer.putBytes("wqtoZWxsbyB0aGVyZcK7", "base64url"); + assert.equal(buffer.toString("binary"), "\xc2\xabhello there\xc2\xbb"); + }); + + it("writes one byte at a time", function() { + var buffer = new utils.DataBuffer(); + + for (var idx = 0; idx < 33; idx++) { + var b = idx % 256; + buffer.putByte(b); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, idx + 1); + assert.equal(buffer.data[idx], b); + } + }); + + it("reads one byte at a time", function() { + var input = new Uint8Array(33); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + + var buffer = new utils.DataBuffer(input); + for (idx = 0; 0 > buffer.length(); idx++) { + var b = buffer.getByte(); + assert.equal(buffer.read, idx + 1); + assert.equal(buffer.write, 33); + assert.equal(b, idx % 256); + } + }); + + it("writes a big-endian halfword at a time", function() { + var buffer = new utils.DataBuffer(); + + for (var idx = 0; idx < 33; idx++) { + var h = ((idx % 256) << 8) + (idx % 256); + buffer.putInt16(h); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, (idx + 1) * 2); + assert.equal(buffer.data.readUInt16BE(idx * 2), h); + } + }); + + it("reads a big-endian halfword at a time", function() { + var input = new Uint16Array(33); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = ((idx % 256) << 8) + (idx % 256); + } + + var buffer = new utils.DataBuffer(input.buffer); + for (idx = 0; 0 > buffer.length(); idx++) { + var h = buffer.getInt16(); + assert.equal(buffer.read, (idx + 1) * 2); + assert.equal(buffer.write, 66); + assert.equal(h, ((idx % 256) << 8) + (idx % 256)); + } + }); + + it("writes a big-endian word at a time", function() { + var buffer = new utils.DataBuffer(); + + var w = (255 << 24) | (254 << 16) | (253 << 8) | (252); + buffer.putInt32(w); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 4); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), new Buffer("fffefdfc", "hex")); + + w = (129 << 24) | (128 << 16) | (127 << 8) | 126; + buffer.read = 4; + buffer.putInt32(w); + assert.equal(buffer.read, 4); + assert.equal(buffer.write, 8); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), new Buffer("81807f7e", "hex")); + }); + + it("reads a big-endian word at a time", function() { + var input = new Uint32Array(33); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = ((idx % 256) << 24) | ((idx % 256) << 16) | ((idx % 256) << 8) | (idx % 256); + } + + var buffer = new utils.DataBuffer(input.buffer); + for (idx = 0; 0 > buffer.length(); idx++) { + var w = buffer.getInt32(); + assert.equal(buffer.read, (idx = 1) * 4); + assert.equal(buffer.write, 132); + assert.equal(w, input[idx]); + } + }); + + it("fills a DataBuffer with a specific value", function() { + var buffer = new utils.DataBuffer(), + expected; + + expected = new Buffer("a5a5a5a5a5", "hex"); + buffer.fillWithByte(0xa5, 5); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 5); + assertArrayEqual(buffer.data.slice(0, 5), expected); + + expected = new Buffer("a5a5a5a5a5bcbcbc", "hex"); + buffer.fillWithByte(0xbc, 3); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 8); + assertArrayEqual(buffer.data.slice(0, 8), expected); + + expected = new Buffer("01010101010101010101010101010101", "hex"); + buffer = new utils.DataBuffer(); + buffer.fillWithByte(0x01); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 16); + assertArrayEqual(buffer.data.slice(0, 16), expected); + }); + + it("truncates a DataBuffer", function() { + var buffer; + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assert.strictEqual(buffer.truncate(5), buffer); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 11); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), new Buffer("000102030405060708090a", "hex")); + assertArrayEqual(buffer.data.slice(buffer.write), new Buffer("0000000000", "hex")); + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assert.strictEqual(buffer.truncate(20), buffer); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 0); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), new Buffer("", "hex")); + assertArrayEqual(buffer.data.slice(buffer.write), new Buffer("00000000000000000000000000000000", "hex")); + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + buffer.read = 5; + assert.strictEqual(buffer.truncate(5), buffer); + assert.equal(buffer.read, 5); + assert.equal(buffer.write, 11); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), new Buffer("05060708090a", "hex")); + assertArrayEqual(buffer.data.slice(buffer.write), new Buffer("0000000000", "hex")); + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + buffer.read = 5; + assert.strictEqual(buffer.truncate(20), buffer); + assert.equal(buffer.read, 5); + assert.equal(buffer.write, 5); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), new Buffer("", "hex")); + assertArrayEqual(buffer.data.slice(buffer.write), new Buffer("0000000000000000000000", "hex")); + }); + + it("compacts a DataBuffer", function() { + var buffer; + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + // does nothing + assert.strictEqual(buffer.compact(), buffer); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 16); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + + buffer.read = 5; + assert.strictEqual(buffer.compact(), buffer); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 11); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), new Buffer("05060708090a0b0c0d0e0f", "hex")); + assertArrayEqual(buffer.data.slice(buffer.write), new Buffer("0000000000", "hex")); + + buffer.read = buffer.write; + assert.strictEqual(buffer.compact(), buffer); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 0); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), new Buffer("", "hex")); + assertArrayEqual(buffer.data.slice(buffer.write), new Buffer("00000000000000000000000000000000", "hex")); + }); + + it("returns a buffer (no changes)", function() { + var buffer; + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assertArrayEqual(buffer.buffer(), new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assert.equal(buffer.length(), 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 16); + + assertArrayEqual(buffer.buffer(4), new Buffer("00010203", "hex")); + assert.equal(buffer.length(), 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 16); + }); + + it("returns a buffer, consuming", function() { + var buffer; + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assertArrayEqual(buffer.getBuffer(), new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assert.equal(buffer.length(), 0); + assert.equal(buffer.read, 16); + assert.equal(buffer.write, 16); + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assertArrayEqual(buffer.getBuffer(4), new Buffer("00010203", "hex")); + assert.equal(buffer.length(), 12); + assert.equal(buffer.read, 4); + assert.equal(buffer.write, 16); + }); + + it("returns bytes (no changes)", function() { + var buffer; + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assert.equal(buffer.bytes(), new Buffer("000102030405060708090a0b0c0d0e0f", "hex").toString("binary")); + assert.equal(buffer.length(), 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 16); + + assert.equal(buffer.bytes(4), new Buffer("00010203", "hex").toString("binary")); + assert.equal(buffer.length(), 16); + assert.equal(buffer.read, 0); + assert.equal(buffer.write, 16); + }); + + it("returns bytes, consuming", function() { + var buffer; + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assert.equal(buffer.getBytes(), new Buffer("000102030405060708090a0b0c0d0e0f", "hex").toString("binary")); + assert.equal(buffer.length(), 0); + assert.equal(buffer.read, 16); + assert.equal(buffer.write, 16); + + buffer = new utils.DataBuffer(new Buffer("000102030405060708090a0b0c0d0e0f", "hex")); + assert.equal(buffer.getBytes(4), new Buffer("00010203", "hex").toString("binary")); + assert.equal(buffer.length(), 12); + assert.equal(buffer.read, 4); + assert.equal(buffer.write, 16); + }); + + it("tests equality", function() { + var input, buffer; + + input = buffer = new utils.DataBuffer("hello world"); + assert.ok(buffer.equals(input)); + + input = new utils.DataBuffer("hello world"); + assert.ok(buffer.equals(input)); + + input = new utils.DataBuffer("goodbye cruel world!"); + assert.ok(!buffer.equals(input)); + + input = new utils.DataBuffer("ehlo world!"); + assert.ok(!buffer.equals(input)); + + input = null; + assert.ok(!buffer.equals(input)); + }); + + it("tests for DataBuffer instances", function() { + var buffer; + + buffer = new utils.DataBuffer(); + assert.equal(utils.DataBuffer.isBuffer(buffer), true); + + buffer = new Uint8Array(31); + assert.equal(utils.DataBuffer.isBuffer(buffer), false); + + buffer = "not a buffer"; + assert.equal(utils.DataBuffer.isBuffer(buffer), false); + + buffer = null; + assert.equal(utils.DataBuffer.isBuffer(buffer), false); + + buffer = 42; + assert.equal(utils.DataBuffer.isBuffer(buffer), false); + }); + + it("(DataBuffer.asBuffer) 'converts' a DataBuffer as itself", function() { + var input, buffer; + + input = new Uint8Array(31); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + input = new utils.DataBuffer(input); + buffer = utils.DataBuffer.asBuffer(input); + assert.ok(utils.DataBuffer.isBuffer(buffer)); + assert.ok(input === buffer); + }); + + it("(DataBuffer.asBuffer) converts an ArrayBuffer to a DataBuffer", function() { + var input, buffer; + + input = new Uint8Array(31); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + input = input.buffer; + buffer = utils.DataBuffer.asBuffer(input); + assert.ok(utils.DataBuffer.isBuffer(buffer)); + assert.ok(input !== buffer); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), + new Uint8Array(input)); + }); + + it("(DataBuffer.asBuffer) converts an ArrayBufferView to a DataBuffer", function() { + var input, buffer; + + input = new Uint8Array(31); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + buffer = utils.DataBuffer.asBuffer(input); + assert.ok(utils.DataBuffer.isBuffer(buffer)); + assert.ok(input !== buffer); + assertArrayEqual(buffer.data.slice(buffer.read, buffer.write), input); + }); + + it("(DataBuffer.asBuffer) returns an empty buffer for null", function() { + var input, buffer; + + input = null; + buffer = utils.DataBuffer.asBuffer(input); + assert.ok(utils.DataBuffer.isBuffer(buffer)); + assert.ok(input !== buffer); + assert.equal(buffer.length(), 0); + }); +}); diff --git a/test/util/index-test.js b/test/util/index-test.js new file mode 100644 index 0000000..980b7ed --- /dev/null +++ b/test/util/index-test.js @@ -0,0 +1,102 @@ +/** + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; +var UTIL = require("../../lib/util"); + +describe("util", function() { + describe("#randomBytes", function() { + it("returns a Buffer of randomized bytes", function() { + var result; + + result = UTIL.randomBytes(12); + assert.equal(Buffer.isBuffer(result), true); + assert.equal(result.length, 12); + + result = UTIL.randomBytes(41); + assert.equal(Buffer.isBuffer(result), true); + assert.equal(result.length, 41); + + result = UTIL.randomBytes(4096); + assert.equal(Buffer.isBuffer(result), true); + assert.equal(result.length, 4096); + }); + }); + + describe("#asBuffer", function() { + it("returns a Buffer for an Array", function() { + var input, output; + + input = new Array(256); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + output = UTIL.asBuffer(input); + assert.equal(Buffer.isBuffer(output), true); + assert.deepEqual(output, new Buffer(input)); + }); + it("returns a Buffer for an Uint8Array", function() { + var input, output; + + input = new Uint8Array(256); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + output = UTIL.asBuffer(input); + assert.equal(Buffer.isBuffer(output), true); + assert.deepEqual(output, new Buffer(input)); + }); + it("returns a Buffer for an ArrayBuffer", function() { + var input, output; + + input = new Uint8Array(256); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + output = UTIL.asBuffer(input.buffer); + assert.equal(Buffer.isBuffer(output), true); + assert.deepEqual(output, new Buffer(input)); + }); + it("returns a Buffer for some other TypedArray", function() { + var input, output; + + input = new Uint8Array(256); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = idx % 256; + } + output = UTIL.asBuffer(new Float32Array(input.buffer)); + assert.equal(Buffer.isBuffer(output), true); + }); + it("retuns a Buffer for a binary string", function() { + var input, output; + + input = new Array(256); + for (var idx = 0; idx < input.length; idx++) { + input[idx] = String.fromCharCode(idx % 256); + } + output = UTIL.asBuffer(input.map(function(c) { return String.fromCharCode(c); }).join("")); + assert.equal(Buffer.isBuffer(output), true); + assert.deepEqual(output, new Buffer(input)); + }); + it("retuns a Buffer for a text string", function() { + var input, output; + + input = "hello there, world!"; + output = UTIL.asBuffer(input, "utf8"); + assert.equal(Buffer.isBuffer(output), true); + assert.deepEqual(output, new Buffer(input, "utf8")); + }); + it("returns a Buffer for a base64url string", function() { + var input, output; + + input = "aGVsbG8gdGhlcmUsIHdvcmxkIQ"; + output = UTIL.asBuffer(input, "base64url"); + assert.equal(Buffer.isBuffer(output), true); + assert.deepEqual(output, UTIL.base64url.decode(input)); + }); + }); +}); diff --git a/test/util/merge-test.js b/test/util/merge-test.js new file mode 100644 index 0000000..61fb891 --- /dev/null +++ b/test/util/merge-test.js @@ -0,0 +1,97 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var utils = { + merge: require("../../lib/util/merge") +}; + +describe("util/merge", function() { + it("maintains node.js Buffer", function() { + var expected = { + foo: "bar", + bar: new Buffer([0x62, 0x61, 0x7a]), + baz: 42 + }; + var actual = { + foo: "bar" + }; + actual = utils.merge(actual, { + baz: 42, + bar: new Buffer([0x62, 0x61, 0x7a]) + }); + assert.deepEqual(actual, expected); + assert.ok(Buffer.isBuffer(actual.bar)); + }); + it("maintains Uint8Array Buffer", function() { + var expected = { + foo: "bar", + bar: new Uint8Array([0x62, 0x61, 0x7a]), + baz: 42 + }; + var actual = { + foo: "bar" + }; + actual = utils.merge(actual, { + baz: 42, + bar: new Uint8Array([0x62, 0x61, 0x7a]) + }); + assert.deepEqual(actual, expected); + assert.ok(actual.bar instanceof Uint8Array); + }); + if ("undefined" !== typeof Uint8ClampedArray) { + it("maintains Uint8ClampedArray Buffer", function() { + var expected = { + foo: "bar", + bar: new Uint8ClampedArray([0x62, 0x61, 0x7a]), + baz: 42 + }; + var actual = { + foo: "bar" + }; + actual = utils.merge(actual, { + baz: 42, + bar: new Uint8ClampedArray([0x62, 0x61, 0x7a]) + }); + assert.deepEqual(actual, expected); + assert.ok(actual.bar instanceof Uint8ClampedArray); + }); + } + it("maintains Uint16Array Buffer", function() { + var expected = { + foo: "bar", + bar: new Uint16Array([0x62, 0x61, 0x7a]), + baz: 42 + }; + var actual = { + foo: "bar" + }; + actual = utils.merge(actual, { + baz: 42, + bar: new Uint16Array([0x62, 0x61, 0x7a]) + }); + assert.deepEqual(actual, expected); + assert.ok(actual.bar instanceof Uint16Array); + }); + it("maintains Uint32Array Buffer", function() { + var expected = { + foo: "bar", + bar: new Uint32Array([0x62, 0x61, 0x7a]), + baz: 42 + }; + var actual = { + foo: "bar" + }; + actual = utils.merge(actual, { + baz: 42, + bar: new Uint32Array([0x62, 0x61, 0x7a]) + }); + assert.deepEqual(actual, expected); + assert.ok(actual.bar instanceof Uint32Array); + }); +}); diff --git a/test/util/utf8-test.js b/test/util/utf8-test.js new file mode 100644 index 0000000..249dbe6 --- /dev/null +++ b/test/util/utf8-test.js @@ -0,0 +1,62 @@ +/*! + * + * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. + */ +"use strict"; + +var chai = require("chai"); +var assert = chai.assert; + +var utils = { + utf8: require("../../lib/util/utf8.js") +}; + +describe("util/utf8", function() { + it("should encode an ascii string unchanged", function() { + var input = "hello world!"; + var output = utils.utf8.encode(input); + assert.equal(output, input); + }); + + it("should encode a single UCS-2 character", function() { + var input = "\u00a3"; // POUNDS SIGN + var output = utils.utf8.encode(input); + assert.equal(output, "\xc2\xa3"); + }); + + it("should encode a SMP character", function() { + var input = "\ud83d\udc4d"; // THUMBS UP SIGN + var output = utils.utf8.encode(input); + assert.equal(output, "\xf0\x9f\x91\x8d"); + }); + + it("should encode a complex string", function() { + var input = "For only £5.99! \ud83d\udc4d"; + var output = utils.utf8.encode(input); + assert.equal(output, "For only \xc2\xa35.99! \xf0\x9f\x91\x8d"); + }); + + it("should decode an ascii string unchanged", function() { + var input = "hello world"; + var output = utils.utf8.decode(input); + assert.equal(output, input); + }); + + it("should decode to a single UCS-2 character", function() { + var input = "\xc2\xa3"; // POUNDS SIGN + var output = utils.utf8.decode(input); + assert.equal(output, "\u00a3"); + }); + + it("should decode to a SMP character", function() { + var input = "\xf0\x9f\x91\x8d"; + var output = utils.utf8.decode(input); + assert.equal(output, "\ud83d\udc4d"); + }); + + it("should decode a complex string", function() { + var input = "For only \xc2\xa35.99! \xf0\x9f\x91\x8d"; + var output = utils.utf8.decode(input); + assert.equal(output, "For only £5.99! \ud83d\udc4d"); + }); +});