├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .yo-rc.json ├── BUG-BOUNTY.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── Todo.md ├── babel.config.js ├── dist ├── jose.js ├── jose.js.map └── jose.min.js ├── examples ├── jose-jwe-example1.html ├── jose-jwe-example2.html ├── jose-jws-ecdsa-example.html └── jose-jws-rsa-example.html ├── jose-jwe-jws.d.ts ├── lib ├── jose-core.js ├── jose-core.test.js ├── jose-jwe-decrypt.js ├── jose-jwe-encrypt.js ├── jose-jwe-webcryptographer.js ├── jose-jws-sign.js ├── jose-jws-verify.js └── jose-utils.js ├── package.json ├── test ├── jose-jwe-test.js ├── jose-jws-ecdsa-test.js ├── jose-jws-rsa-test.js └── qunit-promises.js ├── webpack.dev.js └── webpack.prod.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest/globals": true 7 | }, 8 | "extends": [ 9 | "standard" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "rules": { 20 | "semi": ["error", "always"] 21 | }, 22 | "plugins": ["jest"] 23 | } 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/* -diff 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules 3 | .idea 4 | output.log 5 | 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | - "node" 5 | 6 | addons: 7 | chrome: stable 8 | 9 | before_install: 10 | - npm install -g grunt-cli 11 | 12 | install: npm install 13 | 14 | script: grunt with_coverage --verbose 15 | 16 | after_success: grunt coveralls 17 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "jose-jwe-jws": { 3 | "configuration": { 4 | "config": { 5 | "topScope": [ 6 | "const webpack = require('webpack')", 7 | "const path = require('path')", 8 | "\n", 9 | "/*\n * SplitChunksPlugin is enabled by default and replaced\n * deprecated CommonsChunkPlugin. It automatically identifies modules which\n * should be splitted of chunk by heuristics using module duplication count and\n * module category (i. e. node_modules). And splits the chunks…\n *\n * It is safe to remove \"splitChunks\" from the generated configuration\n * and was added as an educational example.\n *\n * https://webpack.js.org/plugins/split-chunks-plugin/\n *\n */", 10 | "/*\n * We've enabled UglifyJSPlugin for you! This minifies your app\n * in order to load faster and run less javascript.\n *\n * https://github.com/webpack-contrib/uglifyjs-webpack-plugin\n *\n */", 11 | "const UglifyJSPlugin = require('uglifyjs-webpack-plugin');", 12 | "\n" 13 | ], 14 | "webpackOptions": { 15 | "module": { 16 | "rules": [ 17 | { 18 | "include": [ 19 | "path.resolve(__dirname, 'src')" 20 | ], 21 | "loader": "'babel-loader'", 22 | "options": { 23 | "plugins": [ 24 | "'syntax-dynamic-import'" 25 | ], 26 | "presets": [ 27 | [ 28 | "'env'", 29 | { 30 | "'modules'": false 31 | } 32 | ] 33 | ] 34 | }, 35 | "test": "/\\.js$/" 36 | } 37 | ] 38 | }, 39 | "entry": "./lib/jose-core", 40 | "output": { 41 | "filename": "'[name].[chunkhash].js'", 42 | "path": "path.resolve(__dirname, 'dist')" 43 | }, 44 | "mode": "'development'", 45 | "plugins": [ 46 | "new UglifyJSPlugin()" 47 | ], 48 | "optimization": { 49 | "splitChunks": { 50 | "cacheGroups": { 51 | "vendors": { 52 | "priority": -10, 53 | "test": "/[\\\\/]node_modules[\\\\/]/" 54 | } 55 | }, 56 | "chunks": "'async'", 57 | "minChunks": 1, 58 | "minSize": 30000, 59 | "name": true 60 | } 61 | } 62 | }, 63 | "configName": "dev" 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /BUG-BOUNTY.md: -------------------------------------------------------------------------------- 1 | Serious about security 2 | ====================== 3 | 4 | Square recognizes the important contributions the security research community 5 | can make. We therefore encourage reporting security issues with the code 6 | contained in this repository. 7 | 8 | If you believe you have discovered a security vulnerability, please follow the 9 | guidelines at https://hackerone.com/square-open-source 10 | 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project should eventually adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), 6 | however it has not stabilized, yet. 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.2.2] - 2020-01-13 11 | ### Fixed 12 | - Fix library export for TypeScript (#125) 13 | 14 | ### Changed 15 | - Update of many dev dependencies (#120, #123, #134, #137, #139, #140) 16 | 17 | ## [0.2.1] - 2019-08-08 18 | ### Fixed 19 | - Fix WebCryptographer export (#116). 20 | 21 | ### Changed 22 | - Update of many dev dependencies (#103, #104, #105, #107, #108, #111, #112) 23 | 24 | ### Deprecated 25 | - Node.js 9 is unsupported. CI is configured to run latest Node.js version and latest 26 | LTS release (#113). 27 | 28 | ## [0.2.0] - 2019-07-23 29 | ### Added 30 | - Support for signing and verifying using EC keys (#95). 31 | 32 | ### Fixed 33 | - Fix babel-loader config (#91). 34 | - `window.crypto` check now works for Service Workers (#92). 35 | - Add missing method signature to type definitions (#98). 36 | - Fix default ES6-style export (#97). 37 | 38 | ### Changed 39 | - Use ESLint instead of JSHint and adopt [JavaScript Standard Style](https://standardjs.com/) 40 | for the project -- with the exception of [semi rule](https://eslint.org/docs/rules/semi) 41 | (we still require semicolons everywhere) (#99). 42 | 43 | ## [0.1.7] - 2019-02-21 44 | ### Changed 45 | - Library is now written in ES6 and Babel is used for backwards compatibility (in browsers 46 | and Node.js environments) and it works with Webpack (#82). 47 | 48 | ### Fixed 49 | - Use passed algorithm when importing private RSA key (#67). 50 | 51 | ## [0.1.6] - 2017-10-16 52 | ### Added 53 | - Better type definitions that now also include allowed algorithm types (#61). 54 | 55 | ## [0.1.5] - 2017-05-20 56 | ### Fixed 57 | - Perform UTF-8 conversion on JSON stringified objects, not just strings (#55). 58 | 59 | 60 | ## [0.1.4] - 2016-10-25 61 | ### Added 62 | - Add payload to successful verification results (#48). 63 | 64 | ### Fixed 65 | - Ability to use `setCrypto` API in browsers (#49). 66 | - Allow CryptoKey to be recognized even when provided by minified applications (#50). 67 | 68 | ## [0.1.2] - 2016-09-16 69 | ### Added 70 | - Handle UTF-8 plaintexts (as allowed by the JOSE spec) (#42 & #46). 71 | 72 | 73 | ## [0.1.1] - 2016-09-06 74 | ### Fixed 75 | - Restore serialization methods for `SignedJws` interface (#39). 76 | - Replace Promise.accept() with Promise.resolve() as Promise API changed (#45). 77 | 78 | ## [0.1.0] - 2016-04-11 79 | ### Added 80 | - JWS support (#15). 81 | - Direct encryption using pre-shared symmetric keys (#22). 82 | - Add optional argument in Verifier that returns `Promise` given 83 | a key id (#24). 84 | - CommonJS packaging that allows for importing js-jose via Webpack (#27). 85 | - Support for Node.js, however it still requires a use of [polyfill](https://github.com/PeculiarVentures/node-webcrypto-ossl) (#30). 86 | - The API is enriched with TypeScript type definitions (#28 & #35). 87 | 88 | ### Changed 89 | - NPM package renamed to `jose-jwe-jws` 90 | 91 | ## [0.0.3] - 2015-03-03 92 | ### Added 93 | - Travis CI integration (#8). 94 | - Browser support check that ensures necessary APIs are available before 95 | JOSE operations (#11). 96 | 97 | ## [0.0.2] - 2015-02-11 98 | ### Added 99 | - Partial support for user defined headers (#2). 100 | - First NPM release. 101 | 102 | ### Fixed 103 | - Bug in public key import (#1). 104 | 105 | ## [0.0.1] - 2014-11-25 106 | ### Added 107 | - Initial release with support for JWE encryption. 108 | - Support for key encryption algorithms: RSA-OAEP (default), RSA-OAEP-256, 109 | A128KW (not recommended for use) and A256KW (not recommended for use). 110 | - Support for content encryption: A128CBC-HS256, A256CBC-HS512, A128GCM, 111 | A256GCM (default). 112 | 113 | [Unreleased]: https://github.com/square/js-jose/compare/v0.2.2...HEAD 114 | [0.2.2]: https://github.com/square/js-jose/compare/v0.2.1...v0.2.2 115 | [0.2.1]: https://github.com/square/js-jose/compare/v0.2.0...v0.2.1 116 | [0.2.0]: https://github.com/square/js-jose/compare/v0.1.7...v0.2.0 117 | [0.1.7]: https://github.com/square/js-jose/compare/v0.1.6...v0.1.7 118 | [0.1.6]: https://github.com/square/js-jose/compare/v0.1.5...v0.1.6 119 | [0.1.5]: https://github.com/square/js-jose/compare/v0.1.4...v0.1.5 120 | [0.1.4]: https://github.com/square/js-jose/compare/v0.1.2...v0.1.4 121 | [0.1.2]: https://github.com/square/js-jose/compare/v0.1.1...v0.1.2 122 | [0.1.1]: https://github.com/square/js-jose/compare/v0.1.0...v0.1.1 123 | [0.1.0]: https://github.com/square/js-jose/compare/v0.0.3...v0.1.0 124 | [0.0.3]: https://github.com/square/js-jose/compare/v0.0.2...v0.0.3 125 | [0.0.2]: https://github.com/square/js-jose/compare/v0.0.1...v0.0.2 126 | [0.0.1]: https://github.com/square/js-jose/releases/tag/v0.0.1 127 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you would like to contribute code to js-jose you can do so through GitHub by 4 | forking the repository and sending a pull request. 5 | 6 | When submitting code, please make every effort to follow existing conventions 7 | and style in order to keep the code as readable as possible. Please also make 8 | sure all tests pass by running `npm run build`. 9 | 10 | Before your code can be accepted into the project you must also sign the 11 | Individual Contributor License Agreement. We use [cla-assistant.io][1] and you 12 | will be prompted to sign once a pull request is opened. 13 | 14 | [1]: https://cla-assistant.io/ 15 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | const webpackDevConfig = require('./webpack.dev'); 2 | const webpackProdConfig = require('./webpack.prod'); 3 | 4 | module.exports = function (grunt) { 5 | var config = { 6 | pkg: grunt.file.readJSON('package.json'), 7 | 8 | run: { 9 | jest: { 10 | cmd: 'jest' 11 | } 12 | }, 13 | 14 | eslint: { 15 | options: { 16 | fix: true 17 | }, 18 | target: 'lib' 19 | }, 20 | 21 | webpack: { 22 | dev: webpackDevConfig, 23 | prod: webpackProdConfig 24 | }, 25 | 26 | karma: { 27 | with_coverage: { 28 | options: { 29 | preprocessors: { 30 | 'dist/jose.js': ['coverage'] 31 | }, 32 | reporters: ['coverage', 'progress'], 33 | coverageReporter: { 34 | type: 'lcovonly', 35 | dir: 'coverage/' 36 | }, 37 | frameworks: ['qunit'], 38 | files: [ 39 | { pattern: 'dist/jose.js', watching: false, included: true }, 40 | { pattern: 'test/qunit-promises.js', watching: false, included: true }, 41 | 'test/jose-*.js' 42 | ], 43 | client: { 44 | clearContext: false, 45 | qunit: { 46 | showUI: true, 47 | testTimeout: 5000 48 | } 49 | }, 50 | autoWatch: true, 51 | browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessNoSandbox'], 52 | customLaunchers: { 53 | ChromeHeadlessNoSandbox: { 54 | base: 'ChromeHeadless', 55 | flags: ['--no-sandbox'] 56 | } 57 | }, 58 | singleRun: true, 59 | plugins: ['karma-coverage', 'karma-qunit', 'karma-chrome-launcher'] 60 | } 61 | }, 62 | without_coverage: { 63 | options: { 64 | frameworks: ['qunit'], 65 | files: [ 66 | { pattern: 'dist/jose.js', watching: false, included: true }, 67 | { pattern: 'test/qunit-promises.js', watching: false, included: true }, 68 | 'test/jose-*.js' 69 | ], 70 | client: { 71 | clearContext: false, 72 | qunit: { 73 | showUI: true, 74 | testTimeout: 5000 75 | } 76 | }, 77 | autoWatch: true, 78 | browsers: ['Chrome'], 79 | browserConsoleLogOptions: { 80 | level: 'info', 81 | format: '%b %T: %m', 82 | path: 'output.log', 83 | terminal: true 84 | }, 85 | singleRun: true, 86 | logLevel: 'info' 87 | } 88 | } 89 | }, 90 | 91 | coveralls: { 92 | options: { 93 | debug: true, 94 | coverageDir: 'coverage', 95 | force: true, 96 | recursive: true 97 | } 98 | } 99 | }; 100 | 101 | if (process.env.TRAVIS) { 102 | config.karma.with_coverage.browsers = ['ChromeHeadlessNoSandbox']; 103 | config.karma.without_coverage.browsers = ['ChromeHeadlessNoSandbox']; 104 | } 105 | 106 | grunt.initConfig(config); 107 | 108 | grunt.loadNpmTasks('grunt-run'); 109 | grunt.loadNpmTasks('grunt-contrib-concat'); 110 | grunt.loadNpmTasks('grunt-eslint'); 111 | grunt.loadNpmTasks('grunt-contrib-uglify'); 112 | grunt.loadNpmTasks('grunt-karma'); 113 | grunt.loadNpmTasks('grunt-karma-coveralls'); 114 | grunt.loadNpmTasks('grunt-webpack'); 115 | 116 | grunt.registerTask('default', ['eslint', 'run:jest', 'webpack', 'karma:without_coverage']); 117 | grunt.registerTask('with_coverage', ['eslint', 'run:jest', 'webpack', 'karma:with_coverage']); 118 | }; 119 | -------------------------------------------------------------------------------- /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 | # JS JOSE (DEPRECATED) 2 | 3 | This repository is **DEPRECATED**. 4 | 5 | It will be placed in an archived state on **February 27th, 2023**. 6 | 7 | ## What should I use instead? 8 | 9 | We recommend users of `square/js-jose` migrate to `panva/jose`: 10 | 11 | https://github.com/panva/jose 12 | 13 | This library is available in [NPM] and actively maintained. No support, security 14 | fixes or updates will be delivered to the Square repository. 15 | 16 | [NPM]: https://www.npmjs.com/package/jose 17 | -------------------------------------------------------------------------------- /Todo.md: -------------------------------------------------------------------------------- 1 | TODO List 2 | ========= 3 | 4 | * Improve README. There is some stigma attached to doing crypto in the browser. 5 | This library uses the browser's Crypto API, so it's not as bad as it sounds :) 6 | * The code currently only works on browsers which implement the Web Crypto API. 7 | We should expose a way to check if the current browser is supported or not. 8 | * Think about pros vs cons of implementing polyfills for older browsers. We will 9 | need to think about entropy, timing leaks, etc. 10 | * Build a unittest coverage map 11 | * Make it easier to build a subset of the library. Some people might only want 12 | to do encryption or encryption with a specific algorithm. 13 | * Rethink error handling. Do we really need JoseJWE.assert? 14 | * Add support for importing public keys as PEM files and validating signatures. 15 | * Improve handling of malformed JSON input (see https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-10.12) 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [ 4 | ['@babel/preset-env', { targets: { node: 'current' } }], 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /dist/jose.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Jose=e():t.Jose=e()}(window,function(){return function(t){var e={};function r(n){if(e[n])return e[n].exports;var i=e[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)r.d(n,i,function(e){return t[e]}.bind(null,i));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=1)}([function(t,e,r){"use strict";r.r(e),r.d(e,"importPublicKey",function(){return u}),r.d(e,"importPrivateKey",function(){return s}),r.d(e,"importEcPublicKey",function(){return f}),r.d(e,"importEcPrivateKey",function(){return c}),r.d(e,"importRsaPublicKey",function(){return h}),r.d(e,"importRsaPrivateKey",function(){return y}),r.d(e,"isString",function(){return p}),r.d(e,"arrayish",function(){return l}),r.d(e,"convertRsaKey",function(){return g}),r.d(e,"arrayFromString",function(){return d}),r.d(e,"arrayFromUtf8String",function(){return v}),r.d(e,"stringFromArray",function(){return m}),r.d(e,"utf8StringFromArray",function(){return w}),r.d(e,"stripLeadingZeros",function(){return S}),r.d(e,"arrayFromInt32",function(){return b}),r.d(e,"arrayBufferConcat",function(){return A}),r.d(e,"sha256",function(){return E}),r.d(e,"isCryptoKey",function(){return _}),r.d(e,"Base64Url",function(){return P});var n=r(2),i=r(1);function o(t,e){for(var r=0;r0&&a.assert(!1,"convertRsaKey: Was expecting "+i.join()),void 0!==t.kty&&a.assert("RSA"===t.kty,"convertRsaKey: expecting rsaKey['kty'] to be 'RSA'"),n.kty="RSA";try{a.getSignConfig(t.alg),r=t.alg}catch(e){try{a.getCryptoConfig(t.alg),r=t.alg}catch(t){a.assert(r,"convertRsaKey: expecting rsaKey['alg'] to have a valid value")}}n.alg=r;for(var o=function(t){return parseInt(t,16)},u=0;u0,o=this.keyEncryption.id;return i.Jose.crypto.subtle.unwrapKey("raw",t,e,o,r.id,n,r.dec_op)}},{key:"getCekWorkaround",value:function(t){var e=t.specific_cekBytes;if(e){if(16===e)return{id:{name:"AES-CBC",length:128},enc_op:["encrypt"],dec_op:["decrypt"]};if(32===e)return{id:{name:"AES-CBC",length:256},enc_op:["encrypt"],dec_op:["decrypt"]};if(64===e)return{id:{name:"HMAC",hash:{name:"SHA-256"}},enc_op:["sign"],dec_op:["verify"]};if(128===e)return{id:{name:"HMAC",hash:{name:"SHA-384"}},enc_op:["sign"],dec_op:["verify"]};this.assert(!1,"getCekWorkaround: invalid len")}return{id:t.id,enc_op:["encrypt"],dec_op:["decrypt"]}}},{key:"encrypt",value:function(t,e,r,n){var o=this,a=this.content_encryption;if(t.length!==a.iv_bytes)return Promise.reject(Error("invalid IV length"));if(a.auth.aead){var u=a.auth.tagBytes,s={name:a.id.name,iv:t,additionalData:e,tagLength:8*u};return r.then(function(t){return i.Jose.crypto.subtle.encrypt(s,t,n).then(function(t){var e=t.byteLength-u;return{cipher:t.slice(0,e),tag:t.slice(e)}})})}var f=this.splitKey(a,r,["encrypt"]),c=f[0],h=f[1].then(function(e){var r={name:a.id.name,iv:t};return i.Jose.crypto.subtle.encrypt(r,e,n)}),y=h.then(function(r){return o.truncatedMac(a,c,e,t,r)});return Promise.all([h,y]).then(function(t){return{cipher:t[0],tag:t[1]}})}},{key:"compare",value:function(t,e,r,n){return this.assert(r instanceof Uint8Array,"compare: invalid input"),this.assert(n instanceof Uint8Array,"compare: invalid input"),e.then(function(e){var o=i.Jose.crypto.subtle.sign(t.auth.id,e,r),a=i.Jose.crypto.subtle.sign(t.auth.id,e,n);return Promise.all([o,a]).then(function(t){var e=new Uint8Array(t[0]),r=new Uint8Array(t[1]);if(e.length!==r.length)throw new Error("compare failed");for(var n=0;n0))throw new Error("No recipients defined. At least one is required to verify the JWS.");if(t.waiting_kid)throw new Error("still generating key IDs");return e.forEach(function(e){var a=e.protected.kid;i&&(r[a]=i(a)),o.push(t.cryptographer.verify(e.aad,t.payload,e.signature,r[a],a).then(function(e){return e.verified&&(e.payload=(new n.Base64Url).decode(t.payload)),e}))}),Promise.all(o)}}])&&o(e.prototype,r),a&&o(e,a),t}()},function(t,e,r){"use strict";(function(t){ 2 | /*! 3 | * The buffer module from node.js, for the browser. 4 | * 5 | * @author Feross Aboukhadijeh 6 | * @license MIT 7 | */ 8 | var n=r(9),i=r(10),o=r(11);function a(){return s.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function u(t,e){if(a()=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|t}function l(t,e){if(s.isBuffer(t))return t.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(t)||t instanceof ArrayBuffer))return t.byteLength;"string"!=typeof t&&(t=""+t);var r=t.length;if(0===r)return 0;for(var n=!1;;)switch(e){case"ascii":case"latin1":case"binary":return r;case"utf8":case"utf-8":case void 0:return N(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*r;case"hex":return r>>>1;case"base64":return L(t).length;default:if(n)return N(t).length;e=(""+e).toLowerCase(),n=!0}}function g(t,e,r){var n=!1;if((void 0===e||e<0)&&(e=0),e>this.length)return"";if((void 0===r||r>this.length)&&(r=this.length),r<=0)return"";if((r>>>=0)<=(e>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return B(this,e,r);case"utf8":case"utf-8":return C(this,e,r);case"ascii":return R(this,e,r);case"latin1":case"binary":return U(this,e,r);case"base64":return P(this,e,r);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return O(this,e,r);default:if(n)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),n=!0}}function d(t,e,r){var n=t[e];t[e]=t[r],t[r]=n}function v(t,e,r,n,i){if(0===t.length)return-1;if("string"==typeof r?(n=r,r=0):r>2147483647?r=2147483647:r<-2147483648&&(r=-2147483648),r=+r,isNaN(r)&&(r=i?0:t.length-1),r<0&&(r=t.length+r),r>=t.length){if(i)return-1;r=t.length-1}else if(r<0){if(!i)return-1;r=0}if("string"==typeof e&&(e=s.from(e,n)),s.isBuffer(e))return 0===e.length?-1:m(t,e,r,n,i);if("number"==typeof e)return e&=255,s.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(t,e,r):Uint8Array.prototype.lastIndexOf.call(t,e,r):m(t,[e],r,n,i);throw new TypeError("val must be string, number or Buffer")}function m(t,e,r,n,i){var o,a=1,u=t.length,s=e.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(t.length<2||e.length<2)return-1;a=2,u/=2,s/=2,r/=2}function f(t,e){return 1===a?t[e]:t.readUInt16BE(e*a)}if(i){var c=-1;for(o=r;ou&&(r=u-s),o=r;o>=0;o--){for(var h=!0,y=0;yi&&(n=i):n=i;var o=e.length;if(o%2!=0)throw new TypeError("Invalid hex string");n>o/2&&(n=o/2);for(var a=0;a>8,i=r%256,o.push(i),o.push(n);return o}(e,t.length-r),t,r,n)}function P(t,e,r){return 0===e&&r===t.length?n.fromByteArray(t):n.fromByteArray(t.slice(e,r))}function C(t,e,r){r=Math.min(t.length,r);for(var n=[],i=e;i239?4:f>223?3:f>191?2:1;if(i+h<=r)switch(h){case 1:f<128&&(c=f);break;case 2:128==(192&(o=t[i+1]))&&(s=(31&f)<<6|63&o)>127&&(c=s);break;case 3:o=t[i+1],a=t[i+2],128==(192&o)&&128==(192&a)&&(s=(15&f)<<12|(63&o)<<6|63&a)>2047&&(s<55296||s>57343)&&(c=s);break;case 4:o=t[i+1],a=t[i+2],u=t[i+3],128==(192&o)&&128==(192&a)&&128==(192&u)&&(s=(15&f)<<18|(63&o)<<12|(63&a)<<6|63&u)>65535&&s<1114112&&(c=s)}null===c?(c=65533,h=1):c>65535&&(c-=65536,n.push(c>>>10&1023|55296),c=56320|1023&c),n.push(c),i+=h}return function(t){var e=t.length;if(e<=k)return String.fromCharCode.apply(String,t);var r="",n=0;for(;n0&&(t=this.toString("hex",0,r).match(/.{2}/g).join(" "),this.length>r&&(t+=" ... ")),""},s.prototype.compare=function(t,e,r,n,i){if(!s.isBuffer(t))throw new TypeError("Argument must be a Buffer");if(void 0===e&&(e=0),void 0===r&&(r=t?t.length:0),void 0===n&&(n=0),void 0===i&&(i=this.length),e<0||r>t.length||n<0||i>this.length)throw new RangeError("out of range index");if(n>=i&&e>=r)return 0;if(n>=i)return-1;if(e>=r)return 1;if(this===t)return 0;for(var o=(i>>>=0)-(n>>>=0),a=(r>>>=0)-(e>>>=0),u=Math.min(o,a),f=this.slice(n,i),c=t.slice(e,r),h=0;hi)&&(r=i),t.length>0&&(r<0||e<0)||e>this.length)throw new RangeError("Attempt to write outside buffer bounds");n||(n="utf8");for(var o=!1;;)switch(n){case"hex":return w(this,t,e,r);case"utf8":case"utf-8":return S(this,t,e,r);case"ascii":return b(this,t,e,r);case"latin1":case"binary":return A(this,t,e,r);case"base64":return E(this,t,e,r);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return _(this,t,e,r);default:if(o)throw new TypeError("Unknown encoding: "+n);n=(""+n).toLowerCase(),o=!0}},s.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var k=4096;function R(t,e,r){var n="";r=Math.min(t.length,r);for(var i=e;in)&&(r=n);for(var i="",o=e;or)throw new RangeError("Trying to access beyond buffer length")}function K(t,e,r,n,i,o){if(!s.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(e>i||et.length)throw new RangeError("Index out of range")}function T(t,e,r,n){e<0&&(e=65535+e+1);for(var i=0,o=Math.min(t.length-r,2);i>>8*(n?i:1-i)}function I(t,e,r,n){e<0&&(e=4294967295+e+1);for(var i=0,o=Math.min(t.length-r,4);i>>8*(n?i:3-i)&255}function J(t,e,r,n,i,o){if(r+n>t.length)throw new RangeError("Index out of range");if(r<0)throw new RangeError("Index out of range")}function M(t,e,r,n,o){return o||J(t,0,r,4),i.write(t,e,r,n,23,4),r+4}function x(t,e,r,n,o){return o||J(t,0,r,8),i.write(t,e,r,n,52,8),r+8}s.prototype.slice=function(t,e){var r,n=this.length;if((t=~~t)<0?(t+=n)<0&&(t=0):t>n&&(t=n),(e=void 0===e?n:~~e)<0?(e+=n)<0&&(e=0):e>n&&(e=n),e0&&(i*=256);)n+=this[t+--e]*i;return n},s.prototype.readUInt8=function(t,e){return e||j(t,1,this.length),this[t]},s.prototype.readUInt16LE=function(t,e){return e||j(t,2,this.length),this[t]|this[t+1]<<8},s.prototype.readUInt16BE=function(t,e){return e||j(t,2,this.length),this[t]<<8|this[t+1]},s.prototype.readUInt32LE=function(t,e){return e||j(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},s.prototype.readUInt32BE=function(t,e){return e||j(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},s.prototype.readIntLE=function(t,e,r){t|=0,e|=0,r||j(t,e,this.length);for(var n=this[t],i=1,o=0;++o=(i*=128)&&(n-=Math.pow(2,8*e)),n},s.prototype.readIntBE=function(t,e,r){t|=0,e|=0,r||j(t,e,this.length);for(var n=e,i=1,o=this[t+--n];n>0&&(i*=256);)o+=this[t+--n]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*e)),o},s.prototype.readInt8=function(t,e){return e||j(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},s.prototype.readInt16LE=function(t,e){e||j(t,2,this.length);var r=this[t]|this[t+1]<<8;return 32768&r?4294901760|r:r},s.prototype.readInt16BE=function(t,e){e||j(t,2,this.length);var r=this[t+1]|this[t]<<8;return 32768&r?4294901760|r:r},s.prototype.readInt32LE=function(t,e){return e||j(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},s.prototype.readInt32BE=function(t,e){return e||j(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},s.prototype.readFloatLE=function(t,e){return e||j(t,4,this.length),i.read(this,t,!0,23,4)},s.prototype.readFloatBE=function(t,e){return e||j(t,4,this.length),i.read(this,t,!1,23,4)},s.prototype.readDoubleLE=function(t,e){return e||j(t,8,this.length),i.read(this,t,!0,52,8)},s.prototype.readDoubleBE=function(t,e){return e||j(t,8,this.length),i.read(this,t,!1,52,8)},s.prototype.writeUIntLE=function(t,e,r,n){(t=+t,e|=0,r|=0,n)||K(this,t,e,r,Math.pow(2,8*r)-1,0);var i=1,o=0;for(this[e]=255&t;++o=0&&(o*=256);)this[e+i]=t/o&255;return e+r},s.prototype.writeUInt8=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,1,255,0),s.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),this[e]=255&t,e+1},s.prototype.writeUInt16LE=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,2,65535,0),s.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):T(this,t,e,!0),e+2},s.prototype.writeUInt16BE=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,2,65535,0),s.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):T(this,t,e,!1),e+2},s.prototype.writeUInt32LE=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,4,4294967295,0),s.TYPED_ARRAY_SUPPORT?(this[e+3]=t>>>24,this[e+2]=t>>>16,this[e+1]=t>>>8,this[e]=255&t):I(this,t,e,!0),e+4},s.prototype.writeUInt32BE=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,4,4294967295,0),s.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):I(this,t,e,!1),e+4},s.prototype.writeIntLE=function(t,e,r,n){if(t=+t,e|=0,!n){var i=Math.pow(2,8*r-1);K(this,t,e,r,i-1,-i)}var o=0,a=1,u=0;for(this[e]=255&t;++o>0)-u&255;return e+r},s.prototype.writeIntBE=function(t,e,r,n){if(t=+t,e|=0,!n){var i=Math.pow(2,8*r-1);K(this,t,e,r,i-1,-i)}var o=r-1,a=1,u=0;for(this[e+o]=255&t;--o>=0&&(a*=256);)t<0&&0===u&&0!==this[e+o+1]&&(u=1),this[e+o]=(t/a>>0)-u&255;return e+r},s.prototype.writeInt8=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,1,127,-128),s.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),t<0&&(t=255+t+1),this[e]=255&t,e+1},s.prototype.writeInt16LE=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,2,32767,-32768),s.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):T(this,t,e,!0),e+2},s.prototype.writeInt16BE=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,2,32767,-32768),s.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):T(this,t,e,!1),e+2},s.prototype.writeInt32LE=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,4,2147483647,-2147483648),s.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8,this[e+2]=t>>>16,this[e+3]=t>>>24):I(this,t,e,!0),e+4},s.prototype.writeInt32BE=function(t,e,r){return t=+t,e|=0,r||K(this,t,e,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),s.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):I(this,t,e,!1),e+4},s.prototype.writeFloatLE=function(t,e,r){return M(this,t,e,!0,r)},s.prototype.writeFloatBE=function(t,e,r){return M(this,t,e,!1,r)},s.prototype.writeDoubleLE=function(t,e,r){return x(this,t,e,!0,r)},s.prototype.writeDoubleBE=function(t,e,r){return x(this,t,e,!1,r)},s.prototype.copy=function(t,e,r,n){if(r||(r=0),n||0===n||(n=this.length),e>=t.length&&(e=t.length),e||(e=0),n>0&&n=this.length)throw new RangeError("sourceStart out of bounds");if(n<0)throw new RangeError("sourceEnd out of bounds");n>this.length&&(n=this.length),t.length-e=0;--i)t[i+e]=this[i+r];else if(o<1e3||!s.TYPED_ARRAY_SUPPORT)for(i=0;i>>=0,r=void 0===r?this.length:r>>>0,t||(t=0),"number"==typeof t)for(o=e;o55295&&r<57344){if(!i){if(r>56319){(e-=3)>-1&&o.push(239,191,189);continue}if(a+1===n){(e-=3)>-1&&o.push(239,191,189);continue}i=r;continue}if(r<56320){(e-=3)>-1&&o.push(239,191,189),i=r;continue}r=65536+(i-55296<<10|r-56320)}else i&&(e-=3)>-1&&o.push(239,191,189);if(i=null,r<128){if((e-=1)<0)break;o.push(r)}else if(r<2048){if((e-=2)<0)break;o.push(r>>6|192,63&r|128)}else if(r<65536){if((e-=3)<0)break;o.push(r>>12|224,r>>6&63|128,63&r|128)}else{if(!(r<1114112))throw new Error("Invalid code point");if((e-=4)<0)break;o.push(r>>18|240,r>>12&63|128,r>>6&63|128,63&r|128)}}return o}function L(t){return n.toByteArray(function(t){if((t=function(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}(t).replace(Y,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function D(t,e,r,n){for(var i=0;i=e.length||i>=t.length);++i)e[i+r]=t[i];return i}}).call(this,r(3))},function(t,e,r){"use strict";e.byteLength=function(t){var e=f(t),r=e[0],n=e[1];return 3*(r+n)/4-n},e.toByteArray=function(t){for(var e,r=f(t),n=r[0],a=r[1],u=new o(function(t,e,r){return 3*(e+r)/4-r}(0,n,a)),s=0,c=a>0?n-4:n,h=0;h>16&255,u[s++]=e>>8&255,u[s++]=255&e;2===a&&(e=i[t.charCodeAt(h)]<<2|i[t.charCodeAt(h+1)]>>4,u[s++]=255&e);1===a&&(e=i[t.charCodeAt(h)]<<10|i[t.charCodeAt(h+1)]<<4|i[t.charCodeAt(h+2)]>>2,u[s++]=e>>8&255,u[s++]=255&e);return u},e.fromByteArray=function(t){for(var e,r=t.length,i=r%3,o=[],a=0,u=r-i;au?u:a+16383));1===i?(e=t[r-1],o.push(n[e>>2]+n[e<<4&63]+"==")):2===i&&(e=(t[r-2]<<8)+t[r-1],o.push(n[e>>10]+n[e>>4&63]+n[e<<2&63]+"="));return o.join("")};for(var n=[],i=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u=0,s=a.length;u0)throw new Error("Invalid string. Length must be a multiple of 4");var r=t.indexOf("=");return-1===r&&(r=e),[r,r===e?0:4-r%4]}function c(t,e,r){for(var i,o,a=[],u=e;u>18&63]+n[o>>12&63]+n[o>>6&63]+n[63&o]);return a.join("")}i["-".charCodeAt(0)]=62,i["_".charCodeAt(0)]=63},function(t,e){e.read=function(t,e,r,n,i){var o,a,u=8*i-n-1,s=(1<>1,c=-7,h=r?i-1:0,y=r?-1:1,p=t[e+h];for(h+=y,o=p&(1<<-c)-1,p>>=-c,c+=u;c>0;o=256*o+t[e+h],h+=y,c-=8);for(a=o&(1<<-c)-1,o>>=-c,c+=n;c>0;a=256*a+t[e+h],h+=y,c-=8);if(0===o)o=1-f;else{if(o===s)return a?NaN:1/0*(p?-1:1);a+=Math.pow(2,n),o-=f}return(p?-1:1)*a*Math.pow(2,o-n)},e.write=function(t,e,r,n,i,o){var a,u,s,f=8*o-i-1,c=(1<>1,y=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,p=n?0:o-1,l=n?1:-1,g=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(u=isNaN(e)?1:0,a=c):(a=Math.floor(Math.log(e)/Math.LN2),e*(s=Math.pow(2,-a))<1&&(a--,s*=2),(e+=a+h>=1?y/s:y*Math.pow(2,1-h))*s>=2&&(a++,s/=2),a+h>=c?(u=0,a=c):a+h>=1?(u=(e*s-1)*Math.pow(2,i),a+=h):(u=e*Math.pow(2,h-1)*Math.pow(2,i),a=0));i>=8;t[r+p]=255&u,p+=l,u/=256,i-=8);for(a=a<0;t[r+p]=255&a,p+=l,a/=256,f-=8);t[r+p-l]|=128*g}},function(t,e){var r={}.toString;t.exports=Array.isArray||function(t){return"[object Array]"==r.call(t)}}])}); -------------------------------------------------------------------------------- /examples/jose-jwe-example1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

plain text: hello world

4 |

cipher text:

5 |

error:

6 | 7 | 8 | 58 | 59 | -------------------------------------------------------------------------------- /examples/jose-jwe-example2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

plain text: hello world, ⛄

4 |

cipher text:

5 |

error:

6 | 7 | 8 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/jose-jws-ecdsa-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

payload: hello world, ⛄

4 | 5 |

signature: 6 |


 7 | 

8 |

error:

9 | 10 | 11 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/jose-jws-rsa-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

payload: hello world, ⛄

4 | 5 |

signature: 6 |


 7 | 

8 |

error:

9 | 10 | 11 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /jose-jwe-jws.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for jose-jwe-jws 2 | // Project: jose-jwe-jws 3 | // Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> 4 | 5 | interface IEncryptionResult { 6 | cipher: ArrayBuffer; 7 | tag: ArrayBuffer; 8 | } 9 | 10 | interface IVerificationResult { 11 | kid: string; 12 | verified: boolean; 13 | payload?: string; 14 | } 15 | 16 | type RsaSsaSignAlgorithm = "RS256" | "RS384" | "RS512"; 17 | type RsaPssSignAlgorithm = "PS256" | "PS384" | "PS512"; 18 | type HmacSignAlgorithm = "HS256" | "HS384" | "HS512"; 19 | type EcdsaSignAlgorithm = "ES256" | "ES384" | "ES512"; 20 | type SignAlgorithm = RsaSsaSignAlgorithm | RsaPssSignAlgorithm | HmacSignAlgorithm | EcdsaSignAlgorithm; 21 | 22 | type RsaOaepEncryptAlgorithm = "RSA-OAEP" | "RSA-OAEP-256"; 23 | type AesKwEncryptAlgorithm = "A128KW" | "A256KW"; 24 | type AesCbcEncryptAlgorithm = "A128CBC-HS256" | "A256CBC-HS512"; 25 | type AesGcmEncryptAlgorithm = "A128GCM" | "A256GCM"; 26 | type EncryptAlgorithm = RsaOaepEncryptAlgorithm | AesKwEncryptAlgorithm | AesCbcEncryptAlgorithm | AesGcmEncryptAlgorithm; 27 | 28 | type JoseAlgorithm = EncryptAlgorithm | SignAlgorithm; 29 | 30 | interface IWebCryptographer { 31 | new(): IWebCryptographer; 32 | getContentEncryptionAlgorithm(): EncryptAlgorithm; 33 | setContentEncryptionAlgorithm(algorithm: EncryptAlgorithm): void; 34 | getKeyEncryptionAlgorithm(): EncryptAlgorithm; 35 | setKeyEncryptionAlgorithm(algorithm: EncryptAlgorithm): void; 36 | getContentSignAlgorithm(): SignAlgorithm; 37 | setContentSignAlgorithm(algorithm: SignAlgorithm): void; 38 | createIV(): ArrayBufferView; 39 | createCek(): PromiseLike; 40 | wrapCek(): PromiseLike; 41 | unwrapCek(): PromiseLike; 42 | encrypt(iv: Uint8Array, aad: Uint8Array, cek: CryptoKey | PromiseLike, plaintext: Uint8Array): PromiseLike; 43 | decrypt(cek: CryptoKey | PromiseLike, aad: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array, tag: Uint8Array): PromiseLike; 44 | sign(aad: Object, payload: string | Object, key: CryptoKey | PromiseLike): PromiseLike; 45 | verify(aad: string, payload: string, signature: Uint8Array, key: CryptoKey | PromiseLike, kid: string): PromiseLike; 46 | } 47 | 48 | interface IJsonWebKey { 49 | kty: string; 50 | alg?: string; 51 | kid?: string; 52 | use?: string; 53 | key_ops?: string[]; 54 | x5u?: string; 55 | x5c?: string[]; 56 | x5t?: string; 57 | "x5t#S256"?: string; 58 | } 59 | 60 | interface JWKRSA extends IJsonWebKey { 61 | n?: string; 62 | e?: string; 63 | d?: string; 64 | p?: string; 65 | q?: string; 66 | dp?: string; 67 | dq?: string; 68 | qi?: string; 69 | oth?: { 70 | r: string; 71 | d: string; 72 | t: string; 73 | }[]; 74 | } 75 | 76 | interface IJwkSet { 77 | keys: IJsonWebKey[]; 78 | } 79 | 80 | interface IHeaders { 81 | alg: string; 82 | jku?: string; 83 | jwk?: IJsonWebKey; 84 | kid?: string; 85 | x5u?: string; 86 | x5c?: string[]; 87 | x5t?: string; 88 | 'x5t#S256'?: string; 89 | typ?: string; 90 | cty?: string; 91 | crit?: string[]; 92 | } 93 | 94 | interface IUtils { 95 | importRsaPublicKey(rsa_key: JWKRSA, alg: JoseAlgorithm): PromiseLike; 96 | importRsaPrivateKey(rsa_key: JWKRSA, alg: JoseAlgorithm): PromiseLike; 97 | importPublicKey(key: IJsonWebKey, alg: JoseAlgorithm): PromiseLike; 98 | } 99 | 100 | interface IJose { 101 | Utils: IUtils; 102 | JoseJWE: IJoseJWE; 103 | JoseJWS: IJoseJWS 104 | caniuse(): boolean; 105 | WebCryptographer: IWebCryptographer; 106 | } 107 | 108 | interface ISignedJws { 109 | header: string; 110 | protected: string; 111 | payload: string; 112 | signature: string; 113 | JsonSerialize(): Object; 114 | CompactSerialize(): string; 115 | } 116 | 117 | interface ISigner { 118 | new(cryptographer: IWebCryptographer): ISigner; 119 | addSigner(pk: CryptoKey, kid: string, aad?: Object, header?: Object): PromiseLike; 120 | sign(payload: any, aad?: Object, header?: Object): PromiseLike; 121 | } 122 | 123 | interface IVerifier { 124 | new(cryptographer: IWebCryptographer, msg: string, 125 | keyfinder?: (kid: string) => PromiseLike): IVerifier; 126 | addRecipient(pk: CryptoKey | string | JWKRSA, kid?: string, alg?: SignAlgorithm): PromiseLike; 127 | verify(): PromiseLike; 128 | } 129 | 130 | interface IJoseJWS { 131 | Signer: ISigner; 132 | Verifier: IVerifier; 133 | } 134 | 135 | interface IEncrypter { 136 | new(cryptographer: IWebCryptographer, pubkey: CryptoKey | PromiseLike): IEncrypter; 137 | addHeader(k: string, v: string): void; 138 | encrypt(plaintext: string): PromiseLike; 139 | } 140 | 141 | interface IDecrypter { 142 | new(cryptographer: IWebCryptographer, key: CryptoKey | PromiseLike): IDecrypter; 143 | decrypt(ciphertext: string): PromiseLike; 144 | } 145 | 146 | interface IJoseJWE { 147 | Encrypter: IEncrypter; 148 | Decrypter: IDecrypter; 149 | } 150 | 151 | interface Window { 152 | Jose: IJose; 153 | } 154 | 155 | declare function setCrypto(cryptoProvider: Crypto): void; 156 | declare const Jose: IJose; 157 | declare var crypto: Crypto; 158 | 159 | declare module "jose-jwe-jws" { 160 | export function setCrypto(cryptoProvider: Crypto): void; 161 | export const Jose: IJose; 162 | export var crypto: Crypto; 163 | } 164 | -------------------------------------------------------------------------------- /lib/jose-core.js: -------------------------------------------------------------------------------- 1 | /* - 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as JoseUtils from './jose-utils'; 17 | import { Encrypter } from './jose-jwe-encrypt'; 18 | import { Decrypter } from './jose-jwe-decrypt'; 19 | import { Signer } from './jose-jws-sign'; 20 | import { Verifier } from './jose-jws-verify'; 21 | import { WebCryptographer } from './jose-jwe-webcryptographer'; 22 | 23 | export var crypto; 24 | /** 25 | * Javascript Object Signing and Encryption library. 26 | * 27 | * @author Alok Menghrajani 28 | */ 29 | 30 | export var Utils = JoseUtils; 31 | 32 | /** 33 | * Initializes a JoseJWE object. 34 | */ 35 | const JoseJWE = { 36 | Encrypter, 37 | Decrypter 38 | }; 39 | 40 | /** 41 | * Initializes a JoseJWS object. 42 | */ 43 | const JoseJWS = { 44 | Signer, 45 | Verifier 46 | }; 47 | 48 | /** 49 | * Set crypto provider to use (window.crypto, node-webcrypto-ossl, node-webcrypto-pkcs11 etc.). 50 | */ 51 | export var setCrypto = function (cp) { 52 | crypto = cp; 53 | }; 54 | 55 | /** 56 | * Default to the global "crypto" variable 57 | */ 58 | if (typeof window !== 'undefined') { 59 | if (typeof window.crypto !== 'undefined') { 60 | setCrypto(window.crypto); 61 | if (!crypto.subtle) { 62 | crypto.subtle = crypto.webkitSubtle; 63 | } 64 | } 65 | } 66 | 67 | const Jose = { JoseJWS, JoseJWE, WebCryptographer, crypto, Utils }; 68 | 69 | export default { Jose, WebCryptographer }; 70 | export { Jose, JoseJWE, JoseJWS, WebCryptographer }; 71 | 72 | /** 73 | * Use Node versions of atob, btoa functions outside the browser 74 | */ 75 | if (typeof atob !== 'function') { 76 | // eslint-disable-next-line no-global-assign 77 | atob = (str) => { 78 | return Buffer.from(str, 'base64').toString('binary'); 79 | }; 80 | } 81 | 82 | if (typeof btoa !== 'function') { 83 | // eslint-disable-next-line no-global-assign 84 | btoa = (str) => { 85 | var buffer; 86 | if (str instanceof Buffer) { 87 | buffer = str; 88 | } else { 89 | buffer = Buffer.from(str.toString(), 'binary'); 90 | } 91 | return buffer.toString('base64'); 92 | }; 93 | } 94 | 95 | /** 96 | * Checks if we have all the required APIs. 97 | * 98 | * It might make sense to take a Cryptographer and delegate some of the checks 99 | * to the cryptographer. I however wanted to keep things simple, so I put all 100 | * the checks here for now. 101 | * 102 | * This list is generated manually and needs to be kept up-to-date. 103 | * 104 | * Casual testing shows that: 105 | * - things work in Chrome 40.0.2214.115 106 | * - things work in Firefox 35.0.1 107 | * - Safari 7.1.3 doesn't support JWK keys. 108 | * - Internet Explorer doesn't support Promises. 109 | * 110 | * Note: We don't check if the browser supports specific crypto operations. 111 | * I.e. it's possible for this function to return true, but encryption or 112 | * decryption to subsequently fail because the browser does not support a 113 | * given encryption, decryption, key wrapping, key unwrapping or hmac 114 | * operation. 115 | * 116 | * @return bool 117 | */ 118 | export const caniuse = () => { 119 | var r = true; 120 | 121 | // Promises/A+ (https://promisesaplus.com/) 122 | r = r && (typeof Promise === 'function'); 123 | r = r && (typeof Promise.reject === 'function'); 124 | r = r && (typeof Promise.prototype.then === 'function'); 125 | r = r && (typeof Promise.all === 'function'); 126 | 127 | const globalObject = window || global; 128 | 129 | // Crypto (http://www.w3.org/TR/WebCryptoAPI/) 130 | r = r && (typeof globalObject.crypto === 'object'); 131 | r = r && (typeof globalObject.crypto.subtle === 'object'); 132 | r = r && (typeof globalObject.crypto.getRandomValues === 'function'); 133 | r = r && (typeof globalObject.crypto.subtle.importKey === 'function'); 134 | r = r && (typeof globalObject.crypto.subtle.generateKey === 'function'); 135 | r = r && (typeof globalObject.crypto.subtle.exportKey === 'function'); 136 | r = r && (typeof globalObject.crypto.subtle.wrapKey === 'function'); 137 | r = r && (typeof globalObject.crypto.subtle.unwrapKey === 'function'); 138 | r = r && (typeof globalObject.crypto.subtle.encrypt === 'function'); 139 | r = r && (typeof globalObject.crypto.subtle.decrypt === 'function'); 140 | r = r && (typeof globalObject.crypto.subtle.sign === 'function'); 141 | 142 | // ArrayBuffer (http://people.mozilla.org/~jorendorff/es6-draft.html#sec-arraybuffer-constructor) 143 | r = r && (typeof ArrayBuffer === 'function'); 144 | r = r && (typeof Uint8Array === 'function' || typeof Uint8Array === 'object'); // Safari uses "object" 145 | r = r && (typeof Uint32Array === 'function' || typeof Uint32Array === 'object'); // Safari uses "object" 146 | // skipping Uint32Array.prototype.buffer because https://people.mozilla.org/~jorendorff/es6-draft.html#sec-properties-of-the-%typedarrayprototype%-object 147 | 148 | // JSON (http://www.ecma-international.org/ecma-262/5.1/#sec-15.12.3) 149 | r = r && (typeof JSON === 'object'); 150 | r = r && (typeof JSON.parse === 'function'); 151 | r = r && (typeof JSON.stringify === 'function'); 152 | 153 | // Base64 (http://www.w3.org/TR/html5/webappapis.html#dom-windowbase64-atob) 154 | r = r && (typeof atob === 'function'); 155 | r = r && (typeof btoa === 'function'); 156 | 157 | // skipping Array functions (map, join, push, length, etc.) 158 | // skipping String functions (split, charCodeAt, fromCharCode, replace, etc.) 159 | // skipping regexp.test and parseInt 160 | 161 | return r; 162 | }; 163 | -------------------------------------------------------------------------------- /lib/jose-core.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-duplicates 2 | import DefaultJose, { Jose } from './jose-core.js'; 3 | // eslint-disable-next-line import/no-duplicates 4 | import * as AsJose from './jose-core.js'; 5 | 6 | it('ensure imports work', () => { 7 | expect(Jose).not.toBeUndefined(); 8 | expect(Jose.JoseJWS).not.toBeUndefined(); 9 | expect(Jose.JoseJWE).not.toBeUndefined(); 10 | expect(Jose.WebCryptographer).not.toBeUndefined(); 11 | 12 | console.log(Jose); 13 | expect(DefaultJose).not.toBeUndefined(); 14 | expect(DefaultJose.WebCryptographer).not.toBeUndefined(); 15 | expect(DefaultJose.Jose).not.toBeUndefined(); 16 | expect(DefaultJose.Jose.JoseJWS).not.toBeUndefined(); 17 | expect(DefaultJose.Jose.JoseJWE).not.toBeUndefined(); 18 | expect(DefaultJose.Jose.WebCryptographer).not.toBeUndefined(); 19 | 20 | expect(AsJose).not.toBeUndefined(); 21 | expect(AsJose.WebCryptographer).not.toBeUndefined(); 22 | expect(AsJose.Jose).not.toBeUndefined(); 23 | expect(AsJose.Jose.JoseJWS).not.toBeUndefined(); 24 | expect(AsJose.Jose.JoseJWE).not.toBeUndefined(); 25 | expect(AsJose.Jose.WebCryptographer).not.toBeUndefined(); 26 | }); 27 | -------------------------------------------------------------------------------- /lib/jose-jwe-decrypt.js: -------------------------------------------------------------------------------- 1 | /* - 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as Utils from './jose-utils'; 18 | 19 | /** 20 | * Handles decryption. 21 | * 22 | * @param cryptographer an instance of WebCryptographer (or equivalent). Keep 23 | * in mind that decryption mutates the cryptographer. 24 | * @param keyPromise Promise, either RSA or shared key 25 | */ 26 | export class Decrypter { 27 | constructor (cryptographer, keyPromise) { 28 | this.cryptographer = cryptographer; 29 | this.keyPromise = keyPromise; 30 | this.headers = {}; 31 | this.base64UrlEncoder = new Utils.Base64Url(); 32 | } 33 | 34 | getHeaders () { 35 | return this.headers; 36 | } 37 | 38 | /** 39 | * Performs decryption. 40 | * 41 | * @param cipherText String 42 | * @return Promise 43 | */ 44 | decrypt (cipherText) { 45 | // Split cipherText in 5 parts 46 | var parts = cipherText.split('.'); 47 | if (parts.length !== 5) { 48 | return Promise.reject(Error('decrypt: invalid input')); 49 | } 50 | 51 | // part 1: header 52 | this.headers = JSON.parse(this.base64UrlEncoder.decode(parts[0])); 53 | if (!this.headers.alg) { 54 | return Promise.reject(Error('decrypt: missing alg')); 55 | } 56 | if (!this.headers.enc) { 57 | return Promise.reject(Error('decrypt: missing enc')); 58 | } 59 | this.cryptographer.setKeyEncryptionAlgorithm(this.headers.alg); 60 | this.cryptographer.setContentEncryptionAlgorithm(this.headers.enc); 61 | 62 | if (this.headers.crit) { 63 | // We don't support the crit header 64 | return Promise.reject(Error('decrypt: crit is not supported')); 65 | } 66 | 67 | var cekPromise; 68 | 69 | if (this.headers.alg === 'dir') { 70 | // with direct mode, we already have the cek 71 | cekPromise = Promise.resolve(this.keyPromise); 72 | } else { 73 | // part 2: decrypt the CEK 74 | // In some modes (e.g. RSA-PKCS1v1.5), you must take precautions to prevent 75 | // chosen-ciphertext attacks as described in RFC 3218, "Preventing 76 | // the Million Message Attack on Cryptographic Message Syntax". We currently 77 | // only support RSA-OAEP, so we don't generate a key if unwrapping fails. 78 | var encryptedCek = this.base64UrlEncoder.decodeArray(parts[1]); 79 | cekPromise = this.keyPromise.then(function (key) { 80 | return this.cryptographer.unwrapCek(encryptedCek, key); 81 | }.bind(this)); 82 | } 83 | 84 | // part 3: decrypt the cipher text 85 | var plainTextPromise = this.cryptographer.decrypt( 86 | cekPromise, 87 | Utils.arrayFromString(parts[0]), 88 | this.base64UrlEncoder.decodeArray(parts[2]), 89 | this.base64UrlEncoder.decodeArray(parts[3]), 90 | this.base64UrlEncoder.decodeArray(parts[4])); 91 | 92 | return plainTextPromise.then(Utils.utf8StringFromArray); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/jose-jwe-encrypt.js: -------------------------------------------------------------------------------- 1 | /* - 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as Utils from './jose-utils'; 17 | 18 | /** 19 | * Handles encryption. 20 | * 21 | * @param cryptographer an instance of WebCryptographer (or equivalent). 22 | * @param keyPromise Promise, either RSA or shared key 23 | */ 24 | export class Encrypter { 25 | constructor (cryptographer, keyPromise) { 26 | this.cryptographer = cryptographer; 27 | this.keyPromise = keyPromise; 28 | this.userHeaders = {}; 29 | } 30 | 31 | /** 32 | * Adds a key/value pair which will be included in the header. 33 | * 34 | * The data lives in plaintext (an attacker can read the header) but is tamper 35 | * proof (an attacker cannot modify the header). 36 | * 37 | * Note: some headers have semantic implications. E.g. if you set the "zip" 38 | * header, you are responsible for properly compressing plainText before 39 | * calling encrypt(). 40 | * 41 | * @param k String 42 | * @param v String 43 | */ 44 | addHeader (k, v) { 45 | this.userHeaders[k] = v; 46 | } 47 | 48 | /** 49 | * Performs encryption. 50 | * 51 | * @param plainText utf-8 string 52 | * @return Promise 53 | */ 54 | encrypt (plainText) { 55 | /** 56 | * Encrypts plainText with CEK. 57 | * 58 | * @param cekPromise Promise 59 | * @param plainText string 60 | * @return Promise 61 | */ 62 | var encryptPlainText = function (cekPromise, plainText) { 63 | // Create header 64 | var headers = {}; 65 | for (var i in this.userHeaders) { 66 | headers[i] = this.userHeaders[i]; 67 | } 68 | headers.alg = this.cryptographer.getKeyEncryptionAlgorithm(); 69 | headers.enc = this.cryptographer.getContentEncryptionAlgorithm(); 70 | var jweProtectedHeader = new Utils.Base64Url().encode(JSON.stringify(headers)); 71 | 72 | // Create the IV 73 | var iv = this.cryptographer.createIV(); 74 | 75 | // Create the AAD 76 | var aad = Utils.arrayFromString(jweProtectedHeader); 77 | plainText = Utils.arrayFromUtf8String(plainText); 78 | 79 | return this.cryptographer.encrypt(iv, aad, cekPromise, plainText).then(function (r) { 80 | r.header = jweProtectedHeader; 81 | r.iv = iv; 82 | return r; 83 | }); 84 | }; 85 | 86 | var cekPromise, encryptedCek; 87 | 88 | if (this.cryptographer.getKeyEncryptionAlgorithm() === 'dir') { 89 | // with direct encryption, this.keyPromise provides the cek 90 | // and encryptedCek is empty 91 | cekPromise = Promise.resolve(this.keyPromise); 92 | encryptedCek = []; 93 | } else { 94 | // Create a CEK key 95 | cekPromise = this.cryptographer.createCek(); 96 | 97 | // Key & Cek allows us to create the encryptedCek 98 | encryptedCek = Promise.all([this.keyPromise, cekPromise]).then(function (all) { 99 | var key = all[0]; 100 | var cek = all[1]; 101 | return this.cryptographer.wrapCek(cek, key); 102 | }.bind(this)); 103 | } 104 | 105 | // Cek allows us to encrypy the plain text 106 | var encPromise = encryptPlainText.bind(this, cekPromise, plainText)(); 107 | 108 | // Once we have all the promises, we can base64 encode all the pieces. 109 | return Promise.all([encryptedCek, encPromise]).then(function (all) { 110 | var encryptedCek = all[0]; 111 | var data = all[1]; 112 | var base64UrlEncoder = new Utils.Base64Url(); 113 | return data.header + '.' + 114 | base64UrlEncoder.encodeArray(encryptedCek) + '.' + 115 | base64UrlEncoder.encodeArray(data.iv) + '.' + 116 | base64UrlEncoder.encodeArray(data.cipher) + '.' + 117 | base64UrlEncoder.encodeArray(data.tag); 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/jose-jwe-webcryptographer.js: -------------------------------------------------------------------------------- 1 | /* - 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // TODO(eslint): figure out how to properly include Jose or expose crypto object 18 | 19 | import * as Utils from './jose-utils'; 20 | import { Jose } from './jose-core'; 21 | 22 | /** 23 | * The WebCryptographer uses http://www.w3.org/TR/WebCryptoAPI/ to perform 24 | * various crypto operations. In theory, this should help build the library with 25 | * different underlying crypto APIs. I'm however unclear if we'll run into code 26 | * duplication or callback vs Promise based API issues. 27 | */ 28 | export class WebCryptographer { 29 | constructor () { 30 | this.setKeyEncryptionAlgorithm('RSA-OAEP'); 31 | this.setContentEncryptionAlgorithm('A256GCM'); 32 | this.setContentSignAlgorithm('RS256'); 33 | } 34 | 35 | /** 36 | * Overrides the default key encryption algorithm 37 | * @param alg string 38 | */ 39 | setKeyEncryptionAlgorithm (alg) { 40 | this.keyEncryption = this.getCryptoConfig(alg); 41 | } 42 | 43 | getKeyEncryptionAlgorithm () { 44 | return this.keyEncryption.jweName; 45 | } 46 | 47 | /** 48 | * Overrides the default content encryption algorithm 49 | * @param alg string 50 | */ 51 | setContentEncryptionAlgorithm (alg) { 52 | this.content_encryption = this.getCryptoConfig(alg); 53 | } 54 | 55 | getContentEncryptionAlgorithm () { 56 | return this.content_encryption.jweName; 57 | } 58 | 59 | /** 60 | * Overrides the default content sign algorithm 61 | * @param alg string 62 | */ 63 | setContentSignAlgorithm (alg) { 64 | this.content_sign = this.getSignConfig(alg); 65 | } 66 | 67 | getContentSignAlgorithm () { 68 | return this.content_sign.jwa_name; 69 | } 70 | 71 | /** 72 | * Generates an IV. 73 | * This function mainly exists so that it can be mocked for testing purpose. 74 | * 75 | * @return Uint8Array with random bytes 76 | */ 77 | createIV () { 78 | var iv = new Uint8Array(new Array(this.content_encryption.iv_bytes)); 79 | return Jose.crypto.getRandomValues(iv); 80 | } 81 | 82 | /** 83 | * Creates a random content encryption key. 84 | * This function mainly exists so that it can be mocked for testing purpose. 85 | * 86 | * @return Promise 87 | */ 88 | createCek () { 89 | var hack = this.getCekWorkaround(this.content_encryption); 90 | return Jose.crypto.subtle.generateKey(hack.id, true, hack.enc_op); 91 | } 92 | 93 | wrapCek (cek, key) { 94 | return Jose.crypto.subtle.wrapKey('raw', cek, key, this.keyEncryption.id); 95 | } 96 | 97 | unwrapCek (cek, key) { 98 | var hack = this.getCekWorkaround(this.content_encryption); 99 | var extractable = (this.content_encryption.specific_cekBytes > 0); 100 | var keyEncryption = this.keyEncryption.id; 101 | 102 | return Jose.crypto.subtle.unwrapKey('raw', cek, key, keyEncryption, hack.id, extractable, hack.dec_op); 103 | } 104 | 105 | /** 106 | * Returns algorithm and operation needed to create a CEK. 107 | * 108 | * In some cases, e.g. A128CBC-HS256, the CEK gets split into two keys. The Web 109 | * Crypto API does not allow us to generate an arbitrary number of bytes and 110 | * then create a CryptoKey without any associated algorithm. We therefore piggy 111 | * back on AES-CBS and HMAC which allows the creation of CEKs of size 16, 32, 64 112 | * and 128 bytes. 113 | */ 114 | getCekWorkaround (alg) { 115 | var len = alg.specific_cekBytes; 116 | if (len) { 117 | if (len === 16) { 118 | return { id: { name: 'AES-CBC', length: 128 }, enc_op: ['encrypt'], dec_op: ['decrypt'] }; 119 | } else if (len === 32) { 120 | return { id: { name: 'AES-CBC', length: 256 }, enc_op: ['encrypt'], dec_op: ['decrypt'] }; 121 | } else if (len === 64) { 122 | return { id: { name: 'HMAC', hash: { name: 'SHA-256' } }, enc_op: ['sign'], dec_op: ['verify'] }; 123 | } else if (len === 128) { 124 | return { id: { name: 'HMAC', hash: { name: 'SHA-384' } }, enc_op: ['sign'], dec_op: ['verify'] }; 125 | } else { 126 | this.assert(false, 'getCekWorkaround: invalid len'); 127 | } 128 | } 129 | return { id: alg.id, enc_op: ['encrypt'], dec_op: ['decrypt'] }; 130 | } 131 | 132 | /** 133 | * Encrypts plainText with cek. 134 | * 135 | * @param iv Uint8Array 136 | * @param aad Uint8Array 137 | * @param cekPromise Promise 138 | * @param plainText Uint8Array 139 | * @return Promise 140 | */ 141 | encrypt (iv, aad, cekPromise, plainText) { 142 | var config = this.content_encryption; 143 | if (iv.length !== config.iv_bytes) { 144 | return Promise.reject(Error('invalid IV length')); 145 | } 146 | if (config.auth.aead) { 147 | var tagBytes = config.auth.tagBytes; 148 | 149 | var enc = { 150 | name: config.id.name, 151 | iv: iv, 152 | additionalData: aad, 153 | tagLength: tagBytes * 8 154 | }; 155 | 156 | return cekPromise.then(function (cek) { 157 | return Jose.crypto.subtle.encrypt(enc, cek, plainText).then(function (cipherText) { 158 | var offset = cipherText.byteLength - tagBytes; 159 | return { 160 | cipher: cipherText.slice(0, offset), 161 | tag: cipherText.slice(offset) 162 | }; 163 | }); 164 | }); 165 | } else { 166 | var keys = this.splitKey(config, cekPromise, ['encrypt']); 167 | var macKeyPromise = keys[0]; 168 | var encKeyPromise = keys[1]; 169 | 170 | // Encrypt the plain text 171 | var cipherTextPromise = encKeyPromise.then(function (encKey) { 172 | var enc = { 173 | name: config.id.name, 174 | iv: iv 175 | }; 176 | return Jose.crypto.subtle.encrypt(enc, encKey, plainText); 177 | }); 178 | 179 | // compute MAC 180 | var macPromise = cipherTextPromise.then((cipherText) => { 181 | return this.truncatedMac( 182 | config, 183 | macKeyPromise, 184 | aad, 185 | iv, 186 | cipherText); 187 | }); 188 | 189 | return Promise.all([cipherTextPromise, macPromise]).then(function (all) { 190 | var cipherText = all[0]; 191 | var mac = all[1]; 192 | return { 193 | cipher: cipherText, 194 | tag: mac 195 | }; 196 | }); 197 | } 198 | } 199 | 200 | /** 201 | * Compares two Uint8Arrays in constant time. 202 | * 203 | * @return Promise 204 | */ 205 | compare (config, macKeyPromise, arr1, arr2) { 206 | this.assert(arr1 instanceof Uint8Array, 'compare: invalid input'); 207 | this.assert(arr2 instanceof Uint8Array, 'compare: invalid input'); 208 | 209 | return macKeyPromise.then(function (macKey) { 210 | var hash1 = Jose.crypto.subtle.sign(config.auth.id, macKey, arr1); 211 | var hash2 = Jose.crypto.subtle.sign(config.auth.id, macKey, arr2); 212 | return Promise.all([hash1, hash2]).then(function (all) { 213 | var hash1 = new Uint8Array(all[0]); 214 | var hash2 = new Uint8Array(all[1]); 215 | if (hash1.length !== hash2.length) { 216 | throw new Error('compare failed'); 217 | } 218 | for (var i = 0; i < hash1.length; i++) { 219 | if (hash1[i] !== hash2[i]) { 220 | throw new Error('compare failed'); 221 | } 222 | } 223 | return Promise.resolve(null); 224 | }); 225 | }); 226 | } 227 | 228 | /** 229 | * Decrypts cipherText with cek. Validates the tag. 230 | * 231 | * @param cekPromise Promise 232 | * @param aad protected header 233 | * @param iv IV 234 | * @param cipherText text to be decrypted 235 | * @param tag to be verified 236 | * @return Promise 237 | */ 238 | decrypt (cekPromise, aad, iv, cipherText, tag) { 239 | if (iv.length !== this.content_encryption.iv_bytes) { 240 | return Promise.reject(Error('decryptCiphertext: invalid IV')); 241 | } 242 | 243 | var config = this.content_encryption; 244 | if (config.auth.aead) { 245 | var dec = { 246 | name: config.id.name, 247 | iv: iv, 248 | additionalData: aad, 249 | tagLength: config.auth.tagBytes * 8 250 | }; 251 | 252 | return cekPromise.then(function (cek) { 253 | var buf = Utils.arrayBufferConcat(cipherText, tag); 254 | return Jose.crypto.subtle.decrypt(dec, cek, buf); 255 | }); 256 | } else { 257 | var keys = this.splitKey(config, cekPromise, ['decrypt']); 258 | var macKeyPromise = keys[0]; 259 | var encKeyPromise = keys[1]; 260 | 261 | // Validate the MAC 262 | var macPromise = this.truncatedMac( 263 | config, 264 | macKeyPromise, 265 | aad, 266 | iv, 267 | cipherText); 268 | 269 | return Promise.all([encKeyPromise, macPromise]).then((all) => { 270 | var encKey = all[0]; 271 | var mac = all[1]; 272 | 273 | return this.compare(config, macKeyPromise, new Uint8Array(mac), tag).then(() => { 274 | var dec = { 275 | name: config.id.name, 276 | iv: iv 277 | }; 278 | return Jose.crypto.subtle.decrypt(dec, encKey, cipherText); 279 | }).catch(() => { 280 | return Promise.reject(Error('decryptCiphertext: MAC failed.')); 281 | }); 282 | }); 283 | } 284 | } 285 | 286 | /** 287 | * Signs plainText. 288 | * 289 | * @param aad json 290 | * @param payload String or json 291 | * @param keyPromise Promise 292 | * @return Promise 293 | */ 294 | sign (aad, payload, keyPromise) { 295 | var config = this.content_sign; 296 | 297 | if (aad.alg) { 298 | config = this.getSignConfig(aad.alg); 299 | } 300 | 301 | // Encrypt the plain text 302 | return keyPromise.then(function (key) { 303 | var base64UrlEncoder = new Utils.Base64Url(); 304 | return Jose.crypto.subtle.sign(config.id, key, Utils.arrayFromString(base64UrlEncoder.encode(JSON.stringify(aad)) + '.' + base64UrlEncoder.encodeArray(payload))); 305 | }); 306 | } 307 | 308 | /** 309 | * Verify JWS. 310 | * 311 | * @param payload Base64Url encoded payload 312 | * @param aad String Base64Url encoded JSON representation of the protected JWS header 313 | * @param signature Uint8Array containing the signature 314 | * @param keyPromise Promise 315 | * @param keyId value of the kid JoseHeader, it'll be passed as part of the result to the returned promise 316 | * @return Promise 317 | */ 318 | verify (aad, payload, signature, keyPromise, keyId) { 319 | var config = this.content_sign; 320 | 321 | return keyPromise.then(function (key) { 322 | return Jose.crypto.subtle.verify(config.id, key, signature, Utils.arrayFromString(aad + '.' + payload)).then(function (res) { 323 | return { kid: keyId, verified: res }; 324 | }); 325 | }); 326 | } 327 | 328 | keyId (rsaKey) { 329 | return Utils.sha256(rsaKey.n + '+' + rsaKey.d); 330 | } 331 | 332 | /** 333 | * Splits a CEK into two pieces: a MAC key and an ENC key. 334 | * 335 | * This code is structured around the fact that the crypto API does not provide 336 | * a way to validate truncated MACs. The MAC key is therefore always imported to 337 | * sign data. 338 | * 339 | * @param config (used for key lengths & algorithms) 340 | * @param cekPromise Promise CEK key to split 341 | * @param purpose Array usages of the imported key 342 | * @return [Promise, Promise] 343 | */ 344 | splitKey (config, cekPromise, purpose) { 345 | // We need to split the CEK key into a MAC and ENC keys 346 | var cekBytesPromise = cekPromise.then(function (cek) { 347 | return Jose.crypto.subtle.exportKey('raw', cek); 348 | }); 349 | var macKeyPromise = cekBytesPromise.then(function (cekBytes) { 350 | if (cekBytes.byteLength * 8 !== config.id.length + config.auth.key_bytes * 8) { 351 | return Promise.reject(Error('encryptPlainText: incorrect cek length')); 352 | } 353 | var bytes = cekBytes.slice(0, config.auth.key_bytes); 354 | return Jose.crypto.subtle.importKey('raw', bytes, config.auth.id, false, ['sign']); 355 | }); 356 | var encKeyPromise = cekBytesPromise.then(function (cekBytes) { 357 | if (cekBytes.byteLength * 8 !== config.id.length + config.auth.key_bytes * 8) { 358 | return Promise.reject(Error('encryptPlainText: incorrect cek length')); 359 | } 360 | var bytes = cekBytes.slice(config.auth.key_bytes); 361 | return Jose.crypto.subtle.importKey('raw', bytes, config.id, false, purpose); 362 | }); 363 | return [macKeyPromise, encKeyPromise]; 364 | } 365 | 366 | /** 367 | * Converts the Jose web algorithms into data which is 368 | * useful for the Web Crypto API. 369 | * 370 | * length = in bits 371 | * bytes = in bytes 372 | */ 373 | getCryptoConfig (alg) { 374 | switch (alg) { 375 | // Key encryption 376 | case 'RSA-OAEP': 377 | return { 378 | jweName: 'RSA-OAEP', 379 | id: { name: 'RSA-OAEP', hash: { name: 'SHA-1' } } 380 | }; 381 | case 'RSA-OAEP-256': 382 | return { 383 | jweName: 'RSA-OAEP-256', 384 | id: { name: 'RSA-OAEP', hash: { name: 'SHA-256' } } 385 | }; 386 | case 'A128KW': 387 | return { 388 | jweName: 'A128KW', 389 | id: { name: 'AES-KW', length: 128 } 390 | }; 391 | case 'A256KW': 392 | return { 393 | jweName: 'A256KW', 394 | id: { name: 'AES-KW', length: 256 } 395 | }; 396 | case 'dir': 397 | return { 398 | jweName: 'dir' 399 | }; 400 | 401 | // Content encryption 402 | case 'A128CBC-HS256': 403 | return { 404 | jweName: 'A128CBC-HS256', 405 | id: { name: 'AES-CBC', length: 128 }, 406 | iv_bytes: 16, 407 | specific_cekBytes: 32, 408 | auth: { 409 | key_bytes: 16, 410 | id: { name: 'HMAC', hash: { name: 'SHA-256' } }, 411 | truncated_bytes: 16 412 | } 413 | }; 414 | case 'A256CBC-HS512': 415 | return { 416 | jweName: 'A256CBC-HS512', 417 | id: { name: 'AES-CBC', length: 256 }, 418 | iv_bytes: 16, 419 | specific_cekBytes: 64, 420 | auth: { 421 | key_bytes: 32, 422 | id: { name: 'HMAC', hash: { name: 'SHA-512' } }, 423 | truncated_bytes: 32 424 | } 425 | }; 426 | case 'A128GCM': 427 | return { 428 | jweName: 'A128GCM', 429 | id: { name: 'AES-GCM', length: 128 }, 430 | iv_bytes: 12, 431 | auth: { 432 | aead: true, 433 | tagBytes: 16 434 | } 435 | }; 436 | case 'A256GCM': 437 | return { 438 | jweName: 'A256GCM', 439 | id: { name: 'AES-GCM', length: 256 }, 440 | iv_bytes: 12, 441 | auth: { 442 | aead: true, 443 | tagBytes: 16 444 | } 445 | }; 446 | default: 447 | throw Error('unsupported algorithm: ' + alg); 448 | } 449 | } 450 | 451 | /** 452 | * Computes a truncated MAC. 453 | * 454 | * @param config configuration 455 | * @param macKeyPromise Promise mac key 456 | * @param aad Uint8Array 457 | * @param iv Uint8Array 458 | * @param cipherText Uint8Array 459 | * @return Promise truncated MAC 460 | */ 461 | truncatedMac (config, macKeyPromise, aad, iv, cipherText) { 462 | return macKeyPromise.then(function (macKey) { 463 | var al = new Uint8Array(Utils.arrayFromInt32(aad.length * 8)); 464 | var alFull = new Uint8Array(8); 465 | alFull.set(al, 4); 466 | var buf = Utils.arrayBufferConcat(aad, iv, cipherText, alFull); 467 | return Jose.crypto.subtle.sign(config.auth.id, macKey, buf).then(function (bytes) { 468 | return bytes.slice(0, config.auth.truncated_bytes); 469 | }); 470 | }); 471 | } 472 | 473 | /** 474 | * Converts the Jose web algorithms into data which is 475 | * useful for the Web Crypto API. 476 | */ 477 | getSignConfig (alg) { 478 | switch (alg) { 479 | case 'RS256': 480 | return { 481 | jwa_name: 'RS256', 482 | id: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } } 483 | }; 484 | case 'RS384': 485 | return { 486 | jwa_name: 'RS384', 487 | id: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-384' } } 488 | }; 489 | case 'RS512': 490 | return { 491 | jwa_name: 'RS512', 492 | id: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-512' } } 493 | }; 494 | case 'PS256': 495 | return { 496 | jwa_name: 'PS256', 497 | id: { name: 'RSA-PSS', hash: { name: 'SHA-256' }, saltLength: 20 } 498 | }; 499 | case 'PS384': 500 | return { 501 | jwa_name: 'PS384', 502 | id: { name: 'RSA-PSS', hash: { name: 'SHA-384' }, saltLength: 20 } 503 | }; 504 | case 'PS512': 505 | return { 506 | jwa_name: 'PS512', 507 | id: { name: 'RSA-PSS', hash: { name: 'SHA-512' }, saltLength: 20 } 508 | }; 509 | case 'HS256': 510 | return { 511 | jwa_name: 'HS256', 512 | id: { name: 'HMAC', hash: { name: 'SHA-256' } } 513 | }; 514 | case 'HS384': 515 | return { 516 | jwa_name: 'HS384', 517 | id: { name: 'HMAC', hash: { name: 'SHA-384' } } 518 | }; 519 | case 'HS512': 520 | return { 521 | jwa_name: 'HS512', 522 | id: { name: 'HMAC', hash: { name: 'SHA-512' } } 523 | }; 524 | case 'ES256': 525 | return { 526 | jwa_name: 'ES256', 527 | id: { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } } 528 | }; 529 | case 'ES384': 530 | return { 531 | jwa_name: 'ES384', 532 | id: { name: 'ECDSA', namedCurve: 'P-384', hash: { name: 'SHA-384' } } 533 | }; 534 | case 'ES512': 535 | return { 536 | jwa_name: 'ES512', 537 | id: { name: 'ECDSA', namedCurve: 'P-521', hash: { name: 'SHA-512' } } 538 | }; 539 | default: 540 | throw Error('unsupported algorithm: ' + alg); 541 | } 542 | } 543 | 544 | /** 545 | * Derives key usage from algorithm's name 546 | * 547 | * @param alg String algorithm name 548 | * @returns {*} 549 | */ 550 | getKeyUsageByAlg (alg) { 551 | switch (alg) { 552 | // signature 553 | case 'RS256': 554 | case 'RS384': 555 | case 'RS512': 556 | case 'PS256': 557 | case 'PS384': 558 | case 'PS512': 559 | case 'HS256': 560 | case 'HS384': 561 | case 'HS512': 562 | case 'ES256': 563 | case 'ES384': 564 | case 'ES512': 565 | case 'ES256K': 566 | return { 567 | publicKey: 'verify', 568 | privateKey: 'sign' 569 | }; 570 | // key encryption 571 | case 'RSA-OAEP': 572 | case 'RSA-OAEP-256': 573 | case 'A128KW': 574 | case 'A256KW': 575 | return { 576 | publicKey: 'wrapKey', 577 | privateKey: 'unwrapKey' 578 | }; 579 | default: 580 | throw Error('unsupported algorithm: ' + alg); 581 | } 582 | } 583 | 584 | /** 585 | * Feel free to override this function. 586 | */ 587 | assert (expr, msg) { 588 | if (!expr) { 589 | throw new Error(msg); 590 | } 591 | } 592 | } 593 | -------------------------------------------------------------------------------- /lib/jose-jws-sign.js: -------------------------------------------------------------------------------- 1 | /* - 2 | * Copyright 2015 Peculiar Ventures 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as Utils from './jose-utils'; 18 | 19 | /** 20 | * Handles decryption. 21 | * 22 | * @param cryptographer an instance of WebCryptographer (or equivalent). Keep 23 | * in mind that decryption mutates the cryptographer. 24 | * 25 | * @author Patrizio Bruno 26 | */ 27 | export class Signer { 28 | constructor (cryptographer) { 29 | this.cryptographer = cryptographer; 30 | 31 | this.keyPromises = {}; 32 | this.waiting_kid = 0; 33 | this.headers = {}; 34 | this.signer_aads = {}; 35 | this.signer_headers = {}; 36 | } 37 | 38 | /** 39 | * Adds a signer to JoseJWS instance. 40 | * 41 | * @param key private key in json format. Parameters can be base64 42 | * encoded, strings or number (for e.g. 'e'), or CryptoKey. 43 | * @param keyId a string identifying the key. OPTIONAL 44 | * @param aad Object protected header 45 | * @param header Object unprotected header 46 | */ 47 | addSigner (key, keyId, aad, header) { 48 | var that = this; 49 | var keyPromise; 50 | if (Utils.isCryptoKey(key)) { 51 | keyPromise = new Promise(function (resolve) { 52 | resolve(key); 53 | }); 54 | } else { 55 | var alg; 56 | if (aad && aad.alg) { 57 | alg = aad.alg; 58 | } else { 59 | alg = that.cryptographer.getContentSignAlgorithm(); 60 | } 61 | keyPromise = Utils.importPrivateKey(key, alg, 'sign'); 62 | } 63 | 64 | var kidPromise; 65 | if (keyId) { 66 | kidPromise = new Promise(function (resolve) { 67 | resolve(keyId); 68 | }); 69 | } else if (Utils.isCryptoKey(key)) { 70 | throw new Error('keyId is a mandatory argument when the key is a CryptoKey'); 71 | } else { 72 | kidPromise = this.cryptographer.keyId(key); 73 | } 74 | 75 | that.waiting_kid++; 76 | 77 | return kidPromise.then(function (kid) { 78 | that.keyPromises[kid] = keyPromise; 79 | that.waiting_kid--; 80 | if (aad) { 81 | that.signer_aads[kid] = aad; 82 | } 83 | if (header) { 84 | that.signer_headers[kid] = header; 85 | } 86 | return kid; 87 | }); 88 | } 89 | 90 | /** 91 | * Adds a signature to a JWS object 92 | * @param jws JWS Object to be signed or its representation 93 | * @param aad Object protected header 94 | * @param header Object unprotected header 95 | * @return Promise 96 | */ 97 | addSignature (jws, aad, header) { 98 | if (Utils.isString(jws)) { 99 | jws = JSON.parse(jws); 100 | } 101 | 102 | if (jws.payload && Utils.isString(jws.payload) && 103 | jws.protected && Utils.isString(jws.protected) && 104 | jws.header && jws.header instanceof Object && 105 | jws.signature && Utils.isString(jws.signature)) { 106 | return this.sign(JWS.fromObject(jws), aad, header); 107 | } else { 108 | throw new Error('JWS is not a valid JWS object'); 109 | } 110 | } 111 | 112 | /** 113 | * Computes signature. 114 | * 115 | * @param payload JWS Object or utf-8 string to be signed 116 | * @param aad Object protected header 117 | * @param header Object unprotected header 118 | * @return Promise 119 | */ 120 | sign (payload, aad, header) { 121 | var that = this; 122 | var kids = []; 123 | 124 | if (Object.keys(that.keyPromises).length === 0) { 125 | throw new Error('No signers defined. At least one is required to sign the JWS.'); 126 | } 127 | 128 | if (that.waiting_kid) { 129 | throw new Error('still generating key IDs'); 130 | } 131 | 132 | function sign (message, protectedHeader, unprotectedHeader, keyPromise, kid) { 133 | var toBeSigned; 134 | 135 | if (!protectedHeader) { 136 | protectedHeader = {}; 137 | } 138 | 139 | if (!protectedHeader.alg) { 140 | protectedHeader.alg = that.cryptographer.getContentSignAlgorithm(); 141 | protectedHeader.typ = 'JWT'; 142 | } 143 | 144 | if (!protectedHeader.kid) { 145 | protectedHeader.kid = kid; 146 | } 147 | 148 | if (Utils.isString(message)) { 149 | toBeSigned = Utils.arrayFromUtf8String(message); 150 | } else { 151 | try { 152 | toBeSigned = Utils.arrayish(message); 153 | } catch (e) { 154 | if (message instanceof JWS) { 155 | toBeSigned = Utils.arrayFromString(new Utils.Base64Url().decode(message.payload)); 156 | } else if (message instanceof Object) { 157 | toBeSigned = Utils.arrayFromUtf8String(JSON.stringify(message)); 158 | } else { 159 | throw new Error('cannot sign this message'); 160 | } 161 | } 162 | } 163 | 164 | return that.cryptographer.sign(protectedHeader, toBeSigned, keyPromise).then(function (signature) { 165 | var jws = new JWS(protectedHeader, unprotectedHeader, toBeSigned, signature); 166 | if (message instanceof JWS) { 167 | delete jws.payload; 168 | if (!message.signatures) { 169 | message.signatures = [jws]; 170 | } else { 171 | message.signatures.push(jws); 172 | } 173 | return message; 174 | } 175 | return jws; 176 | }); 177 | } 178 | 179 | function doSign (pl, ph, uh, kps, kids) { 180 | if (kids.length) { 181 | var kid = kids.shift(); 182 | var rv = sign(pl, that.signer_aads[kid] || ph, that.signer_headers[kid] || uh, kps[kid], kid); 183 | if (kids.length) { 184 | rv = rv.then(function (jws) { 185 | return doSign(jws, null, null, kps, kids); 186 | }); 187 | } 188 | return rv; 189 | } 190 | } 191 | 192 | for (var kid in that.keyPromises) { 193 | if (Object.prototype.hasOwnProperty.call(that.keyPromises, kid)) { 194 | kids.push(kid); 195 | } 196 | } 197 | return doSign(payload, aad, header, that.keyPromises, kids); 198 | } 199 | } 200 | 201 | /** 202 | * Initialize a JWS object. 203 | * 204 | * @param protectedHeader protected header (JS object) 205 | * @param payload Uint8Array payload to be signed 206 | * @param signature ArrayBuffer signature of the payload 207 | * @param header unprotected header (JS object) 208 | * 209 | * @constructor 210 | */ 211 | export class JWS { 212 | constructor (protectedHeader, header, payload, signature) { 213 | this.header = header; 214 | var base64UrlEncoder = new Utils.Base64Url(); 215 | this.payload = base64UrlEncoder.encodeArray(payload); 216 | if (signature) { 217 | this.signature = base64UrlEncoder.encodeArray(signature); 218 | } 219 | this.protected = base64UrlEncoder.encode(JSON.stringify(protectedHeader)); 220 | } 221 | 222 | fromObject (obj) { 223 | var rv = new JWS(obj.protected, obj.header, obj.payload, null); 224 | rv.signature = obj.signature; 225 | rv.signatures = obj.signatures; 226 | return rv; 227 | } 228 | 229 | /** 230 | * Serialize a JWS object using the JSON serialization format 231 | * 232 | * @returns {Object} a copy of this 233 | */ 234 | JsonSerialize () { 235 | return JSON.stringify(this); 236 | } 237 | 238 | /** 239 | * Serialize a JWS object using the Compact Serialization Format 240 | * 241 | * @returns {string} BASE64URL(UTF8(PROTECTED HEADER)).BASE64URL(PAYLOAD).BASE64URL(SIGNATURE) 242 | */ 243 | CompactSerialize () { 244 | return this.protected + '.' + this.payload + '.' + this.signature; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /lib/jose-jws-verify.js: -------------------------------------------------------------------------------- 1 | /* - 2 | * Copyright 2015 Peculiar Ventures 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as Utils from './jose-utils'; 18 | 19 | /** 20 | * Handles signature verification. 21 | * 22 | * @param cryptographer an instance of WebCryptographer (or equivalent). Keep 23 | * in mind that decryption mutates the cryptographer. 24 | * @param message a JWS message 25 | * @param keyfinder (optional) a function returning a Promise given 26 | * a key id 27 | * 28 | * @author Patrizio Bruno 29 | */ 30 | export class Verifier { 31 | constructor (cryptographer, message, keyfinder) { 32 | var that = this; 33 | var alg; 34 | var jwt; 35 | var aad; 36 | var header; 37 | var payload; 38 | var signatures; 39 | var protectedHeader; 40 | var jwtRx = /^([0-9a-z_-]+)\.([0-9a-z_-]+)\.([0-9a-z_-]+)$/i; 41 | 42 | that.cryptographer = cryptographer; 43 | 44 | alg = cryptographer.getContentSignAlgorithm(); 45 | 46 | if (Utils.isString(message)) { 47 | if ((jwt = jwtRx.exec(message))) { 48 | if (jwt.length !== 4) { 49 | throw new Error('wrong JWS compact serialization format'); 50 | } 51 | 52 | message = { 53 | protected: jwt[1], 54 | payload: jwt[2], 55 | signature: jwt[3] 56 | }; 57 | } else { 58 | message = JSON.parse(message); 59 | } 60 | } else if (typeof message !== 'object') { 61 | throw new Error('data format not supported'); 62 | } 63 | 64 | aad = message.protected; 65 | header = message.header; 66 | payload = message.payload; 67 | signatures = message.signatures instanceof Array ? message.signatures.slice(0) : []; 68 | 69 | signatures.forEach(function (sign) { 70 | sign.aad = sign.protected; 71 | sign.protected = JSON.parse(new Utils.Base64Url().decode(sign.protected)); 72 | }); 73 | 74 | that.aad = aad; 75 | protectedHeader = new Utils.Base64Url().decode(aad); 76 | try { 77 | protectedHeader = JSON.parse(protectedHeader); 78 | } catch (e) { 79 | } 80 | 81 | if (!protectedHeader && !header) { 82 | throw new Error('at least one header is required'); 83 | } 84 | 85 | if (!protectedHeader.alg) { 86 | throw new Error("'alg' is a mandatory header"); 87 | } 88 | 89 | if (protectedHeader.alg !== alg) { 90 | throw new Error("the alg header '" + protectedHeader.alg + "' doesn't match the requested algorithm '" + alg + "'"); 91 | } 92 | 93 | if (protectedHeader && protectedHeader.typ && protectedHeader.typ !== 'JWT') { 94 | throw new Error("typ '" + protectedHeader.typ + "' not supported"); 95 | } 96 | 97 | if (message.signature) { 98 | signatures.unshift({ 99 | aad: aad, 100 | protected: protectedHeader, 101 | header: header, 102 | signature: message.signature 103 | }); 104 | } 105 | 106 | that.signatures = []; 107 | for (var i = 0; i < signatures.length; i++) { 108 | that.signatures[i] = JSON.parse(JSON.stringify(signatures[i])); 109 | that.signatures[i].signature = Utils.arrayFromString(new Utils.Base64Url().decode(signatures[i].signature)); 110 | } 111 | 112 | that.payload = payload; 113 | 114 | that.keyPromises = {}; 115 | that.waiting_kid = 0; 116 | 117 | if (keyfinder) { 118 | that.keyfinder = keyfinder; 119 | } 120 | } 121 | 122 | /** 123 | * Add supported recipients to verify multiple signatures 124 | * 125 | * @param key public key in json format. Parameters can be base64 126 | * encoded, strings or number (for 'e'), or CryptoKey. 127 | * @param keyId a string identifying the key. OPTIONAL 128 | * @param alg String signature algorithm. OPTIONAL 129 | * @returns Promise a Promise of a key id 130 | */ 131 | addRecipient (key, keyId, alg) { 132 | var that = this; 133 | var kidPromise; 134 | var keyPromise; 135 | if (Utils.isCryptoKey(key)) { 136 | keyPromise = new Promise(function (resolve) { resolve(key); }); 137 | } else { 138 | keyPromise = Utils.importPublicKey(key, alg || that.cryptographer.getContentSignAlgorithm(), 'verify'); 139 | } 140 | 141 | if (keyId) { 142 | kidPromise = new Promise(function (resolve) { 143 | resolve(keyId); 144 | }); 145 | } else if (Utils.isCryptoKey(key)) { 146 | throw new Error('keyId is a mandatory argument when the key is a CryptoKey'); 147 | } else { 148 | console.log("it's unsafe to omit a keyId"); 149 | kidPromise = this.cryptographer.keyId(key); 150 | } 151 | 152 | that.waiting_kid++; 153 | 154 | return kidPromise.then(function (kid) { 155 | that.keyPromises[kid] = keyPromise; 156 | that.waiting_kid--; 157 | return kid; 158 | }); 159 | } 160 | 161 | /** 162 | * Verifies a JWS signature 163 | * 164 | * @returns Promise a Promise of an array of objects { kid: string, verified: bool, payload?: string } 165 | * 166 | * payload is only populated and usable if verified is true 167 | */ 168 | verify () { 169 | var that = this; 170 | var signatures = that.signatures; 171 | var keyPromises = that.keyPromises; 172 | var keyfinder = that.keyfinder; 173 | var promises = []; 174 | var check = !!keyfinder || Object.keys(that.keyPromises).length > 0; 175 | 176 | if (!check) { 177 | throw new Error('No recipients defined. At least one is required to verify the JWS.'); 178 | } 179 | 180 | if (that.waiting_kid) { 181 | throw new Error('still generating key IDs'); 182 | } 183 | 184 | signatures.forEach(function (sig) { 185 | var kid = sig.protected.kid; 186 | if (keyfinder) { 187 | keyPromises[kid] = keyfinder(kid); 188 | } 189 | promises.push(that.cryptographer.verify(sig.aad, that.payload, sig.signature, keyPromises[kid], kid) 190 | .then(function (vr) { 191 | if (vr.verified) { 192 | vr.payload = new Utils.Base64Url().decode(that.payload); 193 | } 194 | return vr; 195 | })); 196 | }); 197 | return Promise.all(promises); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lib/jose-utils.js: -------------------------------------------------------------------------------- 1 | /* - 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // TODO(eslint): figure out how to properly include Jose or expose crypto object 18 | 19 | import { WebCryptographer } from './jose-jwe-webcryptographer'; 20 | import { Jose } from './jose-core'; 21 | const webCryptographer = new WebCryptographer(); 22 | 23 | /** 24 | * Import a public key in JWK format, either RSA or ECDSA. 25 | * 26 | * @param key JWK public key 27 | * @param alg Name of the JWA signing algorithm (e.g. RS256) 28 | * @return Promise 29 | */ 30 | export const importPublicKey = (key, alg) => { 31 | switch (alg) { 32 | case 'RS256': 33 | case 'RS384': 34 | case 'RS512': 35 | case 'PS256': 36 | case 'PS384': 37 | case 'PS512': 38 | return importRsaPublicKey(key, alg); 39 | case 'ES256': 40 | case 'ES384': 41 | case 'ES512': 42 | return importEcPublicKey(key, alg); 43 | default: 44 | throw Error('unsupported algorithm: ' + alg); 45 | } 46 | }; 47 | 48 | /** 49 | * Import a private key in JWK format, either RSA or EC. 50 | * 51 | * @param key JWK private key 52 | * @param alg Name of the JWA signing algorithm (e.g. RS256) 53 | * @return Promise 54 | */ 55 | export const importPrivateKey = (key, alg) => { 56 | switch (alg) { 57 | case 'RS256': 58 | case 'RS384': 59 | case 'RS512': 60 | case 'PS256': 61 | case 'PS384': 62 | case 'PS512': 63 | return importRsaPrivateKey(key, alg); 64 | case 'ES256': 65 | case 'ES384': 66 | case 'ES512': 67 | return importEcPrivateKey(key, alg); 68 | default: 69 | throw Error('unsupported algorithm: ' + alg); 70 | } 71 | }; 72 | 73 | /** 74 | * Import a public EC key in JWK format. 75 | * 76 | * @param ecKey JWK public key 77 | * @param alg Name of the JWA signing algorithm (e.g. ES256) 78 | * @return Promise 79 | */ 80 | export const importEcPublicKey = (ecKey, alg) => { 81 | var config = webCryptographer.getSignConfig(alg); 82 | var usage = webCryptographer.getKeyUsageByAlg(alg); 83 | 84 | return Jose.crypto.subtle.importKey('jwk', ecKey, config.id, false, [usage.publicKey]); 85 | }; 86 | 87 | /** 88 | * Import a private EC key in JWK format. 89 | * 90 | * @param ecKey JWK private key 91 | * @param alg Name of the JWA signing algorithm (e.g. ES256) 92 | * @return Promise 93 | */ 94 | export const importEcPrivateKey = (ecKey, alg) => { 95 | var config = webCryptographer.getSignConfig(alg); 96 | var usage = webCryptographer.getKeyUsageByAlg(alg); 97 | 98 | return Jose.crypto.subtle.importKey('jwk', ecKey, config.id, false, [usage.privateKey]); 99 | }; 100 | 101 | /** 102 | * Converts the output from `openssl x509 -text` or `openssl rsa -text` into a 103 | * CryptoKey which can then be used with RSA-OAEP. Also accepts (and validates) 104 | * JWK keys. 105 | * 106 | * TODO: this code probably belongs in the webcryptographer. 107 | * 108 | * @param rsaKey public RSA key in json format. Parameters can be base64 109 | * encoded, strings or number (for 'e'). 110 | * @param alg String, name of the algorithm 111 | * @return Promise 112 | */ 113 | export const importRsaPublicKey = (rsaKey, alg) => { 114 | var jwk; 115 | var config; 116 | var usage = webCryptographer.getKeyUsageByAlg(alg); 117 | 118 | if (usage.publicKey === 'wrapKey') { 119 | if (!rsaKey.alg) { 120 | rsaKey.alg = alg; 121 | } 122 | jwk = convertRsaKey(rsaKey, ['n', 'e']); 123 | config = webCryptographer.getCryptoConfig(alg); 124 | } else { 125 | var rk = {}; 126 | for (var name in rsaKey) { 127 | if (Object.prototype.hasOwnProperty.call(rsaKey, name)) { 128 | rk[name] = rsaKey[name]; 129 | } 130 | } 131 | 132 | if (!rk.alg && alg) { 133 | rk.alg = alg; 134 | } 135 | config = webCryptographer.getSignConfig(rk.alg); 136 | jwk = convertRsaKey(rk, ['n', 'e']); 137 | jwk.ext = true; 138 | } 139 | return Jose.crypto.subtle.importKey('jwk', jwk, config.id, false, [usage.publicKey]); 140 | }; 141 | 142 | /** 143 | * Converts the output from `openssl x509 -text` or `openssl rsa -text` into a 144 | * CryptoKey which can then be used with RSA-OAEP and RSA. Also accepts (and validates) 145 | * JWK keys. 146 | * 147 | * TODO: this code probably belongs in the webcryptographer. 148 | * 149 | * @param rsaKey private RSA key in json format. Parameters can be base64 150 | * encoded, strings or number (for 'e'). 151 | * @param alg String, name of the algorithm 152 | * @return Promise 153 | */ 154 | export const importRsaPrivateKey = (rsaKey, alg) => { 155 | var jwk; 156 | var config; 157 | var usage = webCryptographer.getKeyUsageByAlg(alg); 158 | 159 | if (usage.privateKey === 'unwrapKey') { 160 | if (!rsaKey.alg) { 161 | rsaKey.alg = alg; 162 | } 163 | jwk = convertRsaKey(rsaKey, ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi']); 164 | config = webCryptographer.getCryptoConfig(alg); 165 | } else { 166 | var rk = {}; 167 | for (var name in rsaKey) { 168 | if (Object.prototype.hasOwnProperty.call(rsaKey, name)) { 169 | rk[name] = rsaKey[name]; 170 | } 171 | } 172 | config = webCryptographer.getSignConfig(alg); 173 | if (!rk.alg && alg) { 174 | rk.alg = alg; 175 | } 176 | jwk = convertRsaKey(rk, ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi']); 177 | jwk.ext = true; 178 | } 179 | return Jose.crypto.subtle.importKey('jwk', jwk, config.id, false, [usage.privateKey]); 180 | }; 181 | 182 | // Private functions 183 | 184 | export const isString = (str) => { 185 | return ((typeof (str) === 'string') || (str instanceof String)); 186 | }; 187 | 188 | /** 189 | * Takes an arrayish (an array, ArrayBuffer or Uint8Array) 190 | * and returns an array or a Uint8Array. 191 | * 192 | * @param arr arrayish 193 | * @return array or Uint8Array 194 | */ 195 | export const arrayish = (arr) => { 196 | if (arr instanceof Array) { 197 | return arr; 198 | } 199 | if (arr instanceof Uint8Array) { 200 | return arr; 201 | } 202 | if (arr instanceof ArrayBuffer) { 203 | return new Uint8Array(arr); 204 | } 205 | webCryptographer.assert(false, 'arrayish: invalid input'); 206 | }; 207 | 208 | /** 209 | * Checks if an RSA key contains all the expected parameters. Also checks their 210 | * types. Converts hex encoded strings (or numbers) to base64. 211 | * 212 | * @param rsaKey RSA key in json format. Parameters can be base64 encoded, 213 | * strings or number (for 'e'). 214 | * @param parameters array 215 | * @return json 216 | */ 217 | export const convertRsaKey = (rsaKey, parameters) => { 218 | var r = {}; 219 | var alg; 220 | 221 | // Check that we have all the parameters 222 | var missing = []; 223 | parameters.map(function (p) { if (typeof (rsaKey[p]) === 'undefined') { missing.push(p); } }); 224 | 225 | if (missing.length > 0) { 226 | webCryptographer.assert(false, 'convertRsaKey: Was expecting ' + missing.join()); 227 | } 228 | 229 | // kty is either missing or is set to "RSA" 230 | if (typeof (rsaKey.kty) !== 'undefined') { 231 | webCryptographer.assert(rsaKey.kty === 'RSA', "convertRsaKey: expecting rsaKey['kty'] to be 'RSA'"); 232 | } 233 | r.kty = 'RSA'; 234 | 235 | try { 236 | webCryptographer.getSignConfig(rsaKey.alg); 237 | alg = rsaKey.alg; 238 | } catch (err) { 239 | try { 240 | webCryptographer.getCryptoConfig(rsaKey.alg); 241 | alg = rsaKey.alg; 242 | } catch (er) { 243 | webCryptographer.assert(alg, "convertRsaKey: expecting rsaKey['alg'] to have a valid value"); 244 | } 245 | } 246 | r.alg = alg; 247 | 248 | // note: we punt on checking key_ops 249 | 250 | var intFromHex = (e) => { 251 | return parseInt(e, 16); 252 | }; 253 | for (var i = 0; i < parameters.length; i++) { 254 | var p = parameters[i]; 255 | var v = rsaKey[p]; 256 | var base64UrlEncoder = new Base64Url(); 257 | if (p === 'e') { 258 | if (typeof (v) === 'number') { 259 | v = base64UrlEncoder.encodeArray(stripLeadingZeros(arrayFromInt32(v))); 260 | } 261 | } else if (/^([0-9a-fA-F]{2}:)+[0-9a-fA-F]{2}$/.test(v)) { 262 | var arr = v.split(':').map(intFromHex); 263 | v = base64UrlEncoder.encodeArray(stripLeadingZeros(arr)); 264 | } else if (typeof (v) !== 'string') { 265 | webCryptographer.assert(false, "convertRsaKey: expecting rsaKey['" + p + "'] to be a string"); 266 | } 267 | r[p] = v; 268 | } 269 | 270 | return r; 271 | }; 272 | 273 | /** 274 | * Converts a string into an array of ascii codes. 275 | * 276 | * @param str ascii string 277 | * @return Uint8Array 278 | */ 279 | export const arrayFromString = (str) => { 280 | webCryptographer.assert(isString(str), 'arrayFromString: invalid input'); 281 | var arr = str.split('').map(function (c) { 282 | return c.charCodeAt(0); 283 | }); 284 | return new Uint8Array(arr); 285 | }; 286 | 287 | /** 288 | * Converts a string into an array of utf-8 codes. 289 | * 290 | * @param str utf-8 string 291 | * @return Uint8Array 292 | */ 293 | export const arrayFromUtf8String = (str) => { 294 | webCryptographer.assert(isString(str), 'arrayFromUtf8String: invalid input'); 295 | // javascript represents strings as utf-16. Jose imposes the use of 296 | // utf-8, so we need to convert from one representation to the other. 297 | str = unescape(encodeURIComponent(str)); 298 | return arrayFromString(str); 299 | }; 300 | 301 | /** 302 | * Converts an array of ascii bytes into a string. 303 | * 304 | * @param arr arrayish 305 | * @return ascii string 306 | */ 307 | export const stringFromArray = (arr) => { 308 | arr = arrayish(arr); 309 | var r = ''; 310 | for (var i = 0; i < arr.length; i++) { 311 | r += String.fromCharCode(arr[i]); 312 | } 313 | 314 | return r; 315 | }; 316 | 317 | /** 318 | * Converts an array of ascii bytes into a string. 319 | * 320 | * @param arr ArrayBuffer 321 | * @return ascii string 322 | */ 323 | export const utf8StringFromArray = (arr) => { 324 | webCryptographer.assert(arr instanceof ArrayBuffer, 'utf8StringFromArray: invalid input'); 325 | 326 | // javascript represents strings as utf-16. Jose imposes the use of 327 | // utf-8, so we need to convert from one representation to the other. 328 | var r = stringFromArray(arr); 329 | return decodeURIComponent(escape(r)); 330 | }; 331 | 332 | /** 333 | * Strips leading zero in an array. 334 | * 335 | * @param arr arrayish 336 | * @return array 337 | */ 338 | export const stripLeadingZeros = (arr) => { 339 | if (arr instanceof ArrayBuffer) { 340 | arr = new Uint8Array(arr); 341 | } 342 | var isLeadingZero = true; 343 | var r = []; 344 | for (var i = 0; i < arr.length; i++) { 345 | if (isLeadingZero && arr[i] === 0) { 346 | continue; 347 | } 348 | isLeadingZero = false; 349 | r.push(arr[i]); 350 | } 351 | return r; 352 | }; 353 | 354 | /** 355 | * Converts a number into an array of 4 bytes (big endian). 356 | * 357 | * @param i number 358 | * @return ArrayBuffer 359 | */ 360 | export const arrayFromInt32 = (i) => { 361 | webCryptographer.assert(typeof (i) === 'number', 'arrayFromInt32: invalid input'); 362 | // TODO(eslint): figure out if there's a better way to validate i 363 | // eslint-disable-next-line eqeqeq, no-self-compare 364 | webCryptographer.assert(i == i | 0, 'arrayFromInt32: out of range'); 365 | 366 | var buf = new Uint8Array(new Uint32Array([i]).buffer); 367 | var r = new Uint8Array(4); 368 | for (var j = 0; j < 4; j++) { 369 | r[j] = buf[3 - j]; 370 | } 371 | return r.buffer; 372 | }; 373 | 374 | /** 375 | * Concatenates arrayishes. 376 | * 377 | * note: cannot be a Arrow function, because Arrow functions do not expose 'arguments' object 378 | * and Rest parameters are not supported in Babel yet. 379 | * 380 | * @param arguments two or more arrayishes 381 | * @return Uint8Array 382 | */ 383 | export function arrayBufferConcat (/* ... */) { 384 | // Compute total size 385 | var args = []; 386 | var total = 0; 387 | for (var i = 0; i < arguments.length; i++) { 388 | args.push(arrayish(arguments[i])); 389 | total += args[i].length; 390 | } 391 | var r = new Uint8Array(total); 392 | var offset = 0; 393 | for (i = 0; i < arguments.length; i++) { 394 | for (var j = 0; j < args[i].length; j++) { 395 | r[offset++] = args[i][j]; 396 | } 397 | } 398 | webCryptographer.assert(offset === total, 'arrayBufferConcat: unexpected offset'); 399 | return r; 400 | } 401 | 402 | export const sha256 = (str) => { 403 | // Browser docs indicate the first parameter to crypto.subtle.digest to be a 404 | // DOMString. This was initially implemented as an object and continues to be 405 | // supported, so we favor the older form for backwards compatibility. 406 | return Jose.crypto.subtle.digest({ name: 'SHA-256' }, arrayFromString(str)).then(function (hash) { 407 | return new Base64Url().encodeArray(hash); 408 | }); 409 | }; 410 | 411 | export const isCryptoKey = (rsaKey) => { 412 | // Some browsers don't expose the CryptoKey as an object, so we need to check 413 | // the constructor's name. 414 | if (rsaKey.constructor.name === 'CryptoKey') { 415 | return true; 416 | } 417 | 418 | // In the presence of minifiers, relying on class names can be problematic, 419 | // so let's also allow objects that have an 'algorithm' property. 420 | if (Object.prototype.hasOwnProperty.call(rsaKey, 'algorithm')) { 421 | return true; 422 | } 423 | 424 | return false; 425 | }; 426 | export class Base64Url { 427 | /** 428 | * Base64Url encodes a string (no trailing '=') 429 | * 430 | * @param str string 431 | * @return string 432 | */ 433 | encode (str) { 434 | webCryptographer.assert(isString(str), 'Base64Url.encode: invalid input'); 435 | return btoa(str) 436 | .replace(/\+/g, '-') 437 | .replace(/\//g, '_') 438 | .replace(/=+$/, ''); 439 | } 440 | 441 | /** 442 | * Base64Url encodes an array 443 | * 444 | * @param arr array or ArrayBuffer 445 | * @return string 446 | */ 447 | encodeArray (arr) { 448 | return this.encode(stringFromArray(arr)); 449 | } 450 | 451 | /** 452 | * Base64Url decodes a string 453 | * 454 | * @param str string 455 | * @return string 456 | */ 457 | decode (str) { 458 | webCryptographer.assert(isString(str), 'Base64Url.decode: invalid input'); 459 | // atob is nice and ignores missing '=' 460 | return atob(str.replace(/-/g, '+').replace(/_/g, '/')); 461 | } 462 | 463 | decodeArray (str) { 464 | webCryptographer.assert(isString(str), 'Base64Url.decodeArray: invalid input'); 465 | return arrayFromString(this.decode(str)); 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jose-jwe-jws", 3 | "version": "0.2.2", 4 | "description": "Library to encrypt and decrypt data in JSON Web Encryption (JWE) format and to sign data in JSON Web Signature (JWS) format. Leverages Browser's native web crypto API.", 5 | "keywords": [ 6 | "crypto", 7 | "jwe", 8 | "jws", 9 | "encrypt", 10 | "decrypt", 11 | "sign", 12 | "verify" 13 | ], 14 | "license": "Apache-2.0", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/square/js-jose.git" 18 | }, 19 | "scripts": { 20 | "build": "grunt", 21 | "lint": "eslint lib --fix", 22 | "test": "jest" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.4.5", 26 | "@babel/preset-env": "^7.4.5", 27 | "@webpack-cli/init": "^0.3.0", 28 | "babel": "^6.23.0", 29 | "babel-core": "^6.26.3", 30 | "babel-jest": "^25.1.0", 31 | "babel-loader": "^8.0.4", 32 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 33 | "coveralls": "^3.0.5", 34 | "eslint": "^6.0.1", 35 | "eslint-config-standard": "^14.1.0", 36 | "eslint-plugin-import": "^2.18.0", 37 | "eslint-plugin-jest": "^23.1.1", 38 | "eslint-plugin-node": "^11.0.0", 39 | "eslint-plugin-promise": "^4.2.1", 40 | "eslint-plugin-standard": "^4.0.0", 41 | "grunt": "^1.0.4", 42 | "grunt-contrib-concat": "^1.0.1", 43 | "grunt-contrib-uglify": "^4.0.1", 44 | "grunt-eslint": "^22.0.0", 45 | "grunt-karma": "^3.0.2", 46 | "grunt-karma-coveralls": "^2.5.4", 47 | "grunt-run": "^0.8.1", 48 | "grunt-webpack": "^3.1.3", 49 | "jest": "^26.1.0", 50 | "karma": "^4.4.1", 51 | "karma-chrome-launcher": "^3.1.0", 52 | "karma-coverage": "^2.0.1", 53 | "karma-qunit": "^4.0.0", 54 | "qunit": "^2.9.2", 55 | "terser": "^4.1.2", 56 | "uglifyjs-webpack-plugin": "^2.0.1", 57 | "webpack": "^4.20.2", 58 | "webpack-cli": "^3.1.1" 59 | }, 60 | "typings": "jose-jwe-jws.d.ts", 61 | "main": "dist/jose.min.js" 62 | } 63 | -------------------------------------------------------------------------------- /test/jose-jwe-test.js: -------------------------------------------------------------------------------- 1 | QUnit.test('encryption using RSAES OAEP and AES GCM (keys & IV from appendix-A.1)', function (assert) { 2 | const cryptographer = new Jose.WebCryptographer(); 3 | cryptographer.createIV = () => { 4 | return new Uint8Array([227, 197, 117, 252, 2, 219, 233, 68, 180, 225, 77, 219]); 5 | }; 6 | cryptographer.createCek = () => { 7 | const cek = new Uint8Array([177, 161, 244, 128, 84, 143, 225, 115, 63, 180, 3, 255, 107, 154, 212, 246, 138, 7, 110, 91, 112, 46, 34, 105, 47, 130, 203, 46, 122, 234, 64, 252]); 8 | return crypto.subtle.importKey('raw', cek, { name: 'AES-GCM' }, true, ['encrypt']); 9 | }; 10 | const rsaKey = 11 | { 12 | 'kty': 'RSA', 13 | 'n': 'oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW' + 14 | 'cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S' + 15 | 'psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a' + 16 | 'sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS' + 17 | 'tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj' + 18 | 'YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw', 19 | 'e': 'AQAB', 20 | 'd': 'kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N' + 21 | 'WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9' + 22 | '3Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghk' + 23 | 'qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl' + 24 | 't3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd' + 25 | 'VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ', 26 | 'p': '1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-' + 27 | 'SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPTPjXv_hI2_1eTSPVZfzL0lf' + 28 | 'fNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0', 29 | 'q': 'wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBm' + 30 | 'UDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cRUoeBB_jKI1oma0Orv1T9aX' + 31 | 'IWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc', 32 | 'dp': 'ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KL' + 33 | 'hMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjVw_x4NyqyXPM5nULPkcpU827' + 34 | 'rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE', 35 | 'dq': 'Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCj' + 36 | 'ywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDB' + 37 | 'UfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis', 38 | 'qi': 'VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7' + 39 | 'AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3' + 40 | 'eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY' 41 | }; 42 | const publicRsaKey = Jose.Utils.importRsaPublicKey(rsaKey, 'RSA-OAEP'); 43 | const plaintext = 'The true sign of intelligence is not knowledge but imagination.'; 44 | const encrypter = new Jose.JoseJWE.Encrypter(cryptographer, publicRsaKey); 45 | const cipherTextPromise = encrypter.encrypt(plaintext); 46 | 47 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.').length; }), 48 | 5, 'got right number of components'); 49 | 50 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.')[0]; }), 51 | 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ', 52 | 'got expected header'); 53 | 54 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.')[2]; }), 55 | '48V1_ALb6US04U3b', 56 | 'got expected IV'); 57 | 58 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.')[3]; }), 59 | '5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A', 60 | 'got expected cipher text'); 61 | 62 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.')[4]; }), 63 | 'XFBoMYUZodetZdvTiFvSkQ', 64 | 'got expected tag'); 65 | 66 | const privateRsaKey = Jose.Utils.importRsaPrivateKey(rsaKey, 'RSA-OAEP'); 67 | const decrypter = new Jose.JoseJWE.Decrypter(cryptographer, privateRsaKey); 68 | const decryptedPlaintextPromise = cipherTextPromise.then(function (ciphertext) { 69 | return decrypter.decrypt(ciphertext); 70 | }); 71 | assert.willEqual(decryptedPlaintextPromise, plaintext, 'Error: got expected decrypted plain text'); 72 | 73 | // Modify string and check for invalid tag 74 | const macFailure = cipherTextPromise.then(function (ciphertext) { 75 | ciphertext = ciphertext.split('.'); 76 | ciphertext.pop(); 77 | ciphertext.push('WFBoMYUZodetZdvTiFvSkQ'); 78 | return decrypter.decrypt(ciphertext.join('.')); 79 | }); 80 | assert.wont(macFailure, 'invalid tag did not cause failure'); 81 | }); 82 | 83 | // We can't test appendix-A.2 because Chrome dropped support for RSAES-PKCS1-V1_5. 84 | QUnit.test('encryption using AES Key Wrap and AES_128_CBC_HMAC_SHA_256 (keys & IV from appendix-A.3)', function (assert) { 85 | const cryptographer = new Jose.WebCryptographer(); 86 | cryptographer.setKeyEncryptionAlgorithm('A128KW'); 87 | cryptographer.setContentEncryptionAlgorithm('A128CBC-HS256'); 88 | 89 | cryptographer.createIV = function () { 90 | return new Uint8Array([3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101]); 91 | }; 92 | cryptographer.createCek = function () { 93 | const cek = new Uint8Array([4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, 207]); 94 | return crypto.subtle.importKey('raw', cek, { name: 'AES-GCM' }, true, ['encrypt']); 95 | }; 96 | let sharedKey = { 'kty': 'oct', 'k': 'GawgguFyGrWKav7AX4VKUg' }; 97 | sharedKey = crypto.subtle.importKey('jwk', sharedKey, { name: 'AES-KW' }, true, ['wrapKey', 'unwrapKey']); 98 | 99 | const plaintext = 'Live long and prosper.'; 100 | const encrypter = new Jose.JoseJWE.Encrypter(cryptographer, sharedKey); 101 | const cipherTextPromise = encrypter.encrypt(plaintext); 102 | 103 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.').length; }), 104 | 5, 'got right number of components'); 105 | 106 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.')[0]; }), 107 | 'eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0', 108 | 'got expected header'); 109 | 110 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.')[1]; }), 111 | '6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ', 112 | 'got expected encrypted key'); 113 | 114 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.')[2]; }), 115 | 'AxY8DCtDaGlsbGljb3RoZQ', 116 | 'got expected IV'); 117 | 118 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.')[3]; }), 119 | 'KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY', 120 | 'got expected cipher text'); 121 | 122 | assert.willEqual(cipherTextPromise.then(function (result) { return result.split('.')[4]; }), 123 | 'U0m_YmjN04DJvceFICbCVQ', 124 | 'got expected tag'); 125 | 126 | const decrypter = new Jose.JoseJWE.Decrypter(cryptographer, sharedKey); 127 | const decryptedPlaintextPromise = cipherTextPromise.then(function (ciphertext) { 128 | return decrypter.decrypt(ciphertext); 129 | }); 130 | assert.willEqual(decryptedPlaintextPromise, plaintext, 'got expected decrypted plain text'); 131 | 132 | // Modify string and check for invalid tag 133 | const macFailure = cipherTextPromise.then(function (ciphertext) { 134 | ciphertext = ciphertext.split('.'); 135 | ciphertext.pop(); 136 | ciphertext.push('WEm_YmjN04DJvceFICbCVQ'); 137 | return decrypter.decrypt(ciphertext.join('.')); 138 | }); 139 | assert.wontEqual(macFailure, 'Error: decryptCiphertext: MAC failed.', 'invalid tag did not cause failure'); 140 | }); 141 | 142 | QUnit.test('key import', function (assert) { 143 | const cryptographer = new Jose.WebCryptographer(); 144 | cryptographer.setContentEncryptionAlgorithm('A128CBC-HS256'); 145 | 146 | // Key was generated with: `openssl genrsa 1024 | openssl rsa -text` 147 | const rsaKey = { 148 | 'e': 65537, 149 | 'n': '00:bf:53:bf:5b:19:bc:80:5c:88:15:d8:a5:f7:70:cf:c7:0c:aa:f5:b4:07:b8:d1:7b:3a:34:fc:b3:d8:25:46:86:58:5a:d5:69:49:8d:83:23:cd:e2:87:cd:4a:6d:39:a6:8a:dd:dd:c2:e4:d0:ce:7b:a3:d8:28:f8:f0:af:eb:87:2f:f5:c5:bc:5b:c2:08:ec:67:8a:96:57:bb:b6:6b:b5:a2:7d:ab:ff:67:13:09:1b:32:ba:89:c2:27:fd:1b:00:8b:1b:a0:44:3f:a5:4c:39:75:cd:dd:be:75:e6:c7:a5:4f:27:e3:e8:91:0b:1f:38:b7:f5:f7:03:88:f5:1e:b5', 150 | 'd': '0d:f4:61:c4:97:3f:f4:6c:cb:50:2c:99:0e:4f:20:18:78:88:0f:9b:ad:e4:81:02:e7:df:ed:7e:80:89:57:77:7d:02:43:06:86:e2:d7:69:c9:1e:78:a1:34:88:7a:e7:f6:c0:ef:e7:c3:20:a7:ae:c4:e8:83:34:84:f9:8f:c8:10:22:b5:19:ad:07:de:18:5d:d2:ff:27:c2:a7:42:1b:9a:6b:64:43:75:6e:e7:6d:5e:3a:77:fd:2a:65:18:a5:e9:46:79:ea:50:60:a9:27:21:7b:da:71:9b:00:0d:07:63:0f:e4:a7:f7:d7:3c:32:19:b2:73:a5:2b:24:8d:01', 151 | 'p': '00:de:16:f5:44:0a:bf:b5:4c:00:ce:1b:fe:e9:33:6b:47:66:0e:f9:a8:b1:44:ee:54:3f:1c:51:0d:36:fb:40:3a:53:61:46:3b:63:ee:6b:95:54:1d:b2:49:30:47:92:fd:b7:69:87:a5:f0:91:ab:16:ed:1d:0a:c3:ee:27:3c:71', 152 | 'q': '00:dc:8a:57:d7:1a:ba:2d:e9:07:39:bf:64:ef:b2:f2:91:20:6c:32:4b:0d:15:2e:78:ab:a5:99:c5:4f:25:40:cc:8a:9c:d4:f5:3d:ab:a7:e1:e6:d4:97:90:66:bc:fb:45:af:c6:84:1d:1c:56:f6:18:7a:b2:81:27:e3:fa:38:85', 153 | 'dp': '00:9b:4b:2e:61:4f:aa:d1:98:bd:8f:61:a0:13:6c:b2:fd:0f:ee:34:c0:b2:83:e2:aa:e2:1e:68:c6:76:c5:a5:19:a3:a8:07:36:0c:20:70:f5:d0:05:9b:de:f5:75:76:e1:16:59:22:52:f4:2e:c7:95:96:63:92:5d:82:af:c8:e1', 154 | 'dq': '74:1d:fb:05:ec:b2:9e:3d:95:6a:58:55:82:c7:4b:64:12:18:25:9a:d2:76:96:93:3e:7c:e0:ab:bc:72:36:dd:fb:15:7c:22:eb:a7:97:ab:1f:68:4b:ac:e2:0b:1a:99:a4:64:f7:66:84:67:5d:07:a2:82:9d:f2:2c:dc:b0:29', 155 | 'qi': '0c:c8:32:20:2e:df:d7:85:0f:e6:50:ec:ba:1b:6f:60:dd:18:79:3f:d4:ac:d8:6c:bf:05:d7:68:11:3f:2e:1b:26:d5:63:9d:c7:02:0f:e0:c2:70:49:c9:d1:7b:68:66:da:17:36:f5:f2:6b:4e:06:bd:db:29:04:c6:34:7a:e0' 156 | }; 157 | 158 | const publicRsaKey = Jose.Utils.importRsaPublicKey(rsaKey, 'RSA-OAEP'); 159 | const plaintext = 'Look deep into nature, and then you will understand everything better. --Albert Einstein'; 160 | const encrypter = new Jose.JoseJWE.Encrypter(cryptographer, publicRsaKey); 161 | const cipherTextPromise = encrypter.encrypt(plaintext); 162 | 163 | const privateRsaKey = Jose.Utils.importRsaPrivateKey(rsaKey, 'RSA-OAEP'); 164 | const decryptedPlaintextPromise = cipherTextPromise.then(function (ciphertext) { 165 | const decrypter = new Jose.JoseJWE.Decrypter(cryptographer, privateRsaKey); 166 | return decrypter.decrypt(ciphertext); 167 | }); 168 | assert.willEqual(decryptedPlaintextPromise, plaintext, 'got expected decrypted plain text'); 169 | }); 170 | 171 | QUnit.test('extra headers', function (assert) { 172 | const cryptographer = new Jose.WebCryptographer(); 173 | cryptographer.setKeyEncryptionAlgorithm('A128KW'); 174 | let sharedKey = { 'kty': 'oct', 'k': 'GawgguFyGrWKav7AX4VKUg' }; 175 | sharedKey = crypto.subtle.importKey('jwk', sharedKey, cryptographer.keyEncryption.id, true, ['wrapKey', 'unwrapKey']); 176 | const plaintext = 'I only went out for a walk and finally concluded to stay out till sundown, for going out, I found, was really going in. --John Muir'; 177 | 178 | const encrypter = new Jose.JoseJWE.Encrypter(cryptographer, sharedKey); 179 | encrypter.addHeader('cty', 'text/plain'); 180 | const cipherTextPromise = encrypter.encrypt(plaintext); 181 | 182 | const decrypter = new Jose.JoseJWE.Decrypter(cryptographer, sharedKey); 183 | const decryptedPlaintextPromise = cipherTextPromise.then(function (ciphertext) { 184 | return decrypter.decrypt(ciphertext); 185 | }); 186 | assert.willEqual(decryptedPlaintextPromise.then(function (_) { return decrypter.getHeaders()['cty']; }), 'text/plain', 'got expected header'); 187 | }); 188 | 189 | QUnit.test('RSA-OAEP-256 with A256CBC-HS512', function (assert) { 190 | const cryptographer = new Jose.WebCryptographer(); 191 | cryptographer.setKeyEncryptionAlgorithm('RSA-OAEP-256'); 192 | cryptographer.setContentEncryptionAlgorithm('A256CBC-HS512'); 193 | const keyPromise = crypto.subtle.generateKey({ 194 | name: 'RSA-OAEP', 195 | hash: { name: 'SHA-256' }, 196 | modulusLength: 2048, 197 | publicExponent: new Uint8Array([0x01, 0x00, 0x01]) 198 | }, false, ['wrapKey', 'unwrapKey']); 199 | const plaintext = 'Always remember that you are absolutely unique. Just like everyone else. --Margaret Mead'; 200 | 201 | const decryptedPlaintextPromise = keyPromise.then(function (key) { 202 | const encrypter = new Jose.JoseJWE.Encrypter(cryptographer, Promise.resolve(key.publicKey)); 203 | return encrypter.encrypt(plaintext).then(function (ciphertext) { 204 | const decrypter = new Jose.JoseJWE.Decrypter(cryptographer, Promise.resolve(key.privateKey)); 205 | return decrypter.decrypt(ciphertext); 206 | }); 207 | }); 208 | assert.willEqual(decryptedPlaintextPromise, plaintext, 'got expected decrypted plain text'); 209 | }); 210 | 211 | QUnit.test('A256KW with A128GCM', function (assert) { 212 | const cryptographer = new Jose.WebCryptographer(); 213 | cryptographer.setKeyEncryptionAlgorithm('A256KW'); 214 | cryptographer.setContentEncryptionAlgorithm('A128GCM'); 215 | 216 | let sharedKey = { 'kty': 'oct', 'k': 'GawgguFyGrWKav7AX4VKUg' }; 217 | sharedKey = crypto.subtle.importKey('jwk', sharedKey, cryptographer.keyEncryption.id, true, ['wrapKey', 'unwrapKey']); 218 | 219 | const plaintext = 'All generalizations are false, including this one. --Mark Twain'; 220 | const encrypter = new Jose.JoseJWE.Encrypter(cryptographer, sharedKey); 221 | const cipherTextPromise = encrypter.encrypt(plaintext); 222 | 223 | const decryptedPlaintextPromise = cipherTextPromise.then(function (ciphertext) { 224 | const decrypter = new Jose.JoseJWE.Decrypter(cryptographer, sharedKey); 225 | return decrypter.decrypt(ciphertext); 226 | }); 227 | assert.willEqual(decryptedPlaintextPromise, plaintext, 'got expected decrypted plain text'); 228 | }); 229 | 230 | QUnit.test('direct A256GCM', function (assert) { 231 | const cryptographer = new Jose.WebCryptographer(); 232 | cryptographer.setKeyEncryptionAlgorithm('dir'); 233 | 234 | const sharedJwk = { 'alg': 'A256GCM', 'ext': true, 'k': 'Wx5b1Z2nZFgZ8wrEXHo497ZWuvpej1m3PVCgTReiMic', 'key_ops': ['encrypt', 'decrypt'], 'kty': 'oct' }; 235 | const plaintext = "Idealism increases in direct proportion to one's distance from the problem. --John Galsworthy"; 236 | let sharedKey; 237 | 238 | var roundtrip = crypto.subtle.importKey('jwk', sharedJwk, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt']) 239 | .then(function (key) { 240 | sharedKey = key; 241 | var encrypter = new Jose.JoseJWE.Encrypter(cryptographer, sharedKey); 242 | return encrypter.encrypt(plaintext); 243 | }) 244 | .then(function (ciphertext) { 245 | var decrypter = new Jose.JoseJWE.Decrypter(cryptographer, sharedKey); 246 | return decrypter.decrypt(ciphertext); 247 | }); 248 | 249 | assert.willEqual(roundtrip, plaintext, 'got expected decrypted plain text'); 250 | }); 251 | 252 | QUnit.test('unicode encryption using RSAES OAEP and AES GCM (keys & IV from appendix-A.1)', function (assert) { 253 | const cryptographer = new Jose.WebCryptographer(); 254 | cryptographer.createIV = function () { 255 | return new Uint8Array([227, 197, 117, 252, 2, 219, 233, 68, 180, 225, 77, 219]); 256 | }; 257 | cryptographer.createCek = function () { 258 | const cek = new Uint8Array([177, 161, 244, 128, 84, 143, 225, 115, 63, 180, 3, 255, 107, 154, 212, 246, 138, 7, 110, 91, 112, 46, 34, 105, 47, 130, 203, 46, 122, 234, 64, 252]); 259 | return crypto.subtle.importKey('raw', cek, { name: 'AES-GCM' }, true, ['encrypt']); 260 | }; 261 | const rsaKey = 262 | { 263 | 'kty': 'RSA', 264 | 'n': 'oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUW' + 265 | 'cJoZmds2h7M70imEVhRU5djINXtqllXI4DFqcI1DgjT9LewND8MW2Krf3S' + 266 | 'psk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2a' + 267 | 'sbOenSZeyaxziK72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMS' + 268 | 'tPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2dj' + 269 | 'YgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw', 270 | 'e': 'AQAB', 271 | 'd': 'kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5N' + 272 | 'WV5KntaEeXS1j82E375xxhWMHXyvjYecPT9fpwR_M9gV8n9Hrh2anTpTD9' + 273 | '3Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghk' + 274 | 'qDp0Vqj3kbSCz1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vl' + 275 | 't3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSnd' + 276 | 'VTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ', 277 | 'p': '1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-' + 278 | 'SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPTPjXv_hI2_1eTSPVZfzL0lf' + 279 | 'fNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0', 280 | 'q': 'wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBm' + 281 | 'UDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cRUoeBB_jKI1oma0Orv1T9aX' + 282 | 'IWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc', 283 | 'dp': 'ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KL' + 284 | 'hMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjVw_x4NyqyXPM5nULPkcpU827' + 285 | 'rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE', 286 | 'dq': 'Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCj' + 287 | 'ywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDB' + 288 | 'UfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis', 289 | 'qi': 'VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7' + 290 | 'AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3' + 291 | 'eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY' 292 | }; 293 | const publicRsaKey = Jose.Utils.importRsaPublicKey(rsaKey, 'RSA-OAEP'); 294 | const plaintext = '古池や・蛙飛びこむ・水の音 --松尾芭蕉'; 295 | const encrypter = new Jose.JoseJWE.Encrypter(cryptographer, publicRsaKey); 296 | const cipherTextPromise = encrypter.encrypt(plaintext); 297 | 298 | const privateRsaKey = Jose.Utils.importRsaPrivateKey(rsaKey, 'RSA-OAEP'); 299 | const decrypter = new Jose.JoseJWE.Decrypter(cryptographer, privateRsaKey); 300 | const decryptedPlaintextPromise = cipherTextPromise.then(function (ciphertext) { 301 | return decrypter.decrypt(ciphertext); 302 | }); 303 | assert.willEqual(decryptedPlaintextPromise, plaintext, 'Error: got expected decrypted plain text'); 304 | }); 305 | 306 | QUnit.test('unicode direct A256GCM', function (assert) { 307 | const cryptographer = new Jose.WebCryptographer(); 308 | cryptographer.setKeyEncryptionAlgorithm('dir'); 309 | 310 | const sharedJwk = { 'alg': 'A256GCM', 'ext': true, 'k': 'Wx5b1Z2nZFgZ8wrEXHo497ZWuvpej1m3PVCgTReiMic', 'key_ops': ['encrypt', 'decrypt'], 'kty': 'oct' }; 311 | const plaintext = '古池や・蛙飛びこむ・水の音 --松尾芭蕉'; 312 | let sharedKey; 313 | 314 | var roundtrip = crypto.subtle.importKey('jwk', sharedJwk, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt']) 315 | .then(function (key) { 316 | sharedKey = key; 317 | const encrypter = new Jose.JoseJWE.Encrypter(cryptographer, sharedKey); 318 | return encrypter.encrypt(plaintext); 319 | }) 320 | .then(function (ciphertext) { 321 | const decrypter = new Jose.JoseJWE.Decrypter(cryptographer, sharedKey); 322 | return decrypter.decrypt(ciphertext); 323 | }); 324 | 325 | assert.willEqual(roundtrip, plaintext, 'got expected decrypted plain text'); 326 | }); 327 | 328 | QUnit.test('setting invalid algorithm', function (assert) { 329 | const cryptographer = new Jose.WebCryptographer(); 330 | assert.throws(function () { cryptographer.setKeyEncryptionAlgorithm('blah'); }, 'got exception when setting invalid algorithm'); 331 | }); 332 | 333 | QUnit.test('importing public key', function (assert) { 334 | let rsaKey = { 335 | 'e': 65537 336 | }; 337 | assert.throws(function () { Jose.Utils.importRsaPublicKey(rsaKey, 'RSA-OAEP'); }, 'got exception when importing invalid key'); 338 | 339 | rsaKey = { 340 | 'alg': 'foo', 341 | 'e': 65537, 342 | 'n': 'bf:53:bf:5b:19:bc:80' 343 | }; 344 | assert.throws(function () { Jose.Utils.importRsaPublicKey(rsaKey, 'RSA-OAEP'); }, 'got exception when importing invalid key'); 345 | 346 | rsaKey = { 347 | 'e': 65537, 348 | 'n': [] 349 | }; 350 | assert.throws(function () { Jose.Utils.importRsaPublicKey(rsaKey, 'RSA-OAEP'); }, 'got exception when importing invalid key'); 351 | }); 352 | 353 | QUnit.test('various decryption failures', function (assert) { 354 | const cryptographer = new Jose.WebCryptographer(); 355 | cryptographer.setKeyEncryptionAlgorithm('A128KW'); 356 | 357 | let sharedKey = { 'kty': 'oct', 'k': 'GawgguFyGrWKav7AX4VKUg' }; 358 | sharedKey = crypto.subtle.importKey('jwk', sharedKey, { name: 'AES-KW' }, true, ['wrapKey', 'unwrapKey']); 359 | 360 | const plaintext = 'A yawn is a silent scream for coffee. --unknown'; 361 | const encrypter = new Jose.JoseJWE.Encrypter(cryptographer, sharedKey); 362 | const cipherTextPromise = encrypter.encrypt(plaintext); 363 | 364 | const decrypter = new Jose.JoseJWE.Decrypter(cryptographer, sharedKey); 365 | const decryptTruncatedInputPromise = cipherTextPromise.then(function (ciphertext) { 366 | ciphertext = ciphertext.split('.').slice(1).join('.'); 367 | return decrypter.decrypt(ciphertext); 368 | }); 369 | assert.wontEqual(decryptTruncatedInputPromise, 'Error: decrypt: invalid input', 'truncated input did not cause failure'); 370 | 371 | const decryptMissingAlgPromise = cipherTextPromise.then(function (ciphertext) { 372 | ciphertext = ciphertext.split('.'); 373 | ciphertext[0] = 'eyJlbmMiOiJBMjU2R0NNIn0='; 374 | return decrypter.decrypt(ciphertext.join('.')); 375 | }); 376 | assert.wontEqual(decryptMissingAlgPromise, 'Error: decrypt: missing alg', 'missing alg in header did not cause failure'); 377 | 378 | const decryptMissingEncPromise = cipherTextPromise.then(function (ciphertext) { 379 | ciphertext = ciphertext.split('.'); 380 | ciphertext[0] = 'eyJhbGciOiJBMTI4S1cifQ=='; 381 | return decrypter.decrypt(ciphertext.join('.')); 382 | }); 383 | assert.wontEqual(decryptMissingEncPromise, 'Error: decrypt: missing enc', 'missing enc in header did not cause failure'); 384 | 385 | const decryptCritHeaderPromise = cipherTextPromise.then(function (ciphertext) { 386 | ciphertext = ciphertext.split('.'); 387 | ciphertext[0] = 'eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiQTEyOEtXIiwiY3JpdCI6ImJsYWgifQ=='; 388 | return decrypter.decrypt(ciphertext.join('.')); 389 | }); 390 | assert.wontEqual(decryptCritHeaderPromise, 'Error: decrypt: crit is not supported', 'crit in header did not cause failure'); 391 | 392 | const decryptInvalidIvPromise = cipherTextPromise.then(function (ciphertext) { 393 | ciphertext = ciphertext.split('.'); 394 | ciphertext[2] = 'w4kHDJEum_fHW-U'; 395 | return decrypter.decrypt(ciphertext.join('.')); 396 | }); 397 | assert.wontEqual(decryptInvalidIvPromise, 'Error: decryptCiphertext: invalid IV', 'invalid IV did not cause failure'); 398 | 399 | const cryptographer2 = new Jose.WebCryptographer(); 400 | cryptographer2.setKeyEncryptionAlgorithm('A128KW'); 401 | cryptographer2.createIV = function () { return new Uint8Array(new Array(11)); }; 402 | const encrypter2 = new Jose.JoseJWE.Encrypter(cryptographer2, sharedKey); 403 | assert.wontEqual(encrypter2.encrypt(plaintext), 'Error: invalid IV length', "fails to encrypt when we don't have the right IV length"); 404 | }); 405 | 406 | QUnit.test('caniuse', function (assert) { 407 | assert.equal(Jose.caniuse(), true); 408 | }); 409 | -------------------------------------------------------------------------------- /test/jose-jws-ecdsa-test.js: -------------------------------------------------------------------------------- 1 | QUnit.test('signature using ECDSA P-256 with SHA-256 (keys from appendix-A.3.1)', function (assert) { 2 | const ecKey = { 3 | 'kty': 'EC', 4 | 'crv': 'P-256', 5 | 'x': 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', 6 | 'y': 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', 7 | 'd': 'jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI' 8 | }; 9 | const cryptographer = new Jose.WebCryptographer(); 10 | cryptographer.setContentSignAlgorithm('ES256'); 11 | const signer = new Jose.JoseJWS.Signer(cryptographer); 12 | const plaintext = 'The true sign of intelligence is not knowledge but imagination.'; 13 | const verified = signer 14 | .addSigner(ecKey, 'A.3.1') 15 | .then(function () { 16 | return signer.sign(plaintext); 17 | }) 18 | .then(function (signature) { 19 | const verifier = new Jose.JoseJWS.Verifier(cryptographer, signature); 20 | delete ecKey.d; 21 | return verifier.addRecipient(ecKey, 'A.3.1').then(function () { return verifier.verify(); }); 22 | }) 23 | .then(function (result) { 24 | return result[0].verified && result[0].payload === plaintext; 25 | }); 26 | assert.willEqual(verified, true, 'JWS message has been correctly verified'); 27 | }); 28 | -------------------------------------------------------------------------------- /test/jose-jws-rsa-test.js: -------------------------------------------------------------------------------- 1 | QUnit.test('signature using RSASSA-PKCS1-v1_5 with SHA-256 (keys from appendix-A.2.1)', function (assert) { 2 | const rsaKey = { 3 | 'n': 'A1:F8:16:0A:E2:E3:C9:B4:65:CE:8D:2D:65:62:63:36:2B:92:7D:BE:29:E1:F0:24:77:FC:16:25:CC:90:A1:36:E3:8B:D9:34:97:C5:B6:EA:63:DD:77:11:E6:7C:74:29:F9:56:B0:FB:8A:8F:08:9A:DC:4B:69:89:3C:C1:33:3F:53:ED:D0:19:B8:77:84:25:2F:EC:91:4F:E4:85:77:69:59:4B:EA:42:80:D3:2C:0F:55:BF:62:94:4F:13:03:96:BC:6E:9B:DF:6E:BD:D2:BD:A3:67:8E:EC:A0:C6:68:F7:01:B3:8D:BF:FB:38:C8:34:2C:E2:FE:6D:27:FA:DE:4A:5A:48:74:97:9D:D4:B9:CF:9A:DE:C4:C7:5B:05:85:2C:2C:0F:5E:F8:A5:C1:75:03:92:F9:44:E8:ED:64:C1:10:C6:B6:47:60:9A:A4:78:3A:EB:9C:6C:9A:D7:55:31:30:50:63:8B:83:66:5C:6F:6F:7A:82:A3:96:70:2A:1F:64:1B:82:D3:EB:F2:39:22:19:49:1F:B6:86:87:2C:57:16:F5:0A:F8:35:8D:9A:8B:9D:17:C3:40:72:8F:7F:87:D8:9A:18:D8:FC:AB:67:AD:84:59:0C:2E:CF:75:93:39:36:3C:07:03:4D:6F:60:6F:9E:21:E0:54:56:CA:E5:E9:A1', 4 | 'e': 65537, 5 | 'd': '12:AE:71:A4:69:CD:0A:2B:C3:7E:52:6C:45:00:57:1F:1D:61:75:1D:64:E9:49:70:7B:62:59:0F:9D:0B:A5:7C:96:3C:40:1E:3F:CF:2F:2C:D3:BD:EC:88:E5:03:BF:C6:43:9B:0B:28:C8:2F:7D:37:97:67:1F:52:13:EE:D8:C1:5A:25:D8:D5:CE:A0:02:5E:E3:AB:2E:8B:7F:79:21:6F:C6:3B:EA:56:27:53:B4:06:44:C6:A1:51:27:D9:B2:95:45:40:A0:BB:E1:A3:05:56:98:2D:4E:9F:DE:5F:64:25:F1:4D:4B:71:34:41:B5:5D:C7:3B:9B:4A:ED:CC:92:AC:E3:92:7E:37:F5:7D:0C:FD:5E:75:81:FA:51:2C:8F:49:61:A9:EB:0B:80:F8:A8:07:46:72:8A:55:FF:46:47:1F:34:25:06:3B:9D:53:64:2F:5E:DE:1E:84:D6:13:08:1A:FA:5C:22:D0:51:28:5B:D6:3B:94:3B:56:5D:89:8A:05:68:54:13:E5:3C:3C:6C:65:25:FF:1F:E3:4E:3D:DC:70:F0:D5:64:50:FD:A4:8B:A1:2E:10:4E:9D:EB:9F:B8:18:81:E1:C4:BD:F2:5D:92:47:F4:50:C8:65:92:79:68:E7:73:34:F4:41:4F:75:A7:50:E1:39:54:6E:3A:8A:73:9D', 6 | 'p': 'E0:1C:C4:10:EB:48:A6:65:5D:54:46:4D:0A:A4:BB:6D:A0:B8:72:B7:74:A6:A9:D1:1F:FE:48:07:78:F0:DD:B7:31:1A:7E:90:2E:F9:C4:B5:F7:54:76:26:2B:A8:17:6C:B3:59:79:F0:14:37:2A:1A:28:29:E4:13:6B:D0:01:D0:7C:3F:3F:2E:4F:25:D4:C9:34:F6:3C:71:09:FB:A2:E6:76:28:A0:DC:9A:73:C6:05:8E:6D:EF:22:DC:D5:09:50:E7:11:DD:C1:6D:55:86:F2:A7:FA:75:EA:84:94:C1:80:83:E0:B6:57:42:F8:A0:D5:E7:3F:B2:D9:70:F0:19:47', 7 | 'q': 'B9:03:C4:7E:09:95:B6:32:F4:53:2C:B1:F3:C1:99:14:5D:5F:3A:E9:C7:DF:EE:DC:7A:92:A6:B4:7E:29:C6:1B:42:A0:7A:A9:5A:64:FC:60:09:99:C5:A7:B0:1E:01:C0:8C:B6:2C:A7:56:52:7B:BC:C5:60:78:FB:0B:AB:A5:ED:38:DE:2D:07:99:0F:F6:30:0F:AE:AD:EB:6C:03:9A:97:BF:1E:50:7E:4E:70:40:FA:78:DB:82:57:C8:B1:12:ED:5E:59:C3:CD:09:2F:E5:D4:E5:B5:12:75:D4:12:81:07:2A:5E:78:5E:BA:F3:DA:E3:70:92:03:13:3F:51:59:D7', 8 | 'dp': '07:02:9F:57:70:24:AB:9F:CC:15:90:C5:64:29:D6:FB:0C:E5:F8:20:A8:F3:75:A8:66:F9:CB:43:00:93:78:3B:FC:BB:39:6E:45:29:E6:EF:52:37:40:22:DD:86:BA:84:D9:EF:58:93:1B:EE:C5:D0:5F:A5:3F:CF:23:B6:33:F8:53:8A:9E:ED:51:E8:7B:09:78:30:A3:9F:5D:92:93:7B:E6:02:4B:55:DB:36:F7:E0:C0:9D:CB:B7:29:75:38:7F:61:5A:FB:B6:CB:36:BB:AB:E7:79:3C:2B:03:CE:AB:66:DB:B9:31:BA:F5:0B:55:EC:9A:F9:31:1D:00:1D:62:8D', 9 | 'dq': '87:FF:7A:FA:62:B5:47:FE:E0:96:1B:2E:9B:CD:5D:67:18:D3:9D:8C:A7:3D:B6:69:1F:38:99:8D:E7:87:71:76:2C:5D:A6:8C:C2:43:A5:38:3B:16:6B:B2:3D:C5:70:E8:47:06:CA:80:1E:F5:F6:BA:E6:23:6A:0A:AF:A3:77:0E:8F:54:D1:A8:DA:1C:5F:8D:28:99:F0:82:33:1D:DB:0F:5C:8F:3D:FF:FA:4C:8D:97:10:2B:DA:FE:08:2A:11:8D:A6:63:39:88:88:0E:4B:55:59:9C:E6:7A:F2:6E:BF:A5:B2:C1:4A:9D:E7:B2:C4:DD:96:AB:DD:D2:D2:22:4C:75', 10 | 'qi': '21:87:7B:0C:73:A1:AD:6B:F1:93:03:D0:B1:13:36:B4:E8:2B:8D:B7:2B:7E:FB:50:26:2A:5D:F8:39:5C:C7:25:6E:B8:CF:6C:40:B7:60:8D:59:36:A3:2D:BA:17:41:26:A5:27:06:2E:AD:8C:A3:05:FB:7E:17:7F:40:94:37:C9:DC:B9:71:8E:D8:20:18:BC:EF:0F:77:20:A2:C4:75:F9:DB:26:DA:0E:3C:B5:16:D0:84:EB:25:17:8E:82:8D:5C:AB:D4:9B:B3:16:1A:C0:18:1F:A7:F8:21:E8:0E:7A:D3:79:36:F9:94:30:54:AD:09:29:21:EE:2C:59:2E:43:75' 11 | }; 12 | const cryptographer = new Jose.WebCryptographer(); 13 | const signer = new Jose.JoseJWS.Signer(cryptographer); 14 | const plaintext = 'The true sign of intelligence is not knowledge but imagination.'; 15 | cryptographer.setContentSignAlgorithm('RS256'); 16 | const adsPromise = signer.addSigner(rsaKey, 'A.2.1'); 17 | 18 | assert.willEqual(adsPromise.then(function () { 19 | return signer.sign(plaintext, null, {}).then(function (signature) { 20 | const b64uxp = /^[a-z0-9_-]+$/i; 21 | return b64uxp.test(signature.protected) && 22 | b64uxp.test(signature.payload) && 23 | b64uxp.test(signature.signature); 24 | }); 25 | }), true, 'got right JSON serialization format'); 26 | 27 | assert.willEqual(adsPromise.then(function () { 28 | return signer.sign(plaintext, null, {}).then(function (signature) { 29 | const xp = /([a-z0-9_-]+\.){2}[a-z0-9_-]+/i; 30 | return xp.test(signature.CompactSerialize()); 31 | }); 32 | }), true, 'got correct compact serialization format'); 33 | 34 | assert.willEqual(adsPromise.then(function () { 35 | return signer.sign(plaintext, null, {}).then(function (signature) { 36 | const verifier = new Jose.JoseJWS.Verifier(cryptographer, signature); 37 | return verifier.addRecipient(rsaKey, 'A.2.1').then(function () { 38 | return verifier.verify().then(function (result) { 39 | return result.filter(function (value) { 40 | return !value.verified; 41 | }).length === 0; 42 | }); 43 | }); 44 | }); 45 | }), true, 'JWS message has been correctly verified'); 46 | 47 | assert.willEqual(adsPromise.then(function () { 48 | return signer.sign(plaintext, null, {}).then(function (signature) { 49 | const verifier = new Jose.JoseJWS.Verifier(cryptographer, signature); 50 | return verifier.addRecipient(rsaKey, 'A.2.1').then(function () { 51 | return verifier.verify().then(function (result) { 52 | return result[0].verified && result[0].payload === plaintext; 53 | }); 54 | }); 55 | }); 56 | }), true, 'JWS message payload matched expected plaintext'); 57 | }); 58 | 59 | QUnit.test('keyfinder', function (assert) { 60 | const prvjwk = { 61 | 'alg': 'RS256', 62 | 'd': 'l0-Bq8hePldybBsBUon7eGgwD53XfzZNXKB9yq6V6DyOxpAP2KpASdCcTYlDWJb_tqdWSjqxDuWox0l6S3l-f6xjWu5Du1u0DATjzgtdGyrNw-yX0SZlVq_Nbotw0-0GT1VpyUmvNvCS_gy9tONDv4PIrfMe0fwaRoMFmVo40o18At6y4DMxWkH63wUTtXYFlGLH9dNd1yq4odIoqWYpA_aE0pxz1aGpqJzY0_-mXfB8oH-uAY6a474kAJV2PE25_mLxFRmT0qB-0qcpjoTn1SVQfMRaeWvh4YzU-o30mCKWmDgU5Obc0fK9buMoM3uQKFUkRNsCOg1o1_cjx6epsQ', 63 | 'dp': 'R5dAKUQsBjKUCcoczkn5KOI3du5SU0F_p_2lpogHtR5vJcTZjluSm2lB7qgFheB80ZcVPtFiRN5h9A8qWmhHh5DIHibaQ0J_dVLH4CmmlKOBORzNJPicy54G99gsBzvTeqZ8_CYsrj1fRREMJxiHRk1uqe1VQ19nZQDZwrd8Q5E', 64 | 'dq': 'pfXdH9NJdsMkKVUKruaw4K5jYONKVz499dbdTT8LgkkxoQy1Tvm_C1xJYO4KiK1qvjV9_jazqvGkvdxddPQOIc6bOmtm_UfSoMAdussi2spTK6eO5EAzDRcPfXqdgSc7ihjBdklma33q9iMhtIA2SE3IDG0QXswjKDfQ435n8Qk', 65 | 'e': 'AQAB', 66 | 'ext': true, 67 | 'key_ops': ['sign'], 68 | 'kid': '76d4d722-4398-4fc4-a47e-df3d15de7fab', 69 | 'kty': 'RSA', 70 | 'n': 'xw6vxkASH4Jz23XLQMAj3FB55Dq4xCBwaTtUZpKSddMx3hvVeZb_nWv2Ogu18vL-x8LaYGYYG_LSirVA_tJ3ArEw_rqQDJOvsHTAuGrSOJkusJeC46Z5ZP0TndpunV6-04rxltjUVriwO8XzZdH4RnX_kSUEz3qeUc5j-OhTj59sG1-Os51iYiUEwDa6z8fn20wHRtEvPzG0MNQBX58CPuIM9bS8eNOZKUG-oVWAoWcbMfqEE5D8YsW_cZUWY9OLRa5C6xNRMfwlxYeMedJgehXJmxqaxSn0fe6-VZk9qcJDLiWg6vy1NYOJ8UOgtKtSPr7uL4Kc8K8akYVq-qHXAQ', 71 | 'p': '7ZYCLz4jFWW2ody4tPPOrDJn7dKXvkytUC6jL0_iGHNgjbcV3zEDkJC9eJWZaCXIwFKjnzlP4iQTHRqvE8pOFtHLaCj4MpEe9EIg6cZSoc866dVAD9aKS74TeCIQ5YBEvOAa_3h3Uecui0g_Xd0Hm3cr6VcNIMer2OffRmGTJcU', 72 | 'q': '1nw5av78m5eTQv9IgOuxzvy9NQfm2hw8eq2eBduYalIvpa_3QTWXzvJ7_iOomNzZyEm6AjmKQu1Ualfgqq5YDU-jKBY4Pd27SQXHFwO75dsOZc7Xn2QQbHLNNOL8vNSy9ZeEkvDJtwX1lJ9x9XLXztRqJZr_UqS5680zCPO__A0', 73 | 'qi': 'Zv9pwzMgWoAFu9hwp-foLoCYaFdVGj8vV-QovqW507TzhRiZuaP9SsYEF5ZizhEDLtjSxX2KycQUt-EEP9piUvgPiF_K7zXmQ-clvh_qzXrOE4UhfXbAjLFCUBf8YcgFsCYzvvqom3ktxOC0FPhfJr7s2EXQIQjzK5Drm-fZTZc' 74 | }; 75 | const pubjwk = { 76 | 'alg': 'RS256', 77 | 'e': 'AQAB', 78 | 'ext': true, 79 | 'key_ops': ['verify'], 80 | 'kid': '76d4d722-4398-4fc4-a47e-df3d15de7fab', 81 | 'kty': 'RSA', 82 | 'n': 'xw6vxkASH4Jz23XLQMAj3FB55Dq4xCBwaTtUZpKSddMx3hvVeZb_nWv2Ogu18vL-x8LaYGYYG_LSirVA_tJ3ArEw_rqQDJOvsHTAuGrSOJkusJeC46Z5ZP0TndpunV6-04rxltjUVriwO8XzZdH4RnX_kSUEz3qeUc5j-OhTj59sG1-Os51iYiUEwDa6z8fn20wHRtEvPzG0MNQBX58CPuIM9bS8eNOZKUG-oVWAoWcbMfqEE5D8YsW_cZUWY9OLRa5C6xNRMfwlxYeMedJgehXJmxqaxSn0fe6-VZk9qcJDLiWg6vy1NYOJ8UOgtKtSPr7uL4Kc8K8akYVq-qHXAQ' 83 | }; 84 | const plaintext = "When you make the finding yourself - even if you're the last person on Earth to see the light - you'll never forget it. --Carl Sagan"; 85 | 86 | const pubjwks = {}; 87 | pubjwks[pubjwk.kid] = pubjwk; 88 | 89 | const keyfinder = function (kid) { 90 | const jwk = pubjwks[kid]; 91 | if (jwk) { 92 | return window.crypto.subtle.importKey('jwk', jwk, 93 | { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } }, 94 | true, ['verify']); 95 | } else { 96 | return Promise.reject(Error('unknown sender key id ' + kid)); 97 | } 98 | }; 99 | 100 | let cryptographer = new Jose.WebCryptographer(); 101 | cryptographer.setContentSignAlgorithm('RS256'); 102 | let signer = new Jose.JoseJWS.Signer(cryptographer); 103 | 104 | assert.willEqual(signer.addSigner(prvjwk, prvjwk.kid) 105 | .then(function () { 106 | return signer.sign(plaintext); 107 | }) 108 | .then(function (signed) { 109 | const verifier = new Jose.JoseJWS.Verifier(cryptographer, signed, keyfinder); 110 | return verifier.verify().then(function (result) { 111 | return result.filter(function (value) { 112 | return !value.verified; 113 | }).length === 0; 114 | }); 115 | }), 116 | true, 'keyfinder found key'); 117 | 118 | cryptographer = new Jose.WebCryptographer(); 119 | cryptographer.setContentSignAlgorithm('RS256'); 120 | signer = new Jose.JoseJWS.Signer(cryptographer); 121 | 122 | assert.wont(signer.addSigner(prvjwk, prvjwk.kid) 123 | .then(function (prvkey) { 124 | cryptographer = new Jose.WebCryptographer(); 125 | cryptographer.setContentSignAlgorithm('RS256'); 126 | signer = new Jose.JoseJWS.Signer(cryptographer); 127 | return signer.addSigner(prvkey, 'unknown'); 128 | }) 129 | .then(function () { 130 | return signer.sign(plaintext); 131 | }) 132 | .then(function (signed) { 133 | const verifier = new Jose.JoseJWS.Verifier(cryptographer, signed, keyfinder); 134 | return verifier.verify().then(function (result) { 135 | return result.filter(function (value) { 136 | return !value.verified; 137 | }).length === 0; 138 | }); 139 | }), 'keyfinder rejected properly on unknown key'); 140 | }); 141 | -------------------------------------------------------------------------------- /test/qunit-promises.js: -------------------------------------------------------------------------------- 1 | (function (QUnit) { 2 | // jQuery promise objects have .then and .always methods 3 | // Q promise objects have .then and .finally methods 4 | function verifyPromise (promise) { 5 | if (!promise) { 6 | throw new Error('expected a promise object'); 7 | } 8 | if (typeof promise.then !== 'function') { 9 | throw new Error('promise object does not have .then function'); 10 | } 11 | } 12 | 13 | QUnit.extend(QUnit.assert, { 14 | // resolved promises 15 | will: function (promise, message) { 16 | verifyPromise(promise); 17 | 18 | const assert = this; 19 | const done = assert.async(); 20 | promise.then(function () { 21 | assert.pushResult({ 22 | result: true, 23 | actual: undefined, 24 | expected: undefined, 25 | message: message 26 | }); 27 | done(); 28 | }, function () { 29 | assert.pushResult({ 30 | result: false, 31 | actual: undefined, 32 | expected: undefined, 33 | message: 'promise rejected (but should have been resolved)' 34 | }); 35 | done(); 36 | }); 37 | }, 38 | 39 | willEqual: function (promise, expected, message) { 40 | verifyPromise(promise); 41 | 42 | const assert = this; 43 | const done = assert.async(); 44 | promise.then(function (actual) { 45 | assert.pushResult({ 46 | result: actual == expected, 47 | actual: actual, 48 | expected: expected, 49 | message: message 50 | }); 51 | done(); 52 | }, function (actual) { 53 | assert.pushResult({ 54 | result: false, 55 | actual: actual, 56 | expected: expected, 57 | message: 'promise rejected (but should have been resolved)' 58 | }); 59 | done(); 60 | }); 61 | }, 62 | 63 | willDeepEqual: function (promise, expected, message) { 64 | var always = verifyPromise(promise); 65 | 66 | const assert = this; 67 | const done = assert.async(); 68 | promise.then(function (actual) { 69 | if (typeof QUnit.equiv !== 'function') { 70 | throw new Error('Missing QUnit.equiv function'); 71 | } 72 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 73 | assert.pushResult({ 74 | result: QUnit.equiv(actual, expected), 75 | actual: actual, 76 | expected: expected, 77 | message: message 78 | }); 79 | done(); 80 | }, function (actual) { 81 | assert.pushResult({ 82 | result: false, 83 | actual: actual, 84 | expected: expected, 85 | message: 'promise rejected (but should have been resolved)' 86 | }); 87 | done(); 88 | }); 89 | }, 90 | 91 | // rejected promises 92 | wont: function (promise, message) { 93 | var always = verifyPromise(promise); 94 | 95 | const assert = this; 96 | const done = assert.async(); 97 | promise.then(function () { 98 | assert.pushResult({ 99 | result: false, 100 | actual: undefined, 101 | expected: undefined, 102 | message: 'promise resolved (but should have been rejected)' 103 | }); 104 | done(); 105 | }, function () { 106 | assert.pushResult({ 107 | result: true, 108 | actual: undefined, 109 | expected: undefined, 110 | message: message 111 | }); 112 | done(); 113 | }); 114 | }, 115 | 116 | wontEqual: function (promise, expected, message) { 117 | var always = verifyPromise(promise); 118 | 119 | const assert = this; 120 | const done = assert.async(); 121 | promise.then(function (actual) { 122 | assert.pushResult({ 123 | result: false, 124 | actual: actual, 125 | expected: expected, 126 | message: 'promise resolved (but should have been rejected)' 127 | }); 128 | done(); 129 | }, function (actual) { 130 | assert.pushResult({ 131 | result: actual == expected, 132 | actual: actual, 133 | expected: expected, 134 | message: message 135 | }); 136 | done(); 137 | }); 138 | } 139 | }); 140 | }(QUnit)); 141 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | module: { 6 | rules: [{ 7 | use: { 8 | loader: 'babel-loader', 9 | options: { 10 | presets: ['@babel/preset-env'] 11 | } 12 | }, 13 | test: /\.js$/ 14 | }] 15 | }, 16 | 17 | entry: './lib/jose-core', 18 | 19 | output: { 20 | filename: 'jose.js', 21 | library:'Jose', 22 | libraryTarget: 'var', 23 | path: path.resolve(__dirname, 'dist') 24 | }, 25 | 26 | mode: 'development', 27 | devtool: 'source-map', 28 | 29 | optimization: { 30 | splitChunks: { 31 | cacheGroups: { 32 | vendors: { 33 | priority: -10, 34 | test: /[\\/]node_modules[\\/]/ 35 | } 36 | }, 37 | 38 | chunks: 'async', 39 | minChunks: 1, 40 | minSize: 30000, 41 | name: true 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | module: { 6 | rules: [{ 7 | use: { 8 | loader: 'babel-loader', 9 | options: { 10 | presets: ['@babel/preset-env'] 11 | } 12 | }, 13 | test: /\.js$/ 14 | }] 15 | }, 16 | 17 | entry: './lib/jose-core', 18 | 19 | output: { 20 | filename: 'jose.min.js', 21 | library: 'Jose', 22 | libraryTarget: 'umd', 23 | path: path.resolve(__dirname, 'dist') 24 | }, 25 | 26 | mode: 'production', 27 | 28 | optimization: { 29 | splitChunks: { 30 | cacheGroups: { 31 | vendors: { 32 | priority: -10, 33 | test: /[\\/]node_modules[\\/]/ 34 | } 35 | }, 36 | 37 | chunks: 'async', 38 | minChunks: 1, 39 | minSize: 30000, 40 | name: true 41 | } 42 | } 43 | } 44 | --------------------------------------------------------------------------------