├── .editorconfig ├── .eslintrc ├── .gitignore ├── .hgtags ├── .npmignore ├── .nvmrc ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gulpfile.js ├── lib ├── context.js ├── index.js ├── keyobject.js ├── request.js └── response.js ├── package.json └── test ├── .eslintrc ├── config.js ├── context-test.js ├── index-test.js ├── keyobject-test.js ├── request-test.js ├── response-test.js └── usecase-ecdhe-test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "no-shadow": [0], 7 | "no-underscore-dangle": [0], 8 | "strict": [2, "global"], 9 | "yoda": [0] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage/ 3 | dist/ 4 | old/ 5 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 7c1097b00e768e76fd6acbc412f7d54710faa85a 0.2.0 2 | 6917989487a44c6a1cc5e6d660b3e208c7fe3187 0.2.1 3 | b1a140c309a91cc7ca044c4f70e5fce6de8638ca 0.2.2 4 | bb46476b23d28b946c1cd02bbc6408b8003889c0 0.2.3 5 | ea5f669b4b135d7f4fb7644f851f3f964dbfe2fb 0.2.5 6 | 1be96c1499bb73f7be08a0707575f4dda3d1411b 0.2.6 7 | d468771d7a41d8aaab1c2ccc5a07d197033f101a 0.2.7 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # self + revision control 2 | .npmignore 3 | .gitignore 4 | .git 5 | .hg 6 | .hgtags 7 | 8 | # build environment 9 | .editorconfig 10 | .eslintrc 11 | gulpfile.js 12 | 13 | coverage/ 14 | dist/ 15 | test/ 16 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/dubnium 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # node-kms Contributors 2 | # Listed alphabetically by surname 3 | 4 | Matthew A. Miller 5 | Ian W. Remmel 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | 4 | ## [0.3.2](https://github.com/cisco/node-kms/compare/0.3.1...0.3.2) (2016-08-23) 5 | 6 | 7 | ### Build 8 | 9 | * Fix test naming ([c14789b73576da705c19e7660dd804f556f41118](https://github.com/cisco/node-kms/commit/c14789b73576da705c19e7660dd804f556f41118)) 10 | * upgrade build environment ([e80f4bc59f29356c2eb51f733c2ccfe920e4b56a](https://github.com/cisco/node-kms/commit/e80f4bc59f29356c2eb51f733c2ccfe920e4b56a)) 11 | 12 | 13 | 14 | 15 | ## [0.3.1](https://github.com/cisco/node-kms/compare/0.3.0...0.3.1) (2015-10-13) 16 | 17 | 18 | ### Build 19 | 20 | * export correct name from browserify ([2b53168506429f223e7345c7959e5dbe45d8ae64](https://github.com/cisco/node-kms/commit/2b53168506429f223e7345c7959e5dbe45d8ae64)) 21 | * update node-jose dependency to be more permissive ([679835292ea4bda6caaaec80a86cce32d3090109](https://github.com/cisco/node-kms/commit/679835292ea4bda6caaaec80a86cce32d3090109)), closes [#2](https://github.com/cisco/node-kms/issues/2) 22 | 23 | ### Doc 24 | 25 | * update readme to reflect NPM installs ([527fe905f52e1595b4dbfca084b549420f0c58a7](https://github.com/cisco/node-kms/commit/527fe905f52e1595b4dbfca084b549420f0c58a7)) 26 | 27 | 28 | 29 | 30 | # [0.3.0] (2015-09-17) 31 | 32 | Initial public release. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-kms # 2 | 3 | A JavaScript implementation of Key Management Service (KMS) for current web browsers and node.js-based servers. The KMS API is described in [[draft-abiggs-saag-key-management-service-02](https://tools.ietf.org/html/draft-abiggs-saag-key-management-service-02)]. 4 | 5 | 6 | 7 | 8 | 9 | - [Installing](#installing) 10 | - [Basics](#basics) 11 | - [KeyObjects](#keyobjects) 12 | - [Creating](#creating) 13 | - [Importing/Exporting](#importingexporting) 14 | - [Obtaining a `node-jose` Key](#obtaining-a-node-jose-key) 15 | - [Contexts](#contexts) 16 | - [Creating and Initializing](#creating-and-initializing) 17 | - [Generating an Ephemeral EC Key](#generating-an-ephemeral-ec-key) 18 | - [Deriving an Ephemeral Shared Key](#deriving-an-ephemeral-shared-key) 19 | - [Requests](#requests) 20 | - [Creating](#creating-1) 21 | - [Wrapping](#wrapping) 22 | - [Responses](#responses) 23 | - [Creating](#creating-2) 24 | - [Unwrapping](#unwrapping) 25 | 26 | 27 | 28 | ## Installing ## 29 | 30 | To install the latest from [NPM](https://npmjs.com/): 31 | 32 | ``` 33 | npm install node-kms 34 | ``` 35 | 36 | Or to install a specific release: 37 | 38 | ``` 39 | npm install node-kms@0.3.0 40 | ``` 41 | 42 | Alternatively, the latest unpublished code can be installed directly from the repository: 43 | 44 | ``` 45 | npm install git+ssh://git@github.com:cisco/node-kms.git 46 | ``` 47 | 48 | ## Basics ## 49 | 50 | Require the library as normal: 51 | 52 | ``` 53 | var KMS = require('node-kms'); 54 | ``` 55 | 56 | This library uses [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) for many operations. 57 | 58 | This library supports [Browserify](http://browserify.org/). To use in a web browser, `require('node-kms')` and bundle with the rest of your app. 59 | 60 | ## KeyObjects ## 61 | 62 | A KMS KeyObject wraps a JSON Web Key (JWK) to provide more semantics: a URI to locate it; the creating user and client; the date/time of when a key is created, bound, and/or expires; and the owning resource (once bound). 63 | 64 | ### Creating ### 65 | 66 | To create an empty KeyObject: 67 | 68 | ``` 69 | var keyobj = new KMS.KeyObject(); 70 | ``` 71 | 72 | None of the KMS.KeyObject properties are set. 73 | 74 | Alternatively, to create a KeyObject from a JSON or POJO representation: 75 | 76 | ``` 77 | // {input} is one of: 78 | // * a JSON object (where date/times are RFC3339-encoded Strings) 79 | // * a POJO (where date/times are Date objects) 80 | var keyobj = new.KeyObject(input); 81 | ``` 82 | 83 | ### Importing/Exporting ### 84 | 85 | **NOTE**: The JSON representation includes all properties for a KeyObject, including the full JWK (if present). This can expose secret key material if not carefully handled; do not save to durable storage without protecting it (e.g., encrypting to a JWE). 86 | 87 | To import a KeyObject from a JSON object: 88 | 89 | ``` 90 | // {input} is one of: 91 | // * a JSON object (where date/times are RFC3339-encoded Strings) 92 | // * a POJO (where date/times are Date objects) 93 | // * an existing KeyObject instance 94 | keyobj = KMS.fromObject(input); 95 | ``` 96 | 97 | In the case where `input` is already a KeyObject, it is returned as-is. 98 | 99 | To export a KeyObject to a JSON object: 100 | 101 | ``` 102 | var output = keyobj.toJSON(); 103 | ``` 104 | 105 | ### Obtaining a `node-jose` Key ### 106 | 107 | To convert the `jwk` property of a KeyObject to a `node-jose` Key (to use for encryption or signatures): 108 | 109 | ``` 110 | var jwk; 111 | keyobj.asKey(). 112 | then(function(result) { 113 | // {result} is a jose.JWK.Key 114 | jwk = result; 115 | }); 116 | ``` 117 | 118 | If `jwk` is not set on the KeyObject, the returned Promise is rejected. 119 | 120 | ## Contexts ## 121 | 122 | The KMS.Context holds onto information necessary to wrap Requests and unwrap Responses. 123 | 124 | ### Creating and Initializing ### 125 | 126 | To create an empty Context: 127 | 128 | ``` 129 | var kmsCtx = new KMS.Context(); 130 | ``` 131 | 132 | None of the Context properties are set. 133 | 134 | To finish initializing the Context, set the `clientInfo` and `serverInfo` properties: 135 | 136 | ``` 137 | // {clientId} is a String containing an identifier for the client or session 138 | // {userId} is a String containing the user's identifier 139 | // {oauth2token} is a String containing an OAuth2 Bearer token 140 | kmsCtx.clientInfo = { 141 | clientId: clientId, 142 | credential: { 143 | userId: userId, 144 | bearer: oauth2token 145 | } 146 | }; 147 | // {serverPublicKey} is a JWK JSON object 148 | kmsCtx.serverInfo = { 149 | key: serverPublicKey 150 | }; 151 | ``` 152 | 153 | ### Generating an Ephemeral EC Key ### 154 | 155 | To create a KeyObject representing the local ECDH key: 156 | 157 | ``` 158 | kmsCtx.createECDHKey(). 159 | then(function(result) { 160 | // {result} is a KMS.KeyObject wrapping a "EC" JWK 161 | kmsCtx.ephemeralKey = result; 162 | }) 163 | ``` 164 | 165 | ### Deriving an Ephemeral Shared Key ### 166 | 167 | To derive an ephemeral shared key -- such as the result of the ECDHE handshake: 168 | 169 | ``` 170 | // {remoteECDH} is a KMS.KeyObject wrapping a "EC" JWK 171 | kmsCrx.deriveEphemeralKey(remoteECDH). 172 | then(function(result) { 173 | // {result} is a KMS.KeyObject wrapping a "oct" JWK 174 | kmsCtx.ephemeralKey = result; 175 | }); 176 | ``` 177 | 178 | ## Requests ## 179 | 180 | The KMS.Request embodies a single request from a client to the KMS. 181 | 182 | A Request instance has the following (read/write) properties: 183 | 184 | * `body` -- the full (plaintext) JSON to be sent to the KMS 185 | * `requestId` -- the unique id for this request 186 | * `uri` -- the URI of the request (e.g., "/ecdhe/", "/resources", etc.) 187 | * `method` -- the method (verb) for the request (e.g., "create", "retrieve", etc.) 188 | * `wrapped` -- the wrapped (encrypted) `body` 189 | 190 | When a new `body` is set, the previous `requestId`, `method`, and `uri` are remembered, overwriting any new values that might have been in the provided JSON. 191 | 192 | ### Creating ### 193 | 194 | To create an empty request: 195 | 196 | ``` 197 | var request = new KMS.Request(); 198 | ``` 199 | 200 | To create a request starting with a constructed body: 201 | 202 | ``` 203 | // {input} is a JSON object representing the request 204 | var request = new KMS.Request(input); 205 | ``` 206 | 207 | ### Wrapping ### 208 | 209 | To wrap (encrypt) the Request into a JWE for transmitting to a KMS server, using an ephemeral shared key: 210 | 211 | ``` 212 | var output; 213 | request.wrap(kmsCtx). 214 | then(function(result) { 215 | // {result} is a String of the JWE in the Compact Serialization 216 | // request.wrapped is also set to {result} 217 | output = result; 218 | }); 219 | ``` 220 | 221 | ## Responses ## 222 | 223 | The KMS.Response embodies a single response to a client from the KMS. 224 | 225 | A Response instance has the following (read/write) properties: 226 | 227 | * `body` -- the full (plaintext) JSON received from the KMS 228 | * `requestId` -- the id for the corresponding request 229 | * `status` -- the status code of the response 230 | * `reason` -- the string reason (if any) 231 | * `wrapped` -- the protected (encrypted or signed) `body` 232 | 233 | ### Creating ### 234 | 235 | To create an empty KMS.Response: 236 | 237 | ``` 238 | var response = new KMS.Response(); 239 | ``` 240 | 241 | To creat a KMS.Response with a received wrapped body: 242 | 243 | ``` 244 | // {input} is a String of the JWE (or JWS) using the Compact Serialization 245 | var response = new KMS.Response(input); 246 | ``` 247 | 248 | ### Unwrapping ### 249 | 250 | To unwrap a response into the plaintext body: 251 | 252 | ``` 253 | var input; 254 | response.unwrap(kmsCtx). 255 | then(function(result) { 256 | // {result} is the plaintext JSON object 257 | // response.body is also set to {result} 258 | input = result; 259 | }); 260 | ``` 261 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * gulpfile.js - Gulp-based build 3 | * 4 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 5 | */ 6 | "use strict"; 7 | 8 | var ARGV = require("yargs"). 9 | usage("$0 [options] task [task ...]"). 10 | option("browsers", { 11 | type: "string", 12 | describe: "browsers to run tests in", 13 | default: "" 14 | }). 15 | option("sauce", { 16 | type: "boolean", 17 | describe: "use SauceLabs for tests/reporting", 18 | default: false 19 | }). 20 | help("help"). 21 | argv; 22 | 23 | var webpack = require("webpack-stream"), 24 | clone = require("lodash.clone"), 25 | del = require("del"), 26 | doctoc = require("gulp-doctoc"), 27 | gulp = require("gulp"), 28 | istanbul = require("gulp-istanbul"), 29 | karma = require("karma"), 30 | merge = require("lodash.merge"), 31 | mocha = require("gulp-mocha"), 32 | runSequence = require("run-sequence"); 33 | 34 | // ### 'CONSTANTS' ### 35 | var SOURCES = ["./lib/**/*.js", "!(./lib/old/**/*.js)"], 36 | TESTS = "./test/**/*-test.js"; 37 | 38 | // ### HELPERS ### 39 | var MOCHA_CONFIG = { 40 | timeout: 600000 41 | }; 42 | 43 | // ### LINT TASKS ### 44 | function doEslint() { 45 | var eslint = require("gulp-eslint"); 46 | 47 | return gulp.src([ 48 | "lib/**/*.js", 49 | "test/**/*.js", 50 | "gulpfile.js" 51 | ]) 52 | .pipe(eslint()) 53 | .pipe(eslint.format()); 54 | } 55 | 56 | gulp.task("eslint", function() { 57 | return doEslint(); 58 | }); 59 | 60 | gulp.task("test:lint", function() { 61 | var eslint = require("gulp-eslint"); 62 | return doEslint() 63 | .pipe(eslint.failOnError()); 64 | }); 65 | 66 | // ### CLEAN TASKS ### 67 | gulp.task("clean:coverage:nodejs", function() { 68 | del("coverage/nodejs"); 69 | }); 70 | gulp.task("clean:coverage:browser", function() { 71 | del("coverage/browser"); 72 | }); 73 | gulp.task("clean:coverage", function() { 74 | del("coverage"); 75 | }); 76 | 77 | gulp.task("clean:dist", function() { 78 | del("dist"); 79 | }); 80 | 81 | // ### DOCUMENTATION TASKS ### 82 | gulp.task("doc:readme", function() { 83 | gulp.src("./README.md"). 84 | pipe(doctoc({ 85 | title: "" 86 | })). 87 | pipe(gulp.dest("./")); 88 | }); 89 | 90 | // ### NODEJS TASKS ### 91 | function doTestsNodejs() { 92 | return gulp.src(TESTS). 93 | pipe(mocha(MOCHA_CONFIG)); 94 | } 95 | 96 | gulp.task("test:nodejs:single", function() { 97 | return doTestsNodejs(); 98 | }); 99 | 100 | gulp.task("cover:nodejs", function() { 101 | return gulp.src(SOURCES). 102 | pipe(istanbul()). 103 | pipe(istanbul.hookRequire()). 104 | on("finish", function() { 105 | doTestsNodejs(). 106 | pipe(istanbul.writeReports({ 107 | dir: "./coverage/nodejs", 108 | reporters: ["html", "text-summary"] 109 | })); 110 | }); 111 | }); 112 | 113 | gulp.task("test:nodejs", function(cb) { 114 | runSequence("test:lint", 115 | "test:nodejs:single", 116 | cb); 117 | }); 118 | 119 | // ### BROWSER TASKS ### 120 | function doBrowserify(suffix, plugins) { 121 | var pkg = require("./package.json"); 122 | 123 | suffix = suffix || ".js"; 124 | plugins = plugins || []; 125 | 126 | return gulp.src(require("path").resolve(pkg.main)). 127 | pipe(webpack({ 128 | output: { 129 | filename: pkg.name + suffix 130 | }, 131 | plugins: plugins, 132 | devtool: "source-map" 133 | })). 134 | pipe(gulp.dest("./dist")); 135 | } 136 | 137 | gulp.task("bundle", function() { 138 | return doBrowserify(); 139 | }); 140 | 141 | gulp.task("minify", function() { 142 | return doBrowserify(".min.js", [ 143 | new webpack.webpack.optimize.UglifyJsPlugin({ 144 | minimize: true 145 | }) 146 | ]); 147 | }); 148 | 149 | var KARMA_CONFIG = { 150 | frameworks: ["mocha"], 151 | basePath: ".", 152 | browserNoActivityTimeout: 600000, 153 | client: { 154 | mocha: MOCHA_CONFIG 155 | }, 156 | preprocessors: { 157 | "test/**/*-test.js": ["webpack"] 158 | }, 159 | reporters: ["mocha"], 160 | customLaunchers: { 161 | "SL_Chrome": { 162 | base: "SauceLabs", 163 | browserName: "chrome" 164 | }, 165 | "SL_Firefox": { 166 | base: "SauceLabs", 167 | browserName: "firefox" 168 | }, 169 | "SL_Safari_8": { 170 | base: "SauceLabs", 171 | platform: "OS X 10.10", 172 | browserName: "safari", 173 | version: "8" 174 | }, 175 | "SL_Safari_9": { 176 | base: "SauceLabs", 177 | platform: "OS X 10.11", 178 | browserName: "safari", 179 | version: "9" 180 | }, 181 | "SL_IE_10": { 182 | base: "SauceLabs", 183 | browserName: "internet explorer", 184 | version: "10" 185 | }, 186 | "SL_IE_11": { 187 | base: "SauceLabs", 188 | browserName: "internet explorer", 189 | platform: "Windows 8.1", 190 | version: "11" 191 | }, 192 | "SL_EDGE": { 193 | base: "SauceLabs", 194 | browserName: "microsoftedge", 195 | platform: "Windows 10" 196 | } 197 | }, 198 | captureTimeout: 600000, 199 | sauceLabs: { 200 | testName: require("./package.json").name 201 | }, 202 | files: [TESTS] 203 | }; 204 | var KARMA_BROWSERS = { 205 | local: ["Chrome", "Firefox"], 206 | saucelabs: ["SL_Chrome", "SL_Firefox", "SL_Safari_8", "SL_Safari_9", "SL_IE_10", "SL_IE_11", "SL_EDGE"] 207 | }; 208 | // allow for IE on windows 209 | if (/^win/.test(process.platform)) { 210 | KARMA_BROWSERS.local.push("IE"); 211 | } 212 | // allow for Safari on Mac OS X 213 | if (/^darwin/.test(process.platform)) { 214 | KARMA_BROWSERS.local.push("Safari"); 215 | } 216 | 217 | gulp.task("test:browser:single", function(done) { 218 | var browsers = ARGV.browsers.split(/\s*,\s*/g). 219 | filter(function (v) { return v; }); 220 | 221 | var config = merge({}, KARMA_CONFIG, { 222 | singleRun: true 223 | }); 224 | if (ARGV.sauce) { 225 | config = merge(config, { 226 | reporters: ["mocha", "saucelabs"], 227 | browsers: KARMA_BROWSERS.saucelabs 228 | }); 229 | } else { 230 | config.browsers = KARMA_BROWSERS.local; 231 | } 232 | if (browsers.length) { 233 | config.browsers = config.browsers.filter(function(b) { 234 | b = b.replace("SL_", ""); 235 | return -1 !== browsers.indexOf(b); 236 | }); 237 | } 238 | 239 | karma.server.start(config, done); 240 | }); 241 | 242 | gulp.task("test:browser:watch", function(done) { 243 | var config = clone(KARMA_CONFIG); 244 | 245 | karma.server.start(config, done); 246 | }); 247 | 248 | gulp.task("test:browser", function(cb) { 249 | runSequence("test:lint", 250 | "test:browser:single", 251 | cb); 252 | }); 253 | 254 | // ### MAIN TASKS ### 255 | gulp.task("test", function(cb) { 256 | runSequence("test:lint", 257 | "test:browser:single", 258 | "test:nodejs:single", 259 | cb); 260 | }); 261 | gulp.task("coverage", function(cb) { 262 | runSequence("test:lint", 263 | "cover:nodejs", 264 | cb); 265 | }); 266 | gulp.task("clean", ["clean:coverage", "clean:dist"]); 267 | gulp.task("dist", function(cb) { 268 | runSequence("clean:dist", 269 | "test:lint", 270 | "test:browser", 271 | ["bundle", "minify"], 272 | cb); 273 | }); 274 | 275 | // ### MAIN WATCHERS ### 276 | gulp.task("watch:test", ["test"], function() { 277 | return gulp.watch([SOURCES, TESTS], ["test:nodejs", "test:browser"]); 278 | }); 279 | 280 | // ### DEFAULT ### 281 | gulp.task("default", ["test"]); 282 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * lib/keyobject.js -- KMS Key Representation 3 | * 4 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 5 | */ 6 | "use strict"; 7 | 8 | var jose = require("node-jose"), 9 | uuid = require("uuid"); 10 | 11 | var KMS = { 12 | KeyObject: require("./keyobject") 13 | }; 14 | 15 | function KMSContext() { 16 | var sharedKey = null, 17 | clientInfo = {}, 18 | serverInfo = {}; 19 | 20 | Object.defineProperty(this, "ephemeralKey", { 21 | get: function() { 22 | // TODO: honor expiration 23 | return sharedKey; 24 | }, 25 | set: function(key) { 26 | sharedKey = key ? 27 | KMS.KeyObject.fromObject(key) : 28 | null; 29 | }, 30 | enumerable: true 31 | }); 32 | Object.defineProperty(this, "clientInfo", { 33 | get: function() { return clientInfo; }, 34 | set: function(info) { 35 | // TODO: validate client info 36 | clientInfo = info || {}; 37 | }, 38 | enumerable: true 39 | }); 40 | Object.defineProperty(this, "serverInfo", { 41 | get: function() { return serverInfo; }, 42 | set: function(info) { 43 | // TODO: validate server info 44 | serverInfo = info || {}; 45 | }, 46 | enumerable: true 47 | }); 48 | 49 | Object.defineProperty(this, "requestId", { 50 | value: function() { 51 | return uuid(); 52 | } 53 | }); 54 | } 55 | 56 | KMSContext.prototype.createECDHKey = function() { 57 | var clientInfo = this.clientInfo; 58 | var ks = jose.JWK.createKeyStore(); 59 | 60 | // TODO: make this more configurable 61 | var keyType = "EC", 62 | keyOrder = "P-256", 63 | expiresIn = 3600000; 64 | var promise = ks.generate(keyType, keyOrder); 65 | promise = promise.then(function(k) { 66 | var ts = new Date(); 67 | var rep = { 68 | uri: "-internal/" + k.kid, 69 | jwk: k.toJSON(true), 70 | userId: (clientInfo && clientInfo.credential && clientInfo.credential.userId) || 71 | "", 72 | clientId: (clientInfo && clientInfo.clientId) || 73 | "", 74 | createDate: ts, 75 | expirationDate: new Date(ts.getTime() + expiresIn) 76 | }; 77 | 78 | return new KMS.KeyObject(rep); 79 | }); 80 | return promise; 81 | }; 82 | 83 | KMSContext.prototype.deriveEphemeralKey = function(remote) { 84 | var local = this.ephemeralKey; 85 | if (!local || !local.jwk || "EC" !== local.jwk.kty) { 86 | return Promise.reject(new Error("invalid local ECDH key")); 87 | } 88 | remote = KMS.KeyObject.fromObject(remote); 89 | 90 | var promise; 91 | promise = Promise.all([local.asKey(), remote.asKey()]); 92 | promise = promise.then(function(keys) { 93 | var lkey = keys[0], 94 | rkey = keys[1]; 95 | var props = { 96 | public: rkey.toObject() 97 | }; 98 | var k = lkey.toObject(true); 99 | return jose.JWA.derive("ECDH-HKDF", k, props); 100 | }); 101 | promise = promise.then(function(result) { 102 | var uri = remote.uri, 103 | created = remote.createDate, 104 | expires = remote.expirationDate; 105 | var shared = { 106 | uri: uri, 107 | createDate: created, 108 | expirationDate: expires, 109 | jwk: { 110 | kty: "oct", 111 | kid: uri, 112 | alg: "A256GCM", 113 | k: jose.util.base64url.encode(result) 114 | } 115 | }; 116 | return new KMS.KeyObject(shared); 117 | }); 118 | return promise; 119 | }; 120 | 121 | module.exports = KMSContext; 122 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * lib/index.js -- KMS Entry Point 3 | * 4 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 5 | */ 6 | "use strict"; 7 | 8 | module.exports = { 9 | KeyObject: require("./keyobject"), 10 | Context: require("./context"), 11 | Request: require("./request"), 12 | Response: require("./response") 13 | }; 14 | -------------------------------------------------------------------------------- /lib/keyobject.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * lib/keyobject.js -- KMS Key Representation 3 | * 4 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 5 | */ 6 | "use strict"; 7 | 8 | var cloneDeep = require("lodash.clonedeep"), 9 | jose = require("node-jose"); 10 | 11 | function KMSKeyObject(rep) { 12 | rep = cloneDeep(rep || {}); 13 | // coerce date fields 14 | ["createDate", "expirationDate", "bindDate"].forEach(function(f) { 15 | if (f in rep && rep[f]) { 16 | rep[f] = "string" === typeof rep[f] ? 17 | new Date(Date.parse(rep[f])) : 18 | rep[f]; 19 | } 20 | }); 21 | 22 | Object.defineProperty(this, "uri", { 23 | get: function() { return rep.uri || ""; }, 24 | enumerable: true 25 | }); 26 | Object.defineProperty(this, "jwk", { 27 | get: function() { return rep.jwk || undefined; }, 28 | enumerable: true 29 | }); 30 | Object.defineProperty(this, "userId", { 31 | get: function() { return rep.userId || ""; }, 32 | enumerable: true 33 | }); 34 | Object.defineProperty(this, "clientId", { 35 | get: function() { return rep.clientId || ""; }, 36 | enumerable: true 37 | }); 38 | Object.defineProperty(this, "createDate", { 39 | get: function() { return rep.createDate || undefined; }, 40 | enumerable: true 41 | }); 42 | Object.defineProperty(this, "expirationDate", { 43 | get: function() { return rep.expirationDate || undefined; }, 44 | enumerable: true 45 | }); 46 | Object.defineProperty(this, "bindDate", { 47 | get: function() { return rep.bindDate || undefined; }, 48 | enumerable: true 49 | }); 50 | Object.defineProperty(this, "resourceUri", { 51 | get: function() { return rep.resourceUri || ""; }, 52 | enumerable: true 53 | }); 54 | } 55 | 56 | // ### Instance Methods ### 57 | KMSKeyObject.prototype.asKey = function() { 58 | if (!this.jwk) { 59 | return Promise.reject(new Error("'jwk' not set")); 60 | } 61 | return jose.JWK.asKey(this.jwk); 62 | }; 63 | 64 | KMSKeyObject.prototype.toJSON = function() { 65 | var self = this, 66 | json = {}; 67 | Object.keys(this).forEach(function(f) { 68 | var v = self[f]; 69 | if ("function" === typeof v || "undefined" === typeof v) { 70 | return; 71 | } 72 | if (v instanceof Date) { 73 | v = v.toISOString(); 74 | } 75 | json[f] = cloneDeep(v); 76 | }); 77 | 78 | return json; 79 | }; 80 | 81 | // ### Class Functions ### 82 | KMSKeyObject.fromObject = function(rep) { 83 | if (!rep) { 84 | throw new TypeError("representation required"); 85 | } 86 | if (rep instanceof KMSKeyObject) { 87 | return rep; 88 | } 89 | return new KMSKeyObject(rep); 90 | }; 91 | 92 | module.exports = KMSKeyObject; 93 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * lib/request.js -- KMS (Generic) Request 3 | * 4 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 5 | */ 6 | "use strict"; 7 | 8 | var clone = require("lodash.clone"), 9 | jose = require("node-jose"); 10 | 11 | function KMSRequest(body) { 12 | var wrapped = ""; 13 | body = (body && clone(body)) || {}; 14 | 15 | Object.defineProperty(this, "wrapped", { 16 | get: function() { return wrapped; }, 17 | set: function(w) { wrapped = String(w || ""); }, 18 | enumerable: true 19 | }); 20 | Object.defineProperty(this, "body", { 21 | get: function() { return body; }, 22 | set: function(b) { 23 | b = (b && clone(b)) || {}; 24 | 25 | // carry forward requestId 26 | if ("requestId" in body) { 27 | b.requestId = body.requestId; 28 | } 29 | // carry forward uri 30 | if ("uri" in body) { 31 | b.uri = body.uri; 32 | } 33 | // carry forward method 34 | if ("method" in body) { 35 | b.method = body.method; 36 | } 37 | // clear any wrapped, then save 38 | wrapped = ""; 39 | body = b; 40 | }, 41 | enumerable: true 42 | }); 43 | 44 | Object.defineProperty(this, "requestId", { 45 | get: function() { return body.requestId || ""; }, 46 | set: function(id) { 47 | if (!id) { 48 | delete body.requestId; 49 | } else { 50 | body.requestId = id; 51 | } 52 | }, 53 | enumerable: true 54 | }); 55 | Object.defineProperty(this, "uri", { 56 | get: function() { return body.uri || ""; }, 57 | set: function(uri) { 58 | if (!uri) { 59 | delete body.uri; 60 | } else { 61 | body.uri = uri; 62 | } 63 | }, 64 | enumerable: true 65 | }); 66 | Object.defineProperty(this, "method", { 67 | get: function() { return body.method || ""; }, 68 | set: function(method) { 69 | if (!method) { 70 | delete body.method; 71 | } else { 72 | body.method = method; 73 | } 74 | }, 75 | enumerable: true 76 | }); 77 | } 78 | 79 | KMSRequest.prototype.wrap = function(ctx, opts) { 80 | opts = opts || {}; 81 | 82 | // TODO: make this more configurable 83 | var self = this, 84 | promise; 85 | 86 | // set the requestId if not already set 87 | if (!this.requestId || opts.requestId) { 88 | this.requestId = opts.requestId || ctx.requestId(); 89 | } 90 | 91 | var body = this.body; 92 | body.client = ctx.clientInfo; 93 | 94 | // prepare the key 95 | if (opts.serverKey) { 96 | promise = jose.JWK.asKey(ctx.serverInfo.key); 97 | } else { 98 | promise = ctx.ephemeralKey.asKey(); 99 | } 100 | promise = promise.then(function(jwk) { 101 | var key = jwk; 102 | var cfg = { 103 | compact: true, 104 | contentAlg: opts.contentAlg || "A256GCM" 105 | }; 106 | var jwe = jose.JWE.createEncrypt(cfg, key); 107 | return jwe.final(JSON.stringify(self.body), "utf8"); 108 | }); 109 | promise = promise.then(function(result) { 110 | // save wrapped 111 | self.wrapped = result; 112 | return result; 113 | }); 114 | return promise; 115 | }; 116 | 117 | module.exports = KMSRequest; 118 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * lib/response.js -- KMS (Generic) Response 3 | * 4 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 5 | */ 6 | "use strict"; 7 | 8 | var clone = require("lodash.clone"), 9 | jose = require("node-jose"); 10 | 11 | function KMSResponse(wrapped) { 12 | wrapped = wrapped || ""; 13 | var body = {}; 14 | 15 | Object.defineProperty(this, "wrapped", { 16 | get: function() { return wrapped; }, 17 | set: function(w) { 18 | // clear existing body before saving wrapped 19 | body = {}; 20 | wrapped = String(w || ""); 21 | }, 22 | enumerable: true 23 | }); 24 | Object.defineProperty(this, "body", { 25 | get: function() { return body; }, 26 | set: function(b) { body = (b && clone(b)) || {}; }, 27 | enumerable: true 28 | }); 29 | 30 | Object.defineProperty(this, "status", { 31 | get: function() { return body.status || 0; }, 32 | set: function(s) { 33 | s = parseInt(s); 34 | if (!isNaN(s)) { 35 | body.status = s; 36 | } 37 | }, 38 | enumerable: true 39 | }); 40 | Object.defineProperty(this, "reason", { 41 | get: function() { return body.reason || ""; }, 42 | set: function(r) { body.reason = String(r || ""); }, 43 | enumerable: true 44 | }); 45 | Object.defineProperty(this, "requestId", { 46 | get: function() { return body.requestId || ""; }, 47 | set: function(id) { body.requestId = String(id || ""); }, 48 | enumerable: true 49 | }); 50 | } 51 | 52 | KMSResponse.prototype.unwrap = function(ctx, opts) { 53 | opts = opts || {}; 54 | 55 | var keystore = jose.JWK.createKeyStore(), 56 | waiting = [], 57 | key; 58 | 59 | // add ephemeral key (if any) 60 | key = ctx.ephemeralKey && ctx.ephemeralKey.jwk; 61 | if (key) { 62 | waiting.push(keystore.add(key)); 63 | } 64 | // add server key (if any) 65 | key = ctx.serverInfo && ctx.serverInfo.key; 66 | if (key) { 67 | waiting.push(keystore.add(key)); 68 | } 69 | 70 | var self = this; 71 | var promise = Promise.all(waiting); 72 | promise = promise.then(function() { 73 | var wrapped = self.wrapped; 74 | // count the dots 75 | switch ((wrapped.match(/\./g) || []).length) { 76 | case 2: // signed 77 | return jose.JWS.createVerify(keystore). 78 | verify(wrapped); 79 | case 4: // encrypted 80 | return jose.JWE.createDecrypt(keystore). 81 | decrypt(wrapped); 82 | default: // bogus 83 | return Promise.reject(new Error("invalid wrapped")); 84 | } 85 | }); 86 | promise = promise.then(function(result) { 87 | // parse result to JSON 88 | result = (result.plaintext || result.payload).toString("utf8"); 89 | result = JSON.parse(result); 90 | // save it before returning it 91 | self.body = result; 92 | return result; 93 | }); 94 | return promise; 95 | }; 96 | 97 | module.exports = KMSResponse; 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-kms", 3 | "version": "0.4.1", 4 | "description": "A client implementation of the Key Management Service (KMS) for JavaScript -- for both node.js and browser.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "gulp test:nodejs" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:cisco/node-kms.git" 12 | }, 13 | "keywords": [ 14 | "kms", 15 | "crypto", 16 | "jose" 17 | ], 18 | "author": "Cisco Systems, Inc. ", 19 | "contributors": [ 20 | "Matthew A. Miller ", 21 | "Ian W. Remmel " 22 | ], 23 | "license": "Apache-2.0", 24 | "dependencies": { 25 | "es6-promise": "^2.0.1", 26 | "lodash.clone": "^3.0.2", 27 | "lodash.clonedeep": "^3.0.1", 28 | "node-jose": "^2.2.0", 29 | "uuid": "^2.0.1" 30 | }, 31 | "devDependencies": { 32 | "chai": "^1.10.0", 33 | "del": "^1.1.1", 34 | "gulp": "^3.8.10", 35 | "gulp-doctoc": "^0.1.2", 36 | "gulp-eslint": "^0.5.0", 37 | "gulp-istanbul": "^0.6.0", 38 | "gulp-mocha": "^2.0.0", 39 | "gulp-rename": "^1.2.0", 40 | "gulp-uglify": "^1.1.0", 41 | "istanbul": "^0.3.5", 42 | "karma": "^1.2.0", 43 | "karma-chrome-launcher": "^0.1.7", 44 | "karma-firefox-launcher": "^0.1.4", 45 | "karma-mocha": "^0.1.10", 46 | "karma-mocha-reporter": "^0.3.1", 47 | "karma-safari-launcher": "^0.1.1", 48 | "karma-sauce-launcher": "^0.2.14", 49 | "karma-webpack": "^1.8.0", 50 | "lodash.merge": "^3.3.1", 51 | "lodash.omit": "^3.1.0", 52 | "mocha": "^2.1.0", 53 | "run-sequence": "^1.0.2", 54 | "webpack": "^1.13.2", 55 | "webpack-stream": "^3.2.0", 56 | "yargs": "^3.18.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 4 | */ 5 | "use strict"; 6 | 7 | module.exports = { 8 | clientInfo: { 9 | key: { 10 | uri: "-internal/c3370144-5b1e-40c3-a4ef-21ff63f85dd5", 11 | createDate: "2014-11-06T16:35:05Z", 12 | expirationDat: "2014-11-06T1735:05Z", 13 | jwk: { 14 | kid: "c3370144-5b1e-40c3-a4ef-21ff63f85dd5", 15 | kty: "EC", 16 | crv: "P-256", 17 | x: "jzs-vcNXzSPAtCmgchsQ2uqYcky43XokijoyTC6-FX0", 18 | y: "QM4cPEYv0xS1oWtg19i0xjvn3nhY3rU9cklDfc1GR6M", 19 | d: "WhCpx09Cb8Ge5j07D4rVx_kPl7TyApHnnZuRX7eGsEc" 20 | } 21 | }, 22 | clientId: "tester-web_e1fa9c13-108e-4fd0-a793-00f2644e6ad0", 23 | credential: { 24 | userId: "414baf5d-649b-4b4d-9a5a-0eec960c3db2", 25 | bearer: "IOAO1WboYGJLDb6W-sU7YQ.54k0eIaQDLYOr8uD9ZuEDeKzLRQnkAopV7vsQ0dOrv0" 26 | } 27 | }, 28 | serverInfo: { 29 | key: { 30 | "kty": "RSA", 31 | "kid": "kms.example", 32 | "e": "AQAB", 33 | "n": "zE50BVMiOHcitZ_BKzLY4UJOnBJGejAYUr-kTuYeqJ01FQwVxCYGV6pJUokam44QVoN3EXX7BTDapPUz1VX7UVcmT5v1HZMn0wZOTRT01i3u8BnFEhqL5jn0IWZ1HYYNmwHKVgNdxShfY_64ohPS4mpjCEihhf-KROFKBK_KvL03QF4LSOCYqqHCVTtPB4EGbhtr57tzRqA0-6wn9B-Y796Fxgu4PBdSvojSsubfDFQqqqCHQE53BqgYbpADlgAd-190J1utViA3ywbE29Ga7PX343qLgNKD9nKIpHBA2nf11dXbGf-mqdgcmpB3MEtVIY5WfiJo_MPHF_ON_i4paQ", 34 | "d": "wimQ83qFXBpEv00a3H4PRUypvoNTvU213_ZHJcBmxKHaz6zThr0IbAnmcuzff5PsjPuVjd-M9urt77UkVIvJMw1a2G-D7F5si41Dntfasy2mFUK4WjMmX18WVyOzC6LZjUWPUF0UoYlz1mk-eLHUSd8HTNkRwHIRqtJccMXMK78IbbJwQlzhHGNqwmiVH9cLZy1mN9fgHpD3lGBUwxnppWl5XxsyBpmzBqnRH_1pT2mwZP2KjZbQnRXh02BBOCNuDT32oV004T7FJ9i3FjhLnX3QtDe62n5Jk26AZZLTywg7ZUSfaF-o-Pax6PH4dprwqHleMBq6KJGkoCb5cx7ZAQ", 35 | "p": "5o_EY3Z-VjIA6X1_dJHU_QSLsvp2igT-_LPUvje9Ugdbt9YIoabx50Vfw3n7S5gAwoWzFqtczGob1R3xLDAOMA5avdJIfNV5XlYdL3q0sGA4YfxppUYVV9xQbuqtb1t-7_I9D3B7BSFfCJYVo3kYnxqN5ATXnxEEBlaxrAHD4WE", 36 | "q": "4tkbhABqB70Kph9oRNC2vSlr5DpABBcrmYhHTm34MBUjyLo6Eh2WdkcW94gS2CNDVy623Y-dlwwKg69QBCt3LfLjR6IW-3QawvRcMc_6S8uoqMwhLxZSRaYIv8FvGEAyTEE8qkheESc1eRUSzXc0IMT2QD-a1tzoXJN8rX5LXQk", 37 | "dp": "Lw3CN8ZUDEVSOFS7cU_d1vFZjWJeVNamL26ICXYYtif4SPFue4c1sUxQOfWoGopgBVMB9KtuWe2B1qGwuaLpElernzfOQWPTzDPo6uqw-5n3h8_aw7ck3fSBmdFdKe_TXSbj3UXIZYik-3yd1-n742So9pMiaj0vxBdnVUJa9qE", 38 | "dq": "yR6jfoy_dMztvArsbtZ8rYkj1tyuFPJV_XENh0OIX1jeuQAirBz_YS319s_1pRregVUxW44QsfJDAC1WS9wdded6CfBzX8y4TVBIzQEPcFPH3J_ea9jmaEmkSB4_WMjRzzmnh3FRtPOGPSup1_Bvjv4RNTUvGoAQosNALrFiL8E", 39 | "qi": "ezE-gkT8-VrLFr6awztxQQJbDH9uB-vIwlXhOUyIB_mHVXLZLN-og06jL0QIru18WSChA49IX9WAKLa5tbP7iJK5VZi-7vh3hH0lvdfqgr3CR1LSeOgn096ecHHxZvW2mXxVWEXCZEhbeQ549Zd56Ky8Is8kWGlPKQ-eiqepKbg" 40 | }, 41 | cert: { 42 | "kty": "RSA", 43 | "kid": "kms.example", 44 | "e": "AQAB", 45 | "n": "zE50BVMiOHcitZ_BKzLY4UJOnBJGejAYUr-kTuYeqJ01FQwVxCYGV6pJUokam44QVoN3EXX7BTDapPUz1VX7UVcmT5v1HZMn0wZOTRT01i3u8BnFEhqL5jn0IWZ1HYYNmwHKVgNdxShfY_64ohPS4mpjCEihhf-KROFKBK_KvL03QF4LSOCYqqHCVTtPB4EGbhtr57tzRqA0-6wn9B-Y796Fxgu4PBdSvojSsubfDFQqqqCHQE53BqgYbpADlgAd-190J1utViA3ywbE29Ga7PX343qLgNKD9nKIpHBA2nf11dXbGf-mqdgcmpB3MEtVIY5WfiJo_MPHF_ON_i4paQ" 46 | } 47 | }, 48 | sharedKey: { 49 | "uri": "/ecdhe/a4752411-dd5f-483c-8d93-e8221f6ab4a6", 50 | "jwk": { 51 | "kty": "oct", 52 | "kid": "/ecdhe/a4752411-dd5f-483c-8d93-e8221f6ab4a6", 53 | "alg": "A256GCM", 54 | "k": "B8L7seJG2lX1-dx377Ys6ZBXulEntd3amXjWYy1e00M" 55 | }, 56 | "createDate": "2014-10-09T09:34:12Z", 57 | "expirationDate": "2014-10-09T10:34:12Z" 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /test/context-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 4 | */ 5 | "use strict"; 6 | 7 | var chai = require("chai"), 8 | cloneDeep = require("lodash.clonedeep"); 9 | 10 | var KMS = { 11 | KeyObject: require("../lib/keyobject"), 12 | Context: require("../lib/context") 13 | }; 14 | 15 | var assert = chai.assert; 16 | 17 | describe("KMS/Context", function() { 18 | var clientInfo = { 19 | key: { 20 | uri: "-internal/c3370144-5b1e-40c3-a4ef-21ff63f85dd5", 21 | createDate: "2014-11-06T16:35:05Z", 22 | expirationDat: "2014-11-06T1735:05Z", 23 | jwk: { 24 | kid: "c3370144-5b1e-40c3-a4ef-21ff63f85dd5", 25 | kty: "EC", 26 | crv: "P-256", 27 | x: "jzs-vcNXzSPAtCmgchsQ2uqYcky43XokijoyTC6-FX0", 28 | y: "QM4cPEYv0xS1oWtg19i0xjvn3nhY3rU9cklDfc1GR6M", 29 | d: "WhCpx09Cb8Ge5j07D4rVx_kPl7TyApHnnZuRX7eGsEc" 30 | } 31 | }, 32 | clientId: "tester-web_e1fa9c13-108e-4fd0-a793-00f2644e6ad0", 33 | credential: { 34 | userId: "414baf5d-649b-4b4d-9a5a-0eec960c3db2", 35 | bearer: "IOAO1WboYGJLDb6W-sU7YQ.54k0eIaQDLYOr8uD9ZuEDeKzLRQnkAopV7vsQ0dOrv0" 36 | } 37 | }; 38 | 39 | describe("ctor", function() { 40 | it("creates an empty Context", function() { 41 | var kmsCtx = new KMS.Context(); 42 | assert.isNull(kmsCtx.ephemeralKey); 43 | assert.deepEqual(kmsCtx.clientInfo, {}); 44 | assert.deepEqual(kmsCtx.serverInfo, {}); 45 | }); 46 | }); 47 | describe("#ephemeralKey", function() { 48 | var ephemeral = { 49 | "uri": "/ecdhe/ea9f3858-1240-4328-ae22-a15f6072306f", 50 | "jwk": { 51 | "kty": "EC", 52 | "crv": "P-256", 53 | "x": "8mdasnEZac2LWxMwKExikKU5LLacLQlcOt7A6n1ZGUC", 54 | "y": "lxs7ln5LtZUE_GE7yzc6BZOwBxtOftdsr8HVh-14ksS" 55 | }, 56 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 57 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 58 | "createDate": new Date("2014-10-09T15:54:48Z"), 59 | "expirationDate": new Date("2014-10-09T16:54:48Z") 60 | }; 61 | 62 | it("gets/sets ephemeralKey", function() { 63 | var kmsCtx = new KMS.Context(); 64 | assert.isNull(kmsCtx.ephemeralKey); 65 | 66 | var keyobj = new KMS.KeyObject(ephemeral); 67 | kmsCtx.ephemeralKey = keyobj; 68 | assert.deepEqual(kmsCtx.ephemeralKey, keyobj); 69 | 70 | kmsCtx.ephemeralKey = null; 71 | assert.isNull(kmsCtx.ephemeralKey); 72 | }); 73 | it("gets/sets as JSON", function() { 74 | var kmsCtx = new KMS.Context(); 75 | assert.isNull(kmsCtx.ephemeralKey); 76 | 77 | var json = cloneDeep(ephemeral, function(value) { 78 | return (value instanceof Date) ? 79 | value.toISOString() : 80 | undefined; 81 | }); 82 | var keyobj = new KMS.KeyObject(json); 83 | kmsCtx.ephemeralKey = json; 84 | assert.deepEqual(kmsCtx.ephemeralKey, keyobj); 85 | 86 | kmsCtx.ephemeralKey = null; 87 | assert.isNull(kmsCtx.ephemeralKey); 88 | }); 89 | it("gets/sets as POJO", function() { 90 | var kmsCtx = new KMS.Context(); 91 | assert.isNull(kmsCtx.ephemeralKey); 92 | 93 | var keyobj = new KMS.KeyObject(ephemeral); 94 | kmsCtx.ephemeralKey = ephemeral; 95 | assert.deepEqual(kmsCtx.ephemeralKey, keyobj); 96 | 97 | kmsCtx.ephemeralKey = null; 98 | assert.isNull(kmsCtx.ephemeralKey); 99 | }); 100 | }); 101 | describe("#requestId", function() { 102 | it("gets a new id each time", function() { 103 | var kmsCtx = new KMS.Context(); 104 | 105 | var seq1 = kmsCtx.requestId(), 106 | seq2 = kmsCtx.requestId(); 107 | assert.ok(seq1); 108 | assert.ok(seq2); 109 | assert.notEqual(seq1, seq2); 110 | }); 111 | }); 112 | 113 | describe("#createECDHKey", function() { 114 | it("does it", function() { 115 | var kmsCtx = new KMS.Context(); 116 | kmsCtx.clientInfo = clientInfo; 117 | 118 | var promise = kmsCtx.createECDHKey(); 119 | promise = promise.then(function(result) { 120 | assert.ok(result instanceof KMS.KeyObject); 121 | assert.equal(result.clientId, clientInfo.clientId); 122 | assert.equal(result.userId, clientInfo.credential.userId); 123 | 124 | var jwk = result.jwk; 125 | assert.ok(jwk); 126 | assert.equal(jwk.kty, "EC"); 127 | assert.equal(jwk.crv, "P-256"); 128 | assert.ok(jwk.x); 129 | assert.ok(jwk.y); 130 | assert.ok(jwk.d); 131 | }); 132 | return promise; 133 | }); 134 | }); 135 | 136 | describe("#deriveEphemeralKey", function() { 137 | it("does it", function() { 138 | var kmsCtx = new KMS.Context(); 139 | kmsCtx.ephemeralKey = clientInfo.key; 140 | 141 | var promise; 142 | var remote = { 143 | "uri": "/ecdhe/5d9a92dd-b22d-4bcf-bacb-72775cea11c7", 144 | "jwk": { 145 | "kid": "5d9a92dd-b22d-4bcf-bacb-72775cea11c7", 146 | "kty": "EC", 147 | "crv": "P-256", 148 | "x": "eyDfB_DWmIcKjkLvBZUx-Z7W436DaikKQDpOCt7rFYQ", 149 | "y": "icbVIugK8rnSXRVERLRvoWZu1s4dzivMJa70pEYG6Fs" 150 | }, 151 | "createDate": "2014-10-09T15:54:48Z", 152 | "expirationDate": "2014-10-09T16:54:48Z" 153 | }; 154 | promise = kmsCtx.deriveEphemeralKey(remote); 155 | promise = promise.then(function(ephemeral) { 156 | assert.ok(ephemeral instanceof KMS.KeyObject); 157 | assert.equal(ephemeral.uri, remote.uri); 158 | assert.deepEqual(ephemeral.createDate, 159 | new Date(remote.createDate)); 160 | assert.deepEqual(ephemeral.expirationDate, 161 | new Date(remote.expirationDate)); 162 | assert.equal(ephemeral.jwk.k, 163 | "II4BqHTDSRBKp0QoMYXORF75-1xud9i5_BkZeXaAS3s"); 164 | }); 165 | return promise; 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /test/index-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 4 | */ 5 | "use strict"; 6 | 7 | var chai = require("chai"); 8 | var assert = chai.assert; 9 | 10 | var KMS = require("../"); 11 | 12 | describe("Public API", function() { 13 | it("exports KeyObject", function() { 14 | assert.ok(KMS.KeyObject); 15 | assert.equal(typeof KMS.KeyObject, "function"); 16 | assert.equal(typeof KMS.KeyObject.fromObject, "function"); 17 | }); 18 | it("exports Context", function() { 19 | assert.ok(KMS.Context); 20 | assert.equal(typeof KMS.Context, "function"); 21 | }); 22 | it("exports Request", function() { 23 | assert.ok(KMS.Request); 24 | assert.equal(typeof KMS.Request, "function"); 25 | }); 26 | it("exports Response", function() { 27 | assert.ok(KMS.Response); 28 | assert.equal(typeof KMS.Response, "function"); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/keyobject-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 4 | */ 5 | "use strict"; 6 | 7 | var chai = require("chai"); 8 | 9 | var KMS = { 10 | KeyObject: require("../lib/keyobject") 11 | }; 12 | 13 | var assert = chai.assert; 14 | 15 | describe("KMS/KeyObject", function() { 16 | describe("ctor", function() { 17 | it("creates an empty KeyObject", function() { 18 | var keyobj = new KMS.KeyObject(); 19 | assert.equal(keyobj.uri, ""); 20 | assert.equal(keyobj.userId, ""); 21 | assert.equal(keyobj.clientId, ""); 22 | assert.equal(keyobj.resourceUri, ""); 23 | assert.ok(!keyobj.createDate); 24 | assert.ok(!keyobj.expirationDate); 25 | assert.ok(!keyobj.bindDate); 26 | assert.ok(!keyobj.jwk); 27 | }); 28 | it("creates a KeyObject from a POJO", function() { 29 | var rep = { 30 | "uri": "/keys/52100fa4-c222-46d0-994d-1ca885e4a3a2", 31 | "jwk": { 32 | "kid": "52100fa4-c222-46d0-994d-1ca885e4a3a2", 33 | "kty": "oct", 34 | "k": "ZMpktzGq1g6_r4fKVdnx9OaYr4HjxPjIs7l7SwAsgsg" 35 | }, 36 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 37 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 38 | "createDate": new Date("2014-10-09T15:54:48Z"), 39 | "bindDate": new Date("2014-10-09T15:55:34Z"), 40 | "expirationDate": new Date("2014-10-10T15:55:34Z"), 41 | "resourceUri": "/resources/7f35c3eb-95d6-4558-a7fc-1942e5f03094" 42 | }; 43 | var keyobj = new KMS.KeyObject(rep); 44 | assert.equal(keyobj.uri, rep.uri); 45 | assert.equal(keyobj.userId, rep.userId); 46 | assert.equal(keyobj.clientId, rep.clientId); 47 | assert.equal(keyobj.resourceUri, rep.resourceUri); 48 | assert.deepEqual(keyobj.createDate, rep.createDate); 49 | assert.deepEqual(keyobj.expirationDate, rep.expirationDate); 50 | assert.deepEqual(keyobj.bindDate, rep.bindDate); 51 | assert.deepEqual(keyobj.jwk, rep.jwk); 52 | }); 53 | it("creates a KeyObject from a JSON-Object", function() { 54 | var rep = { 55 | "uri": "/keys/52100fa4-c222-46d0-994d-1ca885e4a3a2", 56 | "jwk": { 57 | "kid": "52100fa4-c222-46d0-994d-1ca885e4a3a2", 58 | "kty": "oct", 59 | "k": "ZMpktzGq1g6_r4fKVdnx9OaYr4HjxPjIs7l7SwAsgsg" 60 | }, 61 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 62 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 63 | "createDate": "2014-10-09T15:54:48Z", 64 | "bindDate": "2014-10-09T15:55:34Z", 65 | "expirationDate": "2014-10-10T15:55:34Z", 66 | "resourceUri": "/resources/7f35c3eb-95d6-4558-a7fc-1942e5f03094" 67 | }; 68 | var keyobj = new KMS.KeyObject(rep); 69 | assert.equal(keyobj.uri, rep.uri); 70 | assert.equal(keyobj.userId, rep.userId); 71 | assert.equal(keyobj.clientId, rep.clientId); 72 | assert.equal(keyobj.resourceUri, rep.resourceUri); 73 | assert.deepEqual(keyobj.createDate, new Date(rep.createDate)); 74 | assert.deepEqual(keyobj.expirationDate, new Date(rep.expirationDate)); 75 | assert.deepEqual(keyobj.bindDate, new Date(rep.bindDate)); 76 | assert.deepEqual(keyobj.jwk, rep.jwk); 77 | }); 78 | }); 79 | 80 | describe("#fromObject", function() { 81 | it("coerces a POJO to a KeyObject", function() { 82 | var rep = { 83 | "uri": "/keys/52100fa4-c222-46d0-994d-1ca885e4a3a2", 84 | "jwk": { 85 | "kid": "52100fa4-c222-46d0-994d-1ca885e4a3a2", 86 | "kty": "oct", 87 | "k": "ZMpktzGq1g6_r4fKVdnx9OaYr4HjxPjIs7l7SwAsgsg" 88 | }, 89 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 90 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 91 | "createDate": new Date("2014-10-09T15:54:48Z"), 92 | "bindDate": new Date("2014-10-09T15:55:34Z"), 93 | "expirationDate": new Date("2014-10-10T15:55:34Z"), 94 | "resourceUri": "/resources/7f35c3eb-95d6-4558-a7fc-1942e5f03094" 95 | }; 96 | var keyobj = KMS.KeyObject.fromObject(rep); 97 | assert.equal(keyobj.uri, rep.uri); 98 | assert.equal(keyobj.userId, rep.userId); 99 | assert.equal(keyobj.clientId, rep.clientId); 100 | assert.equal(keyobj.resourceUri, rep.resourceUri); 101 | assert.deepEqual(keyobj.createDate, rep.createDate); 102 | assert.deepEqual(keyobj.expirationDate, rep.expirationDate); 103 | assert.deepEqual(keyobj.bindDate, rep.bindDate); 104 | assert.deepEqual(keyobj.jwk, rep.jwk); 105 | }); 106 | it("coerces a JSON-Object to a KeyObject", function() { 107 | var rep = { 108 | "uri": "/keys/52100fa4-c222-46d0-994d-1ca885e4a3a2", 109 | "jwk": { 110 | "kid": "52100fa4-c222-46d0-994d-1ca885e4a3a2", 111 | "kty": "oct", 112 | "k": "ZMpktzGq1g6_r4fKVdnx9OaYr4HjxPjIs7l7SwAsgsg" 113 | }, 114 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 115 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 116 | "createDate": "2014-10-09T15:54:48Z", 117 | "bindDate": "2014-10-09T15:55:34Z", 118 | "expirationDate": "2014-10-10T15:55:34Z", 119 | "resourceUri": "/resources/7f35c3eb-95d6-4558-a7fc-1942e5f03094" 120 | }; 121 | var keyobj = KMS.KeyObject.fromObject(rep); 122 | assert.equal(keyobj.uri, rep.uri); 123 | assert.equal(keyobj.userId, rep.userId); 124 | assert.equal(keyobj.clientId, rep.clientId); 125 | assert.equal(keyobj.resourceUri, rep.resourceUri); 126 | assert.deepEqual(keyobj.createDate, new Date(rep.createDate)); 127 | assert.deepEqual(keyobj.expirationDate, new Date(rep.expirationDate)); 128 | assert.deepEqual(keyobj.bindDate, new Date(rep.bindDate)); 129 | assert.deepEqual(keyobj.jwk, rep.jwk); 130 | }); 131 | it("coerces a KeyObject to a KeyObject (identity)", function() { 132 | var rep = new KMS.KeyObject(); 133 | var keyobj = KMS.KeyObject.fromObject(rep); 134 | assert.equal(keyobj.uri, rep.uri); 135 | assert.equal(keyobj.userId, rep.userId); 136 | assert.equal(keyobj.clientId, rep.clientId); 137 | assert.equal(keyobj.resourceUri, rep.resourceUri); 138 | assert.deepEqual(keyobj.createDate, rep.createDate); 139 | assert.deepEqual(keyobj.expirationDate, rep.expirationDate); 140 | assert.deepEqual(keyobj.bindDate, rep.bindDate); 141 | assert.deepEqual(keyobj.jwk, rep.jwk); 142 | }); 143 | }); 144 | 145 | describe("#asKey", function() { 146 | it("converts 'jwk' to JWK.Key", function() { 147 | var json = { 148 | "uri": "/keys/52100fa4-c222-46d0-994d-1ca885e4a3a2", 149 | "jwk": { 150 | "kid": "52100fa4-c222-46d0-994d-1ca885e4a3a2", 151 | "kty": "oct", 152 | "k": "ZMpktzGq1g6_r4fKVdnx9OaYr4HjxPjIs7l7SwAsgsg" 153 | }, 154 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 155 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 156 | "createDate": "2014-10-09T15:54:48Z", 157 | "bindDate": "2014-10-09T15:55:34Z", 158 | "expirationDate": "2014-10-10T15:55:34Z", 159 | "resourceUri": "/resources/7f35c3eb-95d6-4558-a7fc-1942e5f03094" 160 | }; 161 | var keyobj = new KMS.KeyObject(json); 162 | return keyobj.asKey(). 163 | then(function(jwk) { 164 | assert.deepEqual(jwk.toJSON(true), json.jwk); 165 | }); 166 | }); 167 | it("fails if 'jwk' is not set", function() { 168 | var keyobj = new KMS.KeyObject(); 169 | return keyobj.asKey(). 170 | then(function() { 171 | assert.ok(false, "unexpected success"); 172 | }, function(err) { 173 | assert.equal(err.message, "'jwk' not set"); 174 | }); 175 | }); 176 | }); 177 | 178 | describe("#toJSON", function() { 179 | it("returns a JSON-Object", function() { 180 | var json = { 181 | "uri": "/keys/52100fa4-c222-46d0-994d-1ca885e4a3a2", 182 | "jwk": { 183 | "kid": "52100fa4-c222-46d0-994d-1ca885e4a3a2", 184 | "kty": "oct", 185 | "k": "ZMpktzGq1g6_r4fKVdnx9OaYr4HjxPjIs7l7SwAsgsg" 186 | }, 187 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 188 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 189 | "createDate": "2014-10-09T15:54:48Z", 190 | "bindDate": "2014-10-09T15:55:34Z", 191 | "expirationDate": "2014-10-10T15:55:34Z", 192 | "resourceUri": "/resources/7f35c3eb-95d6-4558-a7fc-1942e5f03094" 193 | }; 194 | var keyobj = new KMS.KeyObject(json); 195 | // Dates cause some headache -- stringify to compare 196 | assert.equal(JSON.stringify(keyobj.toJSON()), 197 | JSON.stringify(keyobj)); 198 | }); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /test/request-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 4 | */ 5 | "use strict"; 6 | 7 | var chai = require("chai"), 8 | clone = require("lodash.clone"), 9 | omit = require("lodash.omit"), 10 | jose = require("node-jose"); 11 | 12 | var KMS = { 13 | Context: require("../lib/context"), 14 | Request: require("../lib/request") 15 | }; 16 | var config = require("./config"); 17 | 18 | var assert = chai.assert; 19 | 20 | describe("KMS/Request", function() { 21 | describe("ctor", function() { 22 | it("creates an empty Request", function() { 23 | var kmsReq = new KMS.Request(); 24 | assert.deepEqual(kmsReq.body, {}); 25 | assert.equal(kmsReq.requestId, ""); 26 | assert.equal(kmsReq.uri, ""); 27 | assert.equal(kmsReq.method, ""); 28 | }); 29 | it("creates a Request from a body", function() { 30 | var body = { 31 | "client": { 32 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 33 | "credential": { 34 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 35 | "bearer": "ZWU5NGE2YWYtMGE2NC0..." 36 | } 37 | }, 38 | "method": "create", 39 | "uri": "/ecdhe", 40 | "requestId": "someid", 41 | "jwk": { 42 | "kty": "EC", 43 | "crv": "P-256", 44 | "x": "VoFkf6Wk5kDQ1ob6csBmiMPHU8jALwdtaap35Fsj20M", 45 | "y": "XymwN6u2PmsKbIPy5iij6qZ-mIyej5dvZWB_75lnRgQ" 46 | } 47 | }; 48 | var kmsReq = new KMS.Request(body); 49 | assert.deepEqual(kmsReq.body, body); 50 | assert.equal(kmsReq.requestId, "someid"); 51 | assert.equal(kmsReq.uri, "/ecdhe"); 52 | assert.equal(kmsReq.method, "create"); 53 | }); 54 | }); 55 | 56 | describe("#method", function() { 57 | it("get/sets property", function() { 58 | var kmsReq = new KMS.Request(); 59 | assert.equal(kmsReq.method, ""); 60 | 61 | kmsReq.method = "create"; 62 | assert.equal(kmsReq.method, "create"); 63 | 64 | kmsReq.method = "update"; 65 | assert.equal(kmsReq.method, "update"); 66 | 67 | kmsReq.method = null; 68 | assert.equal(kmsReq.method, ""); 69 | }); 70 | }); 71 | describe("#uri", function() { 72 | it("gets/sets property", function() { 73 | var kmsReq = new KMS.Request(); 74 | assert.equal(kmsReq.uri, ""); 75 | 76 | kmsReq.uri = "/ecdhe"; 77 | assert.equal(kmsReq.uri, "/ecdhe"); 78 | 79 | kmsReq.uri = "/some/other/path"; 80 | assert.equal(kmsReq.uri, "/some/other/path"); 81 | 82 | kmsReq.uri = null; 83 | assert.equal(kmsReq.uri, ""); 84 | }); 85 | }); 86 | describe("#requestId", function() { 87 | it("gets/sets property", function() { 88 | var kmsReq = new KMS.Request(); 89 | assert.equal(kmsReq.requestId, ""); 90 | 91 | kmsReq.requestId = "someid"; 92 | assert.equal(kmsReq.requestId, "someid"); 93 | 94 | kmsReq.requestId = null; 95 | assert.equal(kmsReq.requestId, ""); 96 | }); 97 | }); 98 | describe("#body", function() { 99 | it("gets/sets property", function() { 100 | var kmsReq = new KMS.Request(); 101 | assert.deepEqual(kmsReq.body, {}); 102 | 103 | var body = { 104 | "jwk": { 105 | "kty": "EC", 106 | "crv": "P-256", 107 | "x": "VoFkf6Wk5kDQ1ob6csBmiMPHU8jALwdtaap35Fsj20M", 108 | "y": "XymwN6u2PmsKbIPy5iij6qZ-mIyej5dvZWB_75lnRgQ" 109 | } 110 | }; 111 | kmsReq.body = body; 112 | assert.deepEqual(kmsReq.body, body); 113 | 114 | kmsReq.body = null; 115 | assert.deepEqual(kmsReq.body, {}); 116 | }); 117 | it("preserves core sub-properties across sets", function() { 118 | var kmsReq = new KMS.Request({ 119 | "client": { 120 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 121 | "credential": { 122 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 123 | "bearer": "ZWU5NGE2YWYtMGE2NC0..." 124 | } 125 | }, 126 | "method": "create", 127 | "uri": "/ecdhe", 128 | "requestId": "someid", 129 | "jwk": { 130 | "kty": "EC", 131 | "crv": "P-256", 132 | "x": "VoFkf6Wk5kDQ1ob6csBmiMPHU8jALwdtaap35Fsj20M", 133 | "y": "XymwN6u2PmsKbIPy5iij6qZ-mIyej5dvZWB_75lnRgQ" 134 | } 135 | }); 136 | var body = { 137 | "method": "delete", 138 | "uri": "/ecdhe/some-ecdhe-uri" 139 | }; 140 | kmsReq.body = body; 141 | assert.deepEqual(kmsReq.body, { 142 | "method": "create", 143 | "uri": "/ecdhe", 144 | "requestId": "someid" 145 | }); 146 | }); 147 | }); 148 | 149 | describe("#wrap", function() { 150 | function decryptIt(jwk, jwe) { 151 | var promise = jose.JWK.asKey(jwk); 152 | promise = promise.then(function(key) { 153 | var dec = jose.JWE.createDecrypt(key); 154 | return dec.decrypt(jwe); 155 | }); 156 | return promise; 157 | } 158 | 159 | var kmsCtx; 160 | before(function() { 161 | kmsCtx = new KMS.Context(); 162 | kmsCtx.clientInfo = omit(config.clientInfo, "key"); 163 | kmsCtx.serverInfo = { 164 | key: clone(config.serverInfo.cert) 165 | }; 166 | kmsCtx.ephemeralKey = config.sharedKey; 167 | }); 168 | 169 | it("gets/sets wrapped", function() { 170 | var kmsReq = new KMS.Request(); 171 | assert.equal(kmsReq.wrapped, ""); 172 | 173 | // NOTE: wrapped is not validated when set, so junk is OK 174 | kmsReq.wrapped = "header.encrypted_key.iv.ciphertext.tag"; 175 | assert.equal(kmsReq.wrapped, "header.encrypted_key.iv.ciphertext.tag"); 176 | 177 | kmsReq.wrapped = null; 178 | assert.equal(kmsReq.wrapped, ""); 179 | }); 180 | it("wraps with server key", function() { 181 | // partial from draft-biggs-saag-key-management-service 182 | var body = { 183 | "method": "create", 184 | "uri": "/ecdhe", 185 | "jwk": { 186 | "kty": "EC", 187 | "crv": "P-256", 188 | "x": "VoFkf6Wk5kDQ1ob6csBmiMPHU8jALwdtaap35Fsj20M", 189 | "y": "XymwN6u2PmsKbIPy5iij6qZ-mIyej5dvZWB_75lnRgQ" 190 | } 191 | }; 192 | var kmsReq = new KMS.Request(body); 193 | 194 | var promise = kmsReq.wrap(kmsCtx, { serverKey: true }); 195 | promise = promise.then(function(result) { 196 | assert.equal(typeof result, "string"); 197 | assert.equal(result, kmsReq.wrapped); 198 | 199 | // try to decrypt 200 | return decryptIt(config.serverInfo.key, result); 201 | }); 202 | promise = promise.then(function(result) { 203 | result = result.plaintext; 204 | result = JSON.parse(result.toString("utf8")); 205 | 206 | var expected = { 207 | "client": { 208 | clientId: "tester-web_e1fa9c13-108e-4fd0-a793-00f2644e6ad0", 209 | credential: { 210 | userId: "414baf5d-649b-4b4d-9a5a-0eec960c3db2", 211 | bearer: "IOAO1WboYGJLDb6W-sU7YQ.54k0eIaQDLYOr8uD9ZuEDeKzLRQnkAopV7vsQ0dOrv0" 212 | } 213 | }, 214 | "requestId": result.requestId, 215 | "method": "create", 216 | "uri": "/ecdhe", 217 | "jwk": { 218 | "kty": "EC", 219 | "crv": "P-256", 220 | "x": "VoFkf6Wk5kDQ1ob6csBmiMPHU8jALwdtaap35Fsj20M", 221 | "y": "XymwN6u2PmsKbIPy5iij6qZ-mIyej5dvZWB_75lnRgQ" 222 | } 223 | }; 224 | assert.deepEqual(result, expected); 225 | assert.deepEqual(kmsReq.body, expected); 226 | }); 227 | return promise; 228 | }); 229 | it("wraps with ephemeral key", function() { 230 | var kmsCtx = new KMS.Context(); 231 | kmsCtx.clientInfo = omit(config.clientInfo, "key"); 232 | kmsCtx.serverInfo = { 233 | key: clone(config.serverInfo.cert) 234 | }; 235 | kmsCtx.ephemeralKey = config.sharedKey; 236 | 237 | var body = { 238 | "method": "create", 239 | "uri": "/keys", 240 | "count": 10 241 | }; 242 | var kmsReq = new KMS.Request(body); 243 | 244 | var promise = kmsReq.wrap(kmsCtx, { serverKey: false }); 245 | promise = promise.then(function(result) { 246 | assert.equal(typeof result, "string"); 247 | assert.equal(result, kmsReq.wrapped); 248 | 249 | // try to decrypt 250 | return decryptIt(config.sharedKey.jwk, result); 251 | }); 252 | promise = promise.then(function(result) { 253 | result = result.plaintext; 254 | result = JSON.parse(result.toString("utf8")); 255 | 256 | var expected = { 257 | "client": { 258 | clientId: "tester-web_e1fa9c13-108e-4fd0-a793-00f2644e6ad0", 259 | credential: { 260 | userId: "414baf5d-649b-4b4d-9a5a-0eec960c3db2", 261 | bearer: "IOAO1WboYGJLDb6W-sU7YQ.54k0eIaQDLYOr8uD9ZuEDeKzLRQnkAopV7vsQ0dOrv0" 262 | } 263 | }, 264 | "requestId": result.requestId, 265 | "method": "create", 266 | "uri": "/keys", 267 | "count": 10 268 | }; 269 | assert.deepEqual(result, expected); 270 | assert.deepEqual(kmsReq.body, expected); 271 | }); 272 | return promise; 273 | }); 274 | it("wraps with a body-specified requestId", function() { 275 | var kmsCtx = new KMS.Context(), 276 | requestId = kmsCtx.requestId(); 277 | kmsCtx.clientInfo = omit(config.clientInfo, "key"); 278 | kmsCtx.serverInfo = { 279 | key: clone(config.serverInfo.cert) 280 | }; 281 | kmsCtx.ephemeralKey = config.sharedKey; 282 | 283 | var body = { 284 | "requestId": requestId, 285 | "method": "create", 286 | "uri": "/keys", 287 | "count": 10 288 | }; 289 | var kmsReq = new KMS.Request(body); 290 | 291 | var promise = kmsReq.wrap(kmsCtx, { serverKey: false }); 292 | promise = promise.then(function(result) { 293 | assert.equal(typeof result, "string"); 294 | assert.equal(result, kmsReq.wrapped); 295 | 296 | // try to decrypt 297 | return decryptIt(config.sharedKey.jwk, result); 298 | }); 299 | promise = promise.then(function(result) { 300 | result = result.plaintext; 301 | result = JSON.parse(result.toString("utf8")); 302 | 303 | var expected = { 304 | "client": { 305 | clientId: "tester-web_e1fa9c13-108e-4fd0-a793-00f2644e6ad0", 306 | credential: { 307 | userId: "414baf5d-649b-4b4d-9a5a-0eec960c3db2", 308 | bearer: "IOAO1WboYGJLDb6W-sU7YQ.54k0eIaQDLYOr8uD9ZuEDeKzLRQnkAopV7vsQ0dOrv0" 309 | } 310 | }, 311 | "requestId": result.requestId, 312 | "method": "create", 313 | "uri": "/keys", 314 | "count": 10 315 | }; 316 | assert.equal(result.requestId, requestId); 317 | assert.deepEqual(result, expected); 318 | assert.deepEqual(kmsReq.body, expected); 319 | }); 320 | return promise; 321 | }); 322 | it("wraps with an explicit requestId", function() { 323 | var kmsCtx = new KMS.Context(), 324 | requestId = kmsCtx.requestId(); 325 | kmsCtx.clientInfo = omit(config.clientInfo, "key"); 326 | kmsCtx.serverInfo = { 327 | key: clone(config.serverInfo.cert) 328 | }; 329 | kmsCtx.ephemeralKey = config.sharedKey; 330 | 331 | var body = { 332 | "method": "create", 333 | "uri": "/keys", 334 | "count": 10 335 | }; 336 | var kmsReq = new KMS.Request(body); 337 | kmsReq.requestId = requestId; 338 | 339 | var promise = kmsReq.wrap(kmsCtx, { serverKey: false }); 340 | promise = promise.then(function(result) { 341 | assert.equal(typeof result, "string"); 342 | assert.equal(result, kmsReq.wrapped); 343 | 344 | // try to decrypt 345 | return decryptIt(config.sharedKey.jwk, result); 346 | }); 347 | promise = promise.then(function(result) { 348 | result = result.plaintext; 349 | result = JSON.parse(result.toString("utf8")); 350 | 351 | var expected = { 352 | "client": { 353 | clientId: "tester-web_e1fa9c13-108e-4fd0-a793-00f2644e6ad0", 354 | credential: { 355 | userId: "414baf5d-649b-4b4d-9a5a-0eec960c3db2", 356 | bearer: "IOAO1WboYGJLDb6W-sU7YQ.54k0eIaQDLYOr8uD9ZuEDeKzLRQnkAopV7vsQ0dOrv0" 357 | } 358 | }, 359 | "requestId": result.requestId, 360 | "method": "create", 361 | "uri": "/keys", 362 | "count": 10 363 | }; 364 | assert.equal(result.requestId, requestId); 365 | assert.deepEqual(result, expected); 366 | assert.deepEqual(kmsReq.body, expected); 367 | }); 368 | return promise; 369 | }); 370 | }); 371 | }); 372 | -------------------------------------------------------------------------------- /test/response-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 4 | */ 5 | "use strict"; 6 | 7 | var chai = require("chai"), 8 | clone = require("lodash.clone"), 9 | omit = require("lodash.omit"); 10 | 11 | var KMS = { 12 | Context: require("../lib/context"), 13 | Response: require("../lib/response") 14 | }; 15 | var config = require("./config"); 16 | 17 | var assert = chai.assert; 18 | 19 | describe("KMS/Response", function() { 20 | describe("ctor", function() { 21 | it("creates an empty Response", function() { 22 | var kmsRsp = new KMS.Response(); 23 | assert.equal(kmsRsp.wrapped, ""); 24 | assert.deepEqual(kmsRsp.body, {}); 25 | assert.equal(kmsRsp.status, 0); 26 | assert.equal(kmsRsp.reason, ""); 27 | assert.equal(kmsRsp.requestId, ""); 28 | }); 29 | it("creates a Response from a wrapped", function() { 30 | var wrapped = "eyJhbGciOiJSU0ExXzUiLCJraWQiOiJrbXMuZXhhbXBsZSIsImVuYyI6IkEyNTZHQ00ifQ" + 31 | "." + 32 | "C-ahGqjtglQYo-AQ6ytfmJOQO29DCBhaYKx7n_QapLbDMe3XEgQEstPxw5JuphO9oBl7Hok4pjn-e0k1qnNNzcP4T3IFkXsvqtXNZt_mYuwTVuh02FMf5In-s7-B0PPxwe79cfVaWork88faWu2dqqSF6Xb8Gfl1bq4hdqSPpQs3AkDUAyDHkxEhl3tBH-V6gyju8_8oQ6O1q_BhcPFg6cZsTQqKZDGwzpHHbosoZVSao4O9NFo-QEsIxUTbpzpMeDN4cLjzxpmiodD2jp2ncePKCwCDysSG0rrGgNJgxeSkRPxT-h5nRDFoPzMjrni1pHQIhyaFxOHVqspy7r_SXA" + 33 | "." + 34 | "VitTtnfsjOj1qbhj.zq3_dix20_MVYboUQVJp9i0m7q-2HKmL5bVsJ8ilqT3OZEMcED7KkX9LMa5MDKMMi6USVsqAhnXFxHBhd07TWvxKP1rBfzuoSoNfdlB1LZQPXYxLk4Hk1fuRgEwN3BZ2tdYZUVhPq25OgctDLNVyDvoR-kyVKK9U2PvqU_LiJPdNRkwwwKwc2YTVNIXjSVgQpRkIFhW5rJSmZJrFRo8xQS5Cr6BRC-hp5-ysi-3OERqwJWN4vZNGWVtxD4RpPFoo1YQZSNocz9x6STg1_lDBffSPF57WbDTVuv1B8pupkoLgEMQ61x5jWsLki90MmhLn-jliVU9h-fbdf9tPNDS_Pg2t9g7O_Y9KQjcWR9Wm_OWlmC5Un2F0FybW58uQusqtJXAaitiRPhx_dHqpijRSXsdi0hAd1mPyY3MKWF8gq3PdmGauEFyqDwkIgak4jMUMJUVUaFJJhNKIV1LFdAGjW6nj2_lhOF4NkVTeG8gKnSx8vAJ1TYFYEmOL5WB6PUbhIpZdihOJpz71" + 35 | "." + 36 | "kXkDo2m6JE2tVnSEYDg70w"; 37 | var kmsRsp = new KMS.Response(wrapped); 38 | assert.equal(kmsRsp.wrapped, wrapped); 39 | assert.deepEqual(kmsRsp.body, {}); 40 | assert.equal(kmsRsp.status, 0); 41 | assert.equal(kmsRsp.reason, ""); 42 | assert.equal(kmsRsp.requestId, ""); 43 | }); 44 | }); 45 | 46 | describe("#unwrap", function() { 47 | var kmsCtx; 48 | before(function() { 49 | kmsCtx = new KMS.Context(); 50 | kmsCtx.clientInfo = omit(config.clientInfo, "key"); 51 | kmsCtx.serverInfo = { 52 | key: clone(config.serverInfo.cert) 53 | }; 54 | kmsCtx.ephemeralKey = config.sharedKey; 55 | }); 56 | it("unwraps with server key", function() { 57 | var wrapped = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imttcy5leGFtcGxlIn0" + 58 | "." + 59 | "eyJzdGF0dXMiOjIwMSwicmVxdWVzdElkIjoiODIzNDlhNDEtMGU1Ni00N2E4LTk1OWYtOTk5NDYzZWY5NTc0Iiwia2V5Ijp7InVyaSI6Ii9lY2RoZS9lYTlmMzg1OC0xMjQwLTQzMjgtYWUyMi1hMTVmNjA3MjMwNmYiLCJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiI4bWRhc25FWmFjMkxXeE13S0V4aWtLVTVMTGFjTFFsY090N0E2bjFaR1VDIiwieSI6Imx4czdsbjVMdFpVRV9HRTd5emM2QlpPd0J4dE9mdGRzcjhIVmgtMTRrc1MifSwidXNlcklkIjoiODQyZTJkODItN2U3MS00MDQwLThlYjktZDk3N2ZlODg4ODA3IiwiY2xpZW50SWQiOiJhbmRyb2lkX2E2YWEwMTJhLTA3OTUtNGZiNC1iZGRiLWYwNGFiZGE5ZTM0ZiIsImNyZWF0ZURhdGUiOiIyMDE0LTEwLTA5VDE1OjU0OjQ4WiIsImV4cGlyYXRpb25EYXRlIjoiMjAxNC0xMC0wOVQxNjo1NDo0OFoifX0" + 60 | "." + 61 | "HsJBfmUPUYxD8BX3HE4vHsU3qzkNvdYhE6sTD7r6Kh6quqcd-gUMOe96OlEpLyWTmmH9Yopb-Urlc0NCIbevpCN6I-QaP5wijjNmTQtZtrUF0EvcoJQIgPey_ahrqDHD7YAGAmOmkJfrfRZKjW95BgDg4_6SPyzHu71BrdDqS9AKXPTfMuA_t9cmDobfL-99770h4Pvw4WxN0VMJvcF4pIPssHsU-iKRyKEiFxQt1kwCpYt8tqexIZaIvB9UnmVeULJsqUN1Ui4pZYsdl3zZ9JXFMPKAdB3xKSM5zQTfRx49oatqF0xEVeCnyQJ7KHEZEgfTX3ide_bC8lr4N_miEA"; 62 | var kmsRsp = new KMS.Response(wrapped); 63 | 64 | var promise = kmsRsp.unwrap(kmsCtx); 65 | promise = promise.then(function(result) { 66 | var json = { 67 | "status": 201, 68 | "requestId": "82349a41-0e56-47a8-959f-999463ef9574", 69 | "key": { 70 | "uri": "/ecdhe/ea9f3858-1240-4328-ae22-a15f6072306f", 71 | "jwk": { 72 | "kty": "EC", 73 | "crv": "P-256", 74 | "x": "8mdasnEZac2LWxMwKExikKU5LLacLQlcOt7A6n1ZGUC", 75 | "y": "lxs7ln5LtZUE_GE7yzc6BZOwBxtOftdsr8HVh-14ksS" 76 | }, 77 | "userId": "842e2d82-7e71-4040-8eb9-d977fe888807", 78 | "clientId": "android_a6aa012a-0795-4fb4-bddb-f04abda9e34f", 79 | "createDate": "2014-10-09T15:54:48Z", 80 | "expirationDate": "2014-10-09T16:54:48Z" 81 | } 82 | }; 83 | assert.deepEqual(result, json); 84 | assert.deepEqual(kmsRsp.body, json); 85 | assert.equal(kmsRsp.status, 201); 86 | assert.equal(kmsRsp.reason, ""); 87 | assert.equal(kmsRsp.requestId, "82349a41-0e56-47a8-959f-999463ef9574"); 88 | }); 89 | return promise; 90 | }); 91 | it("unwraps with ephemeral key", function() { 92 | var wrapped = "eyJhbGciOiJkaXIiLCJraWQiOiIvZWNkaGUvYTQ3NTI0MTEtZGQ1Zi00ODNjLThkOTMtZTgyMjFmNmFiNGE2IiwiZW5jIjoiQTI1NkdDTSJ9" + 93 | "." + 94 | "." + 95 | "yy2PuLwzsv5dpTvE" + 96 | "." + 97 | "o7KCl8FCevJloKrrVlo0w00Ze3GrazCbXHVsFZTp2QCnvmiWNtX77MZizvDUH5gs8fhryVU8GevfJuO98kBkt2y36NPZdPoEHdS19FNGlXnZTXZHs53Fx6DguoEXRG6zxZbUQqf6swveRUhMzTCO2iakP74CCAxVl3U6V1Oi8ASV6nioZmGPOYgpXvFaqtIY5uYUCNQIs60GK6DBiQZb2E7ByQIorBKIoGAA6ts2KHjBJPwgS1YSF-9qgQWjazoXuw-ab95WxUOoeRRMiewH7OIKWSEk45dMoyfGZELM9mWUZBWIhGTFOhssQ6IuT3OMzeflcgsyTgMR6KUJVYpvCoqskZTy-ORaYkrSHLfY7RAeh1ONlemF5baamEZKFOy_XyzX2kCZ2T3h_zPm5p97KkQ3yj21pnT2KYERcnlZ8yvH6PYZgNbw_foH9F2GONbPwDRBUrvi5l7l1VF84t2kRvY6dwgTGB_6OfDDWIiHfGo" + 98 | "." + 99 | "LzEjepG_ronHKGBRCf9wag"; 100 | var kmsRsp = new KMS.Response(wrapped); 101 | 102 | var promise = kmsRsp.unwrap(kmsCtx); 103 | promise = promise.then(function(result) { 104 | var json = { 105 | status: 201, 106 | requestId: "4f782388-f76e-477f-ad3d-a4c7cf5ceac8", 107 | resource: { 108 | uri: "/resources/7f35c3eb-95d6-4558-a7fc-1942e5f03094", 109 | authorizationUris: [ 110 | "/authorizations/50e9056d-0700-4919-b55f-84cd78a2a65e", 111 | "/authorizations/db4c95ab-3fbf-42a8-989f-f53c1f13cc9a" 112 | ], 113 | keyUris: [ 114 | "/keys/b4cba4da-a984-4af2-b54f-3ca04acfe461", 115 | "/keys/2671413c-ab80-4f19-a0a4-ae07e1a94e90" 116 | ] 117 | } 118 | }; 119 | assert.deepEqual(result, json); 120 | assert.deepEqual(kmsRsp.body, json); 121 | assert.equal(kmsRsp.status, 201); 122 | assert.equal(kmsRsp.reason, ""); 123 | assert.equal(kmsRsp.requestId, "4f782388-f76e-477f-ad3d-a4c7cf5ceac8"); 124 | }); 125 | return promise; 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test/usecase-ecdhe-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. 4 | */ 5 | "use strict"; 6 | 7 | var chai = require("chai"), 8 | clone = require("lodash.clone"), 9 | omit = require("lodash.omit"), 10 | jose = require("node-jose"), 11 | uuid = require("uuid").v4; 12 | 13 | var assert = chai.assert; 14 | 15 | var KMS = require("../"); 16 | var config = require("./config"); 17 | 18 | function decryptWithKey(key, input) { 19 | var promise; 20 | promise = jose.JWK.asKey(key); 21 | promise = promise.then(function(jwk) { 22 | var jwe = jose.JWE.createDecrypt(jwk); 23 | return jwe.decrypt(input); 24 | }); 25 | promise = promise.then(function(result) { 26 | result = result.plaintext; 27 | result = result.toString("utf8"); 28 | result = JSON.parse(result); 29 | return result; 30 | }); 31 | return promise; 32 | } 33 | function signWithKey(key, input) { 34 | var opts = { 35 | compact: true 36 | }; 37 | var signer = { 38 | header: { 39 | alg: "RS256" 40 | }, 41 | key: key 42 | }; 43 | input = JSON.stringify(input); 44 | input = new Buffer(input, "utf8"); 45 | var jws = jose.JWS.createSign(opts, signer); 46 | return jws.final(input); 47 | } 48 | 49 | describe("KMS/use cases/ECDHE", function() { 50 | var clientCtx, 51 | serverCtx, 52 | channel; 53 | 54 | before(function() { 55 | clientCtx = new KMS.Context(); 56 | clientCtx.clientInfo = omit(config.clientInfo, "key"); 57 | clientCtx.serverInfo = { 58 | key: clone(config.serverInfo.cert) 59 | }; 60 | 61 | serverCtx = new KMS.Context(); 62 | 63 | channel = { 64 | request: null, 65 | response: null 66 | }; 67 | }); 68 | 69 | it("generates ECDH key", function() { 70 | var promise = clientCtx.createECDHKey(); 71 | promise = promise.then(function(result) { 72 | assert.ok(result instanceof KMS.KeyObject); 73 | clientCtx.ephemeralKey = result; 74 | }); 75 | return promise; 76 | }); 77 | it("Creates and Wraps ECDHE Request", function() { 78 | var kmsReq = new KMS.Request({ 79 | uri: "/echde", 80 | method: "create", 81 | jwk: clientCtx.ephemeralKey.jwk 82 | }); 83 | var promise = kmsReq.wrap(clientCtx, { 84 | serverKey: true 85 | }); 86 | promise = promise.then(function(wrapped) { 87 | assert.equal(wrapped, kmsReq.wrapped); 88 | channel.request = wrapped; 89 | }); 90 | return promise; 91 | }); 92 | it("pretends to be a KMS", function() { 93 | var promise, 94 | request; 95 | 96 | promise = Promise.resolve(channel.request); 97 | // Decrypt the ECDHE request 98 | promise = promise.then(function(r) { 99 | return decryptWithKey(config.serverInfo.key, r); 100 | }); 101 | // Generate server ECDH key 102 | promise = promise.then(function(r) { 103 | request = r; 104 | 105 | var serverCtx = new KMS.Context(); 106 | return serverCtx.createECDHKey(); 107 | }); 108 | // create and sign response 109 | promise = promise.then(function(localEcdhe) { 110 | var kid = "/ecdhe/" + uuid(); 111 | var key = localEcdhe.toJSON(); 112 | key.uri = kid; 113 | var json = { 114 | status: 201, 115 | requestId: request.requestId, 116 | key: key 117 | }; 118 | serverCtx.ephemeralKey = key; 119 | return signWithKey(config.serverInfo.key, json); 120 | }); 121 | // "send" response + calculate shared key 122 | promise = promise.then(function(response) { 123 | channel.response = response; 124 | 125 | var derive = [ 126 | serverCtx.ephemeralKey.asKey(), 127 | jose.JWK.asKey(request.jwk) 128 | ]; 129 | derive = Promise.all(derive); 130 | derive = derive.then(function(keys) { 131 | var local = keys[0].toObject(true), 132 | remote = keys[1].toObject(); 133 | return jose.JWA.derive("ECDH-HKDF", local, { public: remote }); 134 | }); 135 | return derive; 136 | }); 137 | // remember shared key in server context 138 | promise = promise.then(function(shared) { 139 | var key = serverCtx.ephemeralKey.toJSON(); 140 | key.jwk = { 141 | kty: "oct", 142 | kid: key.uri, 143 | alg: "A256GCM", 144 | k: jose.util.base64url.encode(shared) 145 | }; 146 | serverCtx.ephemeralKey = key; 147 | }); 148 | 149 | return promise; 150 | }); 151 | 152 | it("Unwraps and Uses ECDHE Response", function() { 153 | var wrapped = channel.response; 154 | 155 | var response = new KMS.Response(wrapped); 156 | var promise = response.unwrap(clientCtx); 157 | promise = promise.then(function(result) { 158 | return clientCtx.deriveEphemeralKey(result.key); 159 | }); 160 | promise = promise.then(function(shared) { 161 | var expected = serverCtx.ephemeralKey; 162 | assert.deepEqual(shared.jwk, expected.jwk); 163 | clientCtx.ephemeralKey = shared; 164 | }); 165 | return promise; 166 | }); 167 | }); 168 | --------------------------------------------------------------------------------