├── .editorconfig ├── .env.example ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples └── private-key-storage.html ├── karma.conf.js ├── package.json ├── rollup.config.js ├── scripts ├── register-assert-browser.js └── register-assert.js ├── src ├── Auth │ ├── AccessTokenProviders │ │ ├── CachingJwtProvider.ts │ │ ├── CallbackJwtProvider.ts │ │ ├── ConstAccessTokenProvider.ts │ │ ├── GeneratorJwtProvider.ts │ │ ├── index.ts │ │ └── interfaces.ts │ ├── Jwt.ts │ ├── JwtGenerator.ts │ ├── JwtVerifier.ts │ └── jwt-constants.ts ├── Cards │ ├── CardManager.ts │ ├── CardUtils.ts │ ├── CardVerifier.ts │ ├── ICard.ts │ ├── ModelSigner.ts │ ├── RawSignedModel.ts │ ├── constants.ts │ └── errors.ts ├── Client │ ├── CardClient.ts │ ├── Connection.ts │ ├── VirgilAgent.ts │ └── errors.ts ├── Lib │ ├── assert.ts │ ├── base64.ts │ ├── fetch.ts │ ├── platform │ │ ├── browserList.ts │ │ └── osList.ts │ └── timestamp.ts ├── Storage │ ├── KeyEntryStorage │ │ ├── IKeyEntryStorage.ts │ │ ├── KeyEntryStorage.ts │ │ └── errors.ts │ ├── KeyStorage.ts │ ├── PrivateKeyStorage.ts │ ├── adapters │ │ ├── DefaultStorageAdapter.ts │ │ ├── FileSystemStorageAdapter.ts │ │ ├── IStorageAdapter.ts │ │ ├── IndexedDbStorageAdapter.ts │ │ ├── errors.ts │ │ └── indexedDb │ │ │ ├── declarations.d.ts │ │ │ └── isIndexedDbValid.ts │ └── errors.ts ├── VirgilError.ts ├── __tests__ │ ├── declarations.d.ts │ ├── index.ts │ ├── integration │ │ ├── CardManager.test.ts │ │ ├── CardVerifier.test.ts │ │ ├── Jwt.test.ts │ │ ├── ModelSigner.test.ts │ │ ├── RawSignedModel.test.ts │ │ └── data │ │ │ ├── data.json │ │ │ └── index.ts │ └── unit │ │ ├── CachingJwtProvider.test.ts │ │ ├── CallbackJwtProvider.test.ts │ │ ├── CardClient.test.ts │ │ ├── ConstAccesTokenProvider.test.ts │ │ ├── GeneratorJwtProvider.test.ts │ │ ├── JwtGenerator.test.ts │ │ ├── KeyEntryStorage.test.ts │ │ ├── KeyStorage.test.ts │ │ ├── PrivateKeyStorage.test.ts │ │ ├── StorageAdapter.test.ts │ │ ├── VirgilAgent.test.ts │ │ ├── base64.test.ts │ │ └── data │ │ └── userAgents.json ├── declarations.d.ts ├── index.ts └── types.ts ├── tsconfig.json ├── tsconfig.spec.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs. 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 4 11 | indent_style = tab 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.{json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_KEY_PRIVATE_KEY= 2 | API_KEY_ID= 3 | APP_ID= 4 | API_URL= 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | .env 4 | .idea/ 5 | npm-debug.log 6 | .rpt2_cache 7 | yarn-error.log 8 | docs/ 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | matrix: 3 | include: 4 | - os: linux 5 | dist: trusty 6 | node_js: "11" 7 | - os: linux 8 | dist: trusty 9 | node_js: "12" 10 | - os: linux 11 | dist: trusty 12 | node_js: "13" 13 | - os: linux 14 | dist: trusty 15 | sudo: required 16 | addons: 17 | chrome: stable # have Travis install chrome stable. 18 | node_js: "10" 19 | env: 20 | - TEST_BROWSER=true 21 | - BUILD_DOCS=true 22 | cache: 23 | yarn: true 24 | directories: 25 | - node_modules 26 | install: 27 | - yarn install 28 | script: 29 | - if [[ "$TEST_BROWSER" = true ]]; then yarn test; else yarn run test:node; fi 30 | - if [[ "$BUILD_DOCS" = true ]]; then yarn run docs; fi 31 | deploy: 32 | provider: pages 33 | skip-cleanup: true 34 | github-token: $GITHUB_TOKEN 35 | keep-history: true 36 | local-dir: docs 37 | on: # deploy only tagged build on Node.js 10 38 | tags: true 39 | node: '10' 40 | condition: $TRAVIS_OS_NAME = linux 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to this Virgil Security SDK 2 | 3 | #### **Did you find a bug?** 4 | 5 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/VirgilSecurity/virgil-sdk-javascript/issues). 6 | 7 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/VirgilSecurity/virgil-sdk-javascript/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. 8 | 9 | #### **Did you write a patch that fixes a bug?** 10 | 11 | * Open a new GitHub pull request with the patch. 12 | 13 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 14 | 15 | #### **Do you intend to add a new feature or change an existing one?** 16 | 17 | * Suggest your change [in a new issue](https://github.com/VirgilSecurity/virgil-sdk-javascript/issues/new) and start writing code. 18 | 19 | * Make sure your new code does not break any tests and include new tests. 20 | 21 | * With good code comes good documentation. Try to copy the existing documentation and adapt it to your needs. 22 | 23 | * Close the issue or mark it as inactive if you decide to discontinue working on the code. 24 | 25 | #### **Do you have questions about the source code?** 26 | 27 | * Ask any question about how to use the CLI by [raising a new issue](https://github.com/VirgilSecurity/virgil-sdk-javascript/issues/new). 28 | 29 | #### **Do you want to contribute to the documentation?** 30 | 31 | * Most of our documentation is provided on our [Developer Portal](https://virgilsecurity.com/docs), so please let us know if any of your changes to the code will need to be reflected in the documentation. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Virgil Security, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /examples/private-key-storage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Private Key Storage 6 | 7 | 8 | 9 | 10 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const dotenv = require('dotenv-webpack') 4 | const packageJson = require('./package.json'); 5 | 6 | 7 | module.exports = function(config) { 8 | config.set({ 9 | frameworks: ['mocha', 'chai', 'sinon-chai', 'chai-as-promised'], 10 | autoWatch: true, 11 | browsers: ['ChromeHeadless'], 12 | files: [ 13 | {pattern: 'scripts/register-assert.js', included: false}, 14 | 'src/__tests__/index.ts', 15 | ], 16 | colors: true, 17 | reporters: ['progress'], 18 | logLevel: config.LOG_INFO, 19 | browserNoActivityTimeout: 60 * 1000, 20 | captureTimeout: 60000, 21 | singleRun: true, 22 | mime: { 23 | 'text/x-typescript': ['ts'], 24 | 'application/wasm': ['wasm'], 25 | }, 26 | preprocessors: { 27 | 'src/__tests__/index.ts': ['webpack'], 28 | }, 29 | webpack: { 30 | output: { 31 | path: '/test', 32 | }, 33 | mode: 'development', 34 | resolve: { 35 | extensions: ['.js', '.ts'], 36 | fallback: { 37 | fs: false, 38 | net: false, 39 | tls: false 40 | }, 41 | }, 42 | module: { 43 | rules: [ 44 | { 45 | test: /\.ts$/, 46 | loader: 'ts-loader', 47 | }, 48 | { 49 | test: /\.wasm$/, 50 | type: "asset/inline", 51 | }, 52 | ], 53 | }, 54 | experiments: { 55 | asyncWebAssembly: true, 56 | }, 57 | plugins: [ 58 | new dotenv({ 59 | browser: JSON.stringify(true), 60 | VERSION: JSON.stringify(packageJson.version), 61 | API_KEY_PRIVATE_KEY: JSON.stringify(process.env.APP_KEY), 62 | API_KEY_ID: JSON.stringify(process.env.APP_KEY_ID), 63 | APP_ID: JSON.stringify(process.env.APP_ID), 64 | API_URL: JSON.stringify(process.env.API_URL), 65 | }), 66 | ], 67 | }, 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virgil-sdk", 3 | "version": "6.2.0", 4 | "description": "Virgil Security Services SDK", 5 | "contributors": [ 6 | "Eugene Baranov (https://github.com/ebaranov/)", 7 | "Egor Gumenyuk (https://github.com/boo1ean/)", 8 | "Vadim Avdeiev (https://github.com/vadimavdeev/)", 9 | "Pavlo Ponomarenko (https://github.com/theshock/)", 10 | "Alexey Smirnov (https://github.com/xlwknx/)", 11 | "Serhii Nanovskyi " 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/VirgilSecurity/virgil-sdk-javascript" 16 | }, 17 | "main": "dist/virgil-sdk.cjs.js", 18 | "module": "dist/virgil-sdk.es.js", 19 | "browser": { 20 | "./dist/virgil-sdk.cjs.js": "./dist/virgil-sdk.browser.cjs.js", 21 | "./dist/virgil-sdk.es.js": "./dist/virgil-sdk.browser.es.js", 22 | "./src/Storage/adapters/FileSystemStorageAdapter.ts": "./src/Storage/adapters/IndexedDbStorageAdapter.ts" 23 | }, 24 | "typings": "dist/types/index.d.ts", 25 | "files": [ 26 | "dist" 27 | ], 28 | "author": "Virgil Security Inc. ", 29 | "keywords": [ 30 | "security", 31 | "elliptic", 32 | "elliptic curve", 33 | "virgil", 34 | "virgilsecurity", 35 | "encryption", 36 | "crypto", 37 | "pki" 38 | ], 39 | "license": "BSD-3-Clause", 40 | "devDependencies": { 41 | "@types/chai": "^4.3.4", 42 | "@types/chai-as-promised": "^7.1.5", 43 | "@types/jest": "^29.2.2", 44 | "@types/mkdirp": "^1.0.2", 45 | "@types/mocha": "^10.0.0", 46 | "@types/node": "^18.11.9", 47 | "@types/rimraf": "^3.0.2", 48 | "@types/sinon": "^10.0.13", 49 | "@types/sinon-chai": "^3.2.9", 50 | "buffer-es6": "^4.9.3", 51 | "builtin-modules": "^3.3.0", 52 | "chai": "^4.3.7", 53 | "chai-as-promised": "^7.1.1", 54 | "dotenv": "^16.0.3", 55 | "file-loader": "^6.2.0", 56 | "karma": "^6.4.1", 57 | "karma-chai-plugins": "^0.9.0", 58 | "karma-chrome-launcher": "^3.1.1", 59 | "karma-mocha": "^2.0.0", 60 | "karma-rollup-preprocessor": "^7.0.8", 61 | "karma-webpack": "^5.0.0", 62 | "mocha": "^10.1.0", 63 | "rollup": "^3.2.5", 64 | "rollup-plugin-commonjs": "^10.0.2", 65 | "rollup-plugin-node-resolve": "^5.2.0", 66 | "rollup-plugin-replace": "^2.2.0", 67 | "rollup-plugin-terser": "^7.0.2", 68 | "rollup-plugin-typescript2": "^0.34.1", 69 | "rollup-plugin-wasm": "^3.0.0", 70 | "sinon": "^14.0.2", 71 | "sinon-chai": "^3.7.0", 72 | "ts-loader": "^9.4.1", 73 | "ts-node": "^10.9.1", 74 | "typedoc": "^0.23.20", 75 | "typescript": "^4.8.4", 76 | "virgil-crypto": "^4.0.0" 77 | }, 78 | "dependencies": { 79 | "@types/base-64": "^1.0.0", 80 | "@types/utf8": "^3.0.1", 81 | "@virgilsecurity/crypto-types": "^1.0.0", 82 | "@virgilsecurity/data-utils": "^1.0.0", 83 | "base-64": "^1.0.0", 84 | "dotenv-webpack": "^8.0.1", 85 | "fetch-ponyfill": "^7.1.0", 86 | "karma-typescript": "^5.5.3", 87 | "mkdirp": "^1.0.4", 88 | "rimraf": "^3.0.2", 89 | "ts-mocha": "^10.0.0", 90 | "utf8": "^3.0.0", 91 | "util": "^0.12.5", 92 | "webpack": "^5.74.0" 93 | }, 94 | "type": "commonjs", 95 | "scripts": { 96 | "clean": "rimraf .rpt2_cache dist", 97 | "build": "rollup -c", 98 | "prepare": "npm run clean && npm run build", 99 | "test:node": "ts-mocha -p tsconfig.spec.json \"src/**/*.test.ts\" --require scripts/register-assert --timeout 10000", 100 | "test:browser": "karma start", 101 | "test": "npm run test:node", 102 | "preversion": "npm run test", 103 | "version": "npm run build", 104 | "docs": "typedoc src" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const builtinModules = require('builtin-modules'); 4 | const commonjs = require('rollup-plugin-commonjs'); 5 | const nodeResolve = require('rollup-plugin-node-resolve'); 6 | const replace = require('rollup-plugin-replace'); 7 | const { terser } = require('rollup-plugin-terser'); 8 | const typescript = require('rollup-plugin-typescript2'); 9 | 10 | const packageJson = require('./package.json'); 11 | 12 | const FORMAT = { 13 | CJS: 'cjs', 14 | ES: 'es', 15 | UMD: 'umd', 16 | }; 17 | 18 | const sourcePath = path.join(__dirname, 'src'); 19 | const outputPath = path.join(__dirname, 'dist'); 20 | const input = path.join(sourcePath, 'index.ts'); 21 | 22 | const createEntry = (format, isBrowser) => { 23 | let filename = 'virgil-sdk'; 24 | if (isBrowser) { 25 | filename += '.browser'; 26 | } 27 | filename += `.${format}.js`; 28 | 29 | let tsconfigOverride = format === FORMAT.ES ? { compilerOptions: { target: 'es2015' } } : {}; 30 | return { 31 | external: builtinModules.concat(isBrowser ? [] : Object.keys(packageJson.dependencies)), 32 | input, 33 | output: { 34 | format, 35 | file: path.join(outputPath, filename), 36 | name: 'Virgil', 37 | }, 38 | plugins: [ 39 | isBrowser && nodeResolve({ 40 | browser: true, 41 | extensions: ['.ts', '.js'] 42 | }), 43 | isBrowser && commonjs(), 44 | replace({ 45 | 'process.browser': JSON.stringify(isBrowser), 46 | 'process.env.VERSION': JSON.stringify(packageJson.version), 47 | }), 48 | typescript({ 49 | typescript: require('typescript'), 50 | exclude: ['**/*.test.ts'], 51 | useTsconfigDeclarationDir: true, 52 | tsconfigOverride, 53 | }), 54 | format === FORMAT.UMD && terser(), 55 | ].filter(Boolean), 56 | }; 57 | }; 58 | 59 | module.exports = [ 60 | createEntry(FORMAT.CJS), 61 | createEntry(FORMAT.ES), 62 | createEntry(FORMAT.CJS, true), 63 | createEntry(FORMAT.ES, true), 64 | createEntry(FORMAT.UMD, true), 65 | ]; 66 | -------------------------------------------------------------------------------- /scripts/register-assert-browser.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const sinon = require('sinon'); 3 | const sinonChai = require('sinon-chai'); 4 | const chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(sinonChai); 7 | chai.use(chaiAsPromised); 8 | 9 | sinon.assert.expose(chai.assert, { prefix: '' }); 10 | 11 | global.sinon = sinon; 12 | global.assert = chai.assert; 13 | -------------------------------------------------------------------------------- /scripts/register-assert.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const sinon = require('sinon'); 3 | const sinonChai = require('sinon-chai'); 4 | const chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(sinonChai); 7 | chai.use(chaiAsPromised); 8 | 9 | sinon.assert.expose(chai.assert, { prefix: '' }); 10 | 11 | global.sinon = sinon; 12 | global.assert = chai.assert; 13 | -------------------------------------------------------------------------------- /src/Auth/AccessTokenProviders/CachingJwtProvider.ts: -------------------------------------------------------------------------------- 1 | import { IAccessToken, IAccessTokenProvider, ITokenContext } from './interfaces'; 2 | import { GetJwtCallback, Jwt } from '../Jwt'; 3 | import { addSeconds } from '../../Lib/timestamp'; 4 | 5 | const TOKEN_EXPIRATION_MARGIN = 5; 6 | 7 | /** 8 | * Implementation of {@link IAccessTokenProvider} that caches the JWT 9 | * in memory while it's fresh (i.e. not expired) and uses the user-provided 10 | * callback function to get the JWT when requested by the clients. 11 | */ 12 | export class CachingJwtProvider implements IAccessTokenProvider { 13 | private cachedJwt?: Jwt; 14 | private readonly getJwt: (tokenContext: ITokenContext) => Promise; 15 | private jwtPromise?: Promise; 16 | 17 | /** 18 | * Creates a new instance of `CachingJwtProvider`. 19 | * @param {GetJwtCallback} renewJwtFn - The function that will be called 20 | * whenever the fresh JWT is needed. If the `renewJwtFn` returns the JWT 21 | * as a string, it will be converted to {@link Jwt} instance automatically. 22 | * @param {Jwt|string} [initialToken] - Optional initial JWT. 23 | */ 24 | constructor(renewJwtFn: GetJwtCallback, initialToken?: Jwt | string) { 25 | if (typeof renewJwtFn !== 'function') { 26 | throw new TypeError('`renewJwtFn` must be a function'); 27 | } 28 | 29 | if (initialToken) { 30 | let jwt; 31 | if (typeof initialToken === 'string') { 32 | jwt = Jwt.fromString(initialToken); 33 | } else if (initialToken instanceof Jwt) { 34 | jwt = initialToken; 35 | } else { 36 | throw new Error( 37 | `Expected "initialToken" to be a string or an instance of Jwt, got ${ 38 | typeof initialToken 39 | }`); 40 | } 41 | 42 | this.cachedJwt = jwt; 43 | } 44 | 45 | this.getJwt = (context: ITokenContext) => { 46 | if (this.cachedJwt && !this.cachedJwt.isExpired(addSeconds(new Date, TOKEN_EXPIRATION_MARGIN))) { 47 | return Promise.resolve(this.cachedJwt); 48 | } 49 | 50 | if (this.jwtPromise) { 51 | return this.jwtPromise; 52 | } 53 | 54 | this.jwtPromise = Promise.resolve(renewJwtFn(context)) 55 | .then(token => { 56 | const jwt = typeof token === 'string' ? Jwt.fromString(token) : token; 57 | this.cachedJwt = jwt; 58 | this.jwtPromise = undefined; 59 | return jwt; 60 | }).catch(err => { 61 | this.jwtPromise = undefined; 62 | throw err; 63 | }); 64 | 65 | return this.jwtPromise; 66 | } 67 | } 68 | 69 | /** 70 | * Returns a `Promise` resolved with the cached token if it's fresh, or the 71 | * token obtained by the call to the `renewJwtCallback` otherwise. The token 72 | * obtained from the `renewJwtCallback` is then cached. If the `renewJwtCallback` 73 | * returns the JWT as a string, it is converted to {@link Jwt} instance before returning. 74 | * @param {ITokenContext} context 75 | * @returns {Promise} 76 | */ 77 | getToken(context: ITokenContext): Promise { 78 | return this.getJwt(context); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Auth/AccessTokenProviders/CallbackJwtProvider.ts: -------------------------------------------------------------------------------- 1 | import { GetJwtCallback, Jwt } from '../Jwt'; 2 | import { IAccessToken, IAccessTokenProvider, ITokenContext } from './interfaces'; 3 | 4 | /** 5 | * Implementation of {@link IAccessToken} that calls the user-provided 6 | * callback function to get the JWT when requested by the clients. 7 | */ 8 | export class CallbackJwtProvider implements IAccessTokenProvider { 9 | private readonly getJwt: (tokenContext: ITokenContext) => Promise; 10 | 11 | /** 12 | * Creates a new instance of `CallbackJwtProvider`. 13 | * 14 | * @param {GetJwtCallback} getJwtFn - The function that will be called 15 | * whenever the JWT is needed. If the `getJwtFn` returns the JWT as a 16 | * string, it will be converted to {@link Jwt} instance automatically. 17 | */ 18 | public constructor (getJwtFn: GetJwtCallback) { 19 | if (typeof getJwtFn !== 'function') { 20 | throw new TypeError('`getJwtFn` must be a function'); 21 | } 22 | 23 | this.getJwt = (context: ITokenContext) => 24 | Promise.resolve(getJwtFn(context)) 25 | .then(token => typeof token === 'string' ? Jwt.fromString(token) : token); 26 | } 27 | 28 | /** 29 | * Returns a `Promise` resolved with the {@link Jwt} instance obtained 30 | * by the call to the {@link CallbackJwtProvider.getJwt}. If the 31 | * `getJwtFn` returns the JWT as a string, it is converted to 32 | * {@link Jwt} instance before returning. 33 | * 34 | * @param {ITokenContext} context 35 | * @returns {Promise} 36 | */ 37 | public getToken(context: ITokenContext): Promise { 38 | return this.getJwt(context); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Auth/AccessTokenProviders/ConstAccessTokenProvider.ts: -------------------------------------------------------------------------------- 1 | import { IAccessToken, IAccessTokenProvider, ITokenContext } from './interfaces'; 2 | 3 | /** 4 | * Implementation of {@link IAccessTokenProvider} that returns a 5 | * user-provided constant access token whenever it is requested by the clients. 6 | */ 7 | export class ConstAccessTokenProvider implements IAccessTokenProvider { 8 | /** 9 | * Creates a new instance of `ConstAccessTokenProvider` 10 | * @param {IAccessToken} accessToken - The access token to be returned 11 | * whenever it is requested. 12 | */ 13 | public constructor (private readonly accessToken: IAccessToken) { 14 | if (accessToken == null) { 15 | throw new TypeError('`accessToken` is required'); 16 | } 17 | }; 18 | 19 | /** 20 | * Returns a `Promise` fulfilled with the 21 | * {@link ConstAccessTokenProvider.accessToken} provided to the constructor 22 | * of this instance. 23 | * 24 | * @param {ITokenContext} context 25 | * @returns {Promise} 26 | */ 27 | public getToken(context: ITokenContext): Promise { 28 | return Promise.resolve(this.accessToken); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Auth/AccessTokenProviders/GeneratorJwtProvider.ts: -------------------------------------------------------------------------------- 1 | import { JwtGenerator } from '../JwtGenerator'; 2 | import { IAccessToken, IAccessTokenProvider, ITokenContext } from './interfaces'; 3 | import { IExtraData } from '../../Cards/ICard'; 4 | 5 | /** 6 | * Implementation of {@link IAccessTokenProvider} that generates a 7 | * new JWT whenever it is requested by the clients. 8 | * 9 | * This class is meant to be used on the server side only. 10 | */ 11 | export class GeneratorJwtProvider implements IAccessTokenProvider { 12 | /** 13 | * Creates a new instance of `GeneratorJwtProvider` with the given 14 | * {@link JwtGenerator}, additional data and default identity. 15 | * 16 | * @param {JwtGenerator} jwtGenerator - Object to delegate the JWT generation to. 17 | * @param {IExtraData} additionalData - Additional data to include with the JWT. 18 | * @param {string} defaultIdentity - Identity of the user to include in the token 19 | * when none is provided explicitly by the client. 20 | */ 21 | constructor( 22 | private readonly jwtGenerator: JwtGenerator, 23 | private readonly additionalData?: IExtraData, 24 | private readonly defaultIdentity?: string 25 | ) { 26 | if (jwtGenerator == null) { 27 | throw new TypeError('`jwtGenerator` is required'); 28 | } 29 | } 30 | 31 | /** 32 | * Returns a `Promise` fulfilled with the JWT obtained from the call 33 | * to {@link GeneratorJwtProvider.jwtGenerator} {@link JwtGenerator.generateToken} 34 | * method, passing it the {@link GeneratorJwtProvider.additionalData} and 35 | * {@link GeneratorJwtProvider.defaultIdentity} 36 | * 37 | * @param {ITokenContext} context 38 | * @returns {Promise} 39 | */ 40 | getToken(context: ITokenContext): Promise { 41 | return Promise.resolve().then(() => { 42 | const jwt = this.jwtGenerator.generateToken( 43 | context.identity || this.defaultIdentity || '', 44 | this.additionalData 45 | ); 46 | 47 | return jwt; 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Auth/AccessTokenProviders/index.ts: -------------------------------------------------------------------------------- 1 | export { CachingJwtProvider } from './CachingJwtProvider'; 2 | export { CallbackJwtProvider } from './CallbackJwtProvider'; 3 | export { ConstAccessTokenProvider } from './ConstAccessTokenProvider'; 4 | export { GeneratorJwtProvider } from './GeneratorJwtProvider'; 5 | export * from './interfaces'; 6 | -------------------------------------------------------------------------------- /src/Auth/AccessTokenProviders/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for objects that represent API Access Tokens. 3 | */ 4 | export interface IAccessToken { 5 | /** 6 | * Retrieves user's identity from the token. 7 | * @returns {string} 8 | */ 9 | identity(): string; 10 | 11 | /** 12 | * Returns the string representation of the token. 13 | * @returns {string} 14 | */ 15 | toString(): string; 16 | } 17 | 18 | /** 19 | * Interface for objects that provide context to the 20 | * {@link IAccessTokenProvider} implementations, i.e. which operation 21 | * is being performed, identity of the user performing the operation 22 | * and whether to force a new token to be retrieved in case the tokens 23 | * are being cached by the provider. 24 | */ 25 | export interface ITokenContext { 26 | /** 27 | * Name of the Service requested. 28 | */ 29 | readonly service: string; 30 | 31 | /** 32 | * Identity of the user performing the operation that 33 | * requires Access Token. May be `undefined`. 34 | */ 35 | readonly identity?: string; 36 | 37 | /** 38 | * Name of the operation that requires an Access Token. 39 | */ 40 | readonly operation: string; 41 | 42 | /** 43 | * Indicates whether to force a new token to be retrieved in case the 44 | * tokens are being cached by the provider. 45 | */ 46 | readonly forceReload?: boolean; 47 | } 48 | 49 | /** 50 | * Interface to be implemented by classes able to provide API 51 | * Access Tokens asynchronously. 52 | */ 53 | export interface IAccessTokenProvider { 54 | /** 55 | * Provides an API Access Token asynchronously. 56 | * 57 | * @param {ITokenContext} context - The context object describing the 58 | * operation for which the token is needed. 59 | * 60 | * @returns {Promise} 61 | */ 62 | getToken(context: ITokenContext): Promise; 63 | } 64 | -------------------------------------------------------------------------------- /src/Auth/Jwt.ts: -------------------------------------------------------------------------------- 1 | import { IExtraData } from '../Cards/ICard'; 2 | import { base64UrlDecode, base64UrlEncode, base64UrlFromBase64, base64UrlToBase64 } from '../Lib/base64'; 3 | import { IAccessToken, ITokenContext } from './AccessTokenProviders/index'; 4 | import { getUnixTimestamp } from '../Lib/timestamp'; 5 | import { IssuerPrefix, SubjectPrefix } from './jwt-constants'; 6 | 7 | /** 8 | * The callback function used to get the JWT as either `string`, or {@Link Jwt} instance 9 | * synchronously or asynchronously. 10 | */ 11 | export type GetJwtCallback = (context: ITokenContext) => Promise | Jwt | string; 12 | 13 | /** 14 | * Interface for objects representing JWT Header. 15 | */ 16 | export interface IJwtHeader { 17 | /** 18 | * The algorithm used to calculate the token signature. 19 | */ 20 | readonly alg: string; 21 | 22 | /** 23 | * The type of the token. Always "JWT". 24 | */ 25 | readonly typ: string; 26 | 27 | /** 28 | * The content type of the token. 29 | */ 30 | readonly cty: string; 31 | 32 | /** 33 | * Id of the API Key used to calculate the token signature. 34 | */ 35 | readonly kid: string; 36 | } 37 | 38 | /** 39 | * Interface for objects representing JWT Body. 40 | */ 41 | export interface IJwtBody { 42 | /** 43 | * The issuer of the token (i.e. Application ID) 44 | */ 45 | readonly iss: string; 46 | 47 | /** 48 | * The subject of the token (i.e. User identity) 49 | */ 50 | readonly sub: string; 51 | 52 | /** 53 | * The token issue date as Unix timestamp 54 | */ 55 | readonly iat: number; 56 | 57 | /** 58 | * The token expiry date as Unix timestamp 59 | */ 60 | readonly exp: number; 61 | 62 | /** 63 | * User-defined attributes associated with the token 64 | */ 65 | readonly ada?: IExtraData; 66 | } 67 | 68 | /** 69 | * Class representing the JWT providing access to the 70 | * Virgil Security APIs. 71 | * Implements {@link IAccessToken} interface. 72 | */ 73 | export class Jwt implements IAccessToken { 74 | 75 | /** 76 | * Parses the string representation of the JWT into 77 | * an object representation. 78 | * 79 | * @param {string} jwtStr - The JWT string. Must have the following format: 80 | * 81 | * `base64UrlEncode(Header) + "." + base64UrlEncode(Body) + "." + base64UrlEncode(Signature)` 82 | * 83 | * See the {@link https://jwt.io/introduction/ | Introduction to JWT} for more details. 84 | * 85 | * @returns {Jwt} 86 | */ 87 | public static fromString (jwtStr: string): Jwt { 88 | return new Jwt(jwtStr); 89 | } 90 | 91 | public readonly header: IJwtHeader; 92 | public readonly body : IJwtBody; 93 | public readonly signature?: string; 94 | 95 | /** 96 | * The data used to calculate the JWT Signature 97 | * 98 | * `base64UrlEncode(header) + "." + base64UrlEncode(body)` 99 | */ 100 | public readonly unsignedData: string; 101 | private readonly stringRepresentation: string; 102 | 103 | /** 104 | * Creates a new instance of `Jwt` from the given string. The string 105 | * must be in the following format: 106 | * `base64UrlEncode(header).base64UrlEncode(body).base64UrlEncode(signature)` 107 | * @param {string} stringRepresentation 108 | */ 109 | constructor (stringRepresentation: string); 110 | /** 111 | * Creates a new instance of `Jwt` with the given header, body and 112 | * optional signature. 113 | * 114 | * @param {IJwtHeader} header 115 | * @param {IJwtBody} body 116 | * @param {string} signature 117 | */ 118 | constructor (header: IJwtHeader, body : IJwtBody, signature?: string); 119 | constructor (header: IJwtHeader | string, body?: IJwtBody, signature?: string) { 120 | if (typeof header === 'string') { 121 | const stringRepresentation = header; 122 | const parts = stringRepresentation.split('.'); 123 | 124 | if (parts.length !== 3) throw new Error('Wrong JWT format'); 125 | 126 | try { 127 | this.header = JSON.parse(base64UrlDecode(parts[0])); 128 | this.body = JSON.parse(base64UrlDecode(parts[1])); 129 | this.signature = base64UrlToBase64(parts[2]); 130 | 131 | } catch (e) { 132 | throw new Error('Wrong JWT format'); 133 | } 134 | this.unsignedData = parts[0] + '.' + parts[1]; 135 | this.stringRepresentation = stringRepresentation; 136 | } else if (typeof header === 'object' && typeof body === 'object') { 137 | this.header = header; 138 | this.body = body; 139 | this.signature = signature; 140 | 141 | this.unsignedData = this.headerBase64() + '.' + this.bodyBase64(); 142 | this.stringRepresentation = this.signature == null 143 | ? this.unsignedData 144 | : this.unsignedData + '.' + this.signatureBase64(); 145 | } else { 146 | throw new TypeError( 147 | 'Invalid arguments for function Jwt. ' + 148 | 'Expected a string representation of a token, or header and body as objects' 149 | ); 150 | } 151 | 152 | } 153 | 154 | /** 155 | * Returns the string representation of this JWT. 156 | * @returns {string} 157 | */ 158 | public toString () : string { 159 | return this.stringRepresentation; 160 | } 161 | 162 | /** 163 | * Retrieves the identity that is the subject of this JWT. 164 | * @returns {string} 165 | */ 166 | public identity(): string { 167 | if (this.body.sub.indexOf(SubjectPrefix) !== 0) { 168 | throw new Error('wrong sub format'); 169 | } 170 | 171 | return this.body.sub.substr(SubjectPrefix.length); 172 | } 173 | 174 | /** 175 | * Retrieves the application ID that is the issuer of this JWT. 176 | * @returns {string} 177 | */ 178 | public appId(): string { 179 | if (this.body.iss.indexOf(IssuerPrefix) !== 0) { 180 | throw new Error('wrong iss format'); 181 | } 182 | 183 | return this.body.iss.substr(IssuerPrefix.length); 184 | } 185 | 186 | /** 187 | * Returns a boolean indicating whether this JWT is (or will be) 188 | * expired at the given date or not. 189 | * 190 | * @param {Date} at - The date to check. Defaults to `new Date()`. 191 | * @returns {boolean} - `true` if token is expired, otherwise `false`. 192 | */ 193 | public isExpired (at: Date = new Date): boolean { 194 | const now = getUnixTimestamp(at); 195 | return this.body.exp < now; 196 | } 197 | 198 | private headerBase64(): string { 199 | return base64UrlEncode( JSON.stringify(this.header) ); 200 | } 201 | 202 | private bodyBase64(): string { 203 | return base64UrlEncode( JSON.stringify(this.body) ); 204 | } 205 | 206 | private signatureBase64(): string { 207 | return base64UrlFromBase64( this.signature! ); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/Auth/JwtGenerator.ts: -------------------------------------------------------------------------------- 1 | import { IPrivateKey, IAccessTokenSigner } from '../types'; 2 | import { IExtraData } from '../Cards/ICard'; 3 | import { 4 | IJwtBody, 5 | IJwtHeader, 6 | Jwt 7 | } from './Jwt'; 8 | import { 9 | IssuerPrefix, 10 | JwtContentType, 11 | SubjectPrefix, 12 | VirgilContentType 13 | } from './jwt-constants'; 14 | import { getUnixTimestamp } from '../Lib/timestamp'; 15 | import { assert } from '../Lib/assert'; 16 | 17 | /** 18 | * {@link JwtGenerator} initialization options. 19 | */ 20 | export interface IJwtGeneratorOptions { 21 | /** 22 | * API Private key from Virgil Dashboard to be used to generate 23 | * signatures of tokens. 24 | */ 25 | apiKey: IPrivateKey; 26 | 27 | /** 28 | * ID of the API Key from Virgil Dashboard. 29 | */ 30 | apiKeyId: string; 31 | 32 | /** 33 | * Application ID from Virgil Dashboard. This will be the value of the 34 | * `Issuer` field in generated tokens. 35 | */ 36 | appId: string; 37 | 38 | /** 39 | * Object used to generate signatures of tokens. 40 | */ 41 | accessTokenSigner: IAccessTokenSigner; 42 | 43 | /** 44 | * Time to live in milliseconds of the tokens created by this generator. 45 | * Optional. Default is `20 * 60 * 1000` (20 minutes). 46 | */ 47 | millisecondsToLive?: number; 48 | } 49 | 50 | const DEFAULT_TOKEN_TTL = 20 * 60 * 1000; // 20 minutes 51 | 52 | /** 53 | * Class responsible for JWT generation. 54 | */ 55 | export class JwtGenerator { 56 | /** 57 | * @see {@link IJwtGeneratorOptions.appId} 58 | */ 59 | readonly appId: string; 60 | 61 | /** 62 | * @see {@link IJwtGeneratorOptions.apiKey} 63 | */ 64 | readonly apiKey: IPrivateKey; 65 | 66 | /** 67 | * @see {@link IJwtGeneratorOptions.apiKeyId} 68 | */ 69 | readonly apiKeyId: string; 70 | 71 | /** 72 | * @see {@link IJwtGeneratorOptions.accessTokenSigner} 73 | */ 74 | readonly accessTokenSigner: IAccessTokenSigner; 75 | 76 | /** 77 | * @see {@link IJwtGeneratorOptions.millisecondsToLive} 78 | */ 79 | readonly millisecondsToLive: number; 80 | 81 | constructor (options: IJwtGeneratorOptions) { 82 | validateOptions(options); 83 | 84 | this.appId = options.appId; 85 | this.apiKey = options.apiKey; 86 | this.apiKeyId = options.apiKeyId; 87 | this.accessTokenSigner = options.accessTokenSigner; 88 | this.millisecondsToLive = options.millisecondsToLive !== undefined 89 | ? Number(options.millisecondsToLive) 90 | : DEFAULT_TOKEN_TTL; 91 | } 92 | 93 | /** 94 | * Generates a token with the given identity as the subject and optional 95 | * additional data. 96 | * @param {string} identity - Identity to be associated with JWT (i.e. 97 | * the Subject). 98 | * @param {IExtraData} ada - Additional data to be encoded in the JWT. 99 | * @returns {Jwt} 100 | */ 101 | generateToken(identity: string, ada?: IExtraData) { 102 | if (!identity) { 103 | throw new TypeError( 104 | 'Illegal arguments for function `generateToken`. Argument `identity` is required.' 105 | ); 106 | } 107 | 108 | const iat = getUnixTimestamp(new Date()); 109 | const exp = getUnixTimestamp(new Date().getTime() + this.millisecondsToLive); 110 | 111 | const body: IJwtBody = { 112 | iss: IssuerPrefix + this.appId, 113 | sub: SubjectPrefix + identity, 114 | iat, 115 | exp, 116 | ada 117 | }; 118 | 119 | const header: IJwtHeader = { 120 | alg: this.accessTokenSigner.getAlgorithm(), 121 | kid: this.apiKeyId, 122 | typ: JwtContentType, 123 | cty: VirgilContentType 124 | }; 125 | 126 | const unsignedJwt = new Jwt(header, body); 127 | const signature = this.accessTokenSigner.generateTokenSignature( 128 | { value: unsignedJwt.unsignedData, encoding: 'utf8' }, 129 | this.apiKey 130 | ); 131 | 132 | return new Jwt(header, body, signature.toString('base64')); 133 | 134 | } 135 | } 136 | 137 | function validateOptions(opts: IJwtGeneratorOptions) { 138 | const invalidOptionMessage = (name: keyof IJwtGeneratorOptions) => 139 | `Invalid JwtGenerator options. \`${name}\` is required`; 140 | 141 | assert(opts != null, 'JwtGenerator options must be provided'); 142 | assert(opts.apiKey != null, invalidOptionMessage('apiKey')); 143 | assert(opts.apiKeyId != null, invalidOptionMessage('apiKeyId')); 144 | assert(opts.appId != null, invalidOptionMessage('appId')); 145 | assert(opts.accessTokenSigner != null, invalidOptionMessage('accessTokenSigner')); 146 | } 147 | -------------------------------------------------------------------------------- /src/Auth/JwtVerifier.ts: -------------------------------------------------------------------------------- 1 | import { IPublicKey, IAccessTokenSigner } from '../types'; 2 | import { Jwt } from './Jwt'; 3 | import { JwtContentType, VirgilContentType } from './jwt-constants'; 4 | import { assert } from '../Lib/assert'; 5 | 6 | /** 7 | * {@link JwtVerifier} initialization options. 8 | */ 9 | export interface IJwtVerifierOptions { 10 | /** 11 | * Object used to verify signatures of tokens. 12 | */ 13 | accessTokenSigner: IAccessTokenSigner; 14 | 15 | /** 16 | * API Public Key from Virgil Dashboard to be used to verify signatures 17 | * of tokens. 18 | */ 19 | apiPublicKey: IPublicKey, 20 | 21 | /** 22 | * ID of the API Key from Virgil Dashboard. 23 | */ 24 | apiKeyId: string; 25 | } 26 | 27 | /** 28 | * Class responsible for verification of JWTs. 29 | */ 30 | export class JwtVerifier { 31 | /** 32 | * @see {@link IJwtVerifierOptions.accessTokenSigner} 33 | */ 34 | public readonly accessTokenSigner: IAccessTokenSigner; 35 | 36 | /** 37 | * @see {@link IJwtVerifierOptions.apiPublicKey} 38 | */ 39 | public readonly apiPublicKey: IPublicKey; 40 | 41 | /** 42 | * @see {@link IJwtVerifierOptions.apiKeyId} 43 | */ 44 | public readonly apiKeyId: string; 45 | 46 | public constructor (options: IJwtVerifierOptions) { 47 | validateOptions(options); 48 | this.accessTokenSigner = options.accessTokenSigner; 49 | this.apiPublicKey = options.apiPublicKey; 50 | this.apiKeyId = options.apiKeyId; 51 | } 52 | 53 | /** 54 | * Verifies the validity of the given JWT. 55 | * @param {Jwt} token - The JWT to verify. 56 | * @returns {boolean} 57 | */ 58 | public verifyToken(token: Jwt): boolean { 59 | if (token == null) { 60 | throw new Error('Token is empty'); 61 | } 62 | 63 | if (!this.allFieldsAreCorrect(token)) { 64 | return false; 65 | } 66 | 67 | return this.accessTokenSigner.verifyTokenSignature( 68 | { value: token.unsignedData, encoding: 'utf8' }, 69 | { value: token.signature!, encoding: 'base64' }, 70 | this.apiPublicKey 71 | ); 72 | } 73 | 74 | private allFieldsAreCorrect (token: Jwt): boolean { 75 | return token.header.kid == this.apiKeyId 76 | && token.header.alg == this.accessTokenSigner.getAlgorithm() 77 | && token.header.cty == VirgilContentType 78 | && token.header.typ == JwtContentType; 79 | } 80 | } 81 | 82 | function validateOptions(opts: IJwtVerifierOptions) { 83 | const invalidOptionMessage = (name: keyof IJwtVerifierOptions) => 84 | `Invalid JwtVerifier options. \`${name}\` is required`; 85 | 86 | assert(opts != null, 'JwtVerifier options must be provided'); 87 | assert(opts.apiPublicKey != null, invalidOptionMessage('apiPublicKey')); 88 | assert(opts.apiKeyId != null, invalidOptionMessage('apiKeyId')); 89 | assert(opts.accessTokenSigner != null, invalidOptionMessage('accessTokenSigner')); 90 | } 91 | -------------------------------------------------------------------------------- /src/Auth/jwt-constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * JWT Subject. 3 | * @hidden 4 | */ 5 | export const SubjectPrefix = "identity-"; 6 | 7 | /** 8 | * JWT Issuer. 9 | * @hidden 10 | */ 11 | export const IssuerPrefix = "virgil-"; 12 | 13 | /** 14 | * Content type of the token. Used to convey structural information 15 | * about the JWT. 16 | * @hidden 17 | */ 18 | export const VirgilContentType = "virgil-jwt;v=1"; 19 | 20 | /** 21 | * Media type of the JWT. 22 | * @hidden 23 | */ 24 | export const JwtContentType = "JWT"; 25 | -------------------------------------------------------------------------------- /src/Cards/CardManager.ts: -------------------------------------------------------------------------------- 1 | import { ICardCrypto } from '../types'; 2 | import { IRawSignedModelJson, RawSignedModel } from './RawSignedModel'; 3 | import { CardClient } from '../Client/CardClient'; 4 | import { ModelSigner } from './ModelSigner'; 5 | import { ICard, INewCardParams, IRawCardContent } from './ICard'; 6 | import { ICardVerifier } from './CardVerifier'; 7 | import { IAccessToken, IAccessTokenProvider, ITokenContext } from '../Auth/AccessTokenProviders'; 8 | import { cardToRawSignedModel, generateRawSigned, linkedCardList, parseRawSignedModel } from './CardUtils'; 9 | import { assert } from '../Lib/assert'; 10 | import { VirgilCardVerificationError } from './errors'; 11 | import { VirgilHttpError, ErrorCode } from '../Client/errors'; 12 | import { SelfSigner } from './constants'; 13 | import { IProductInfo } from '../Client/Connection'; 14 | 15 | const CARDS_SERVICE__NAME = 'cards'; 16 | 17 | /** 18 | * @hidden 19 | */ 20 | const throwingAccessTokenProvider: IAccessTokenProvider = { 21 | getToken: () => { throw new Error( 22 | 'Please set `CardManager.accessTokenProvider` to be able to make requests.' 23 | ); } 24 | }; 25 | 26 | type Omit = Pick>; 27 | 28 | const getCardServiceTokenContext = (context: Omit): ITokenContext => ({ 29 | ...context, 30 | service: CARDS_SERVICE__NAME, 31 | }); 32 | 33 | /** 34 | * User-specified callback function to be called just before publishing 35 | * a Virgil Card to append additional signatures to it. 36 | * 37 | * Receives a single parameter of type {@link RawSignedModel} and must 38 | * return a {@link RawSignedModel} with additional signatures. 39 | * Use {@link ModelSigner} to add signatures. 40 | */ 41 | export type ISignCallback = (model: RawSignedModel) => Promise | RawSignedModel; 42 | 43 | export { ICard }; 44 | 45 | /** 46 | * {@link CardManager} initialization options. 47 | */ 48 | export interface ICardManagerParams { 49 | 50 | /** 51 | * Implementation of {@link IAccessTokenProvider} to use to 52 | * get the token for requests authentication. Optional. 53 | */ 54 | readonly accessTokenProvider?: IAccessTokenProvider; 55 | 56 | /** 57 | * Implementation of {@link ICardCrypto} interface. 58 | */ 59 | readonly cardCrypto: ICardCrypto; 60 | 61 | /** 62 | * Implementation of {@link ICardVerifier} interface to 63 | * be used to verify the validity of Virgil Cards received 64 | * from network and via {@link CardManager.importCard} method. 65 | */ 66 | readonly cardVerifier: ICardVerifier; 67 | 68 | /** 69 | * Alters the behavior of the `CardManager` when it receives an 70 | * HTTP Unauthorized (401) error from the server. If this is set to 71 | * `true` it will silently request a new token form the 72 | * {@link CardManager.accessTokenProvider} and re-send the request, 73 | * otherwise the error will be returned to the client. 74 | */ 75 | readonly retryOnUnauthorized: boolean; 76 | 77 | /** 78 | * Optional {@link ISignCallback}. 79 | */ 80 | readonly signCallback?: ISignCallback; 81 | 82 | /** 83 | * Virgil Services API URL. Optional. 84 | * @hidden 85 | */ 86 | readonly apiUrl?: string; 87 | 88 | /** 89 | * Product information for internal statistics about product usage. Optional. 90 | * @hidden 91 | */ 92 | readonly productInfo?: IProductInfo; 93 | } 94 | 95 | /** 96 | * Class responsible for creating, publishing and retrieving Virgil Cards. 97 | */ 98 | export class CardManager { 99 | public readonly crypto: ICardCrypto; 100 | public readonly client: CardClient; 101 | public readonly modelSigner: ModelSigner; 102 | public readonly signCallback?: ISignCallback; 103 | public readonly cardVerifier: ICardVerifier; 104 | public retryOnUnauthorized: boolean; 105 | 106 | public accessTokenProvider: IAccessTokenProvider; 107 | 108 | public constructor (params: ICardManagerParams) { 109 | this.crypto = params.cardCrypto; 110 | this.client = new CardClient(params.apiUrl, params.productInfo); 111 | this.modelSigner = new ModelSigner(params.cardCrypto); 112 | this.signCallback = params.signCallback; 113 | this.retryOnUnauthorized = params.retryOnUnauthorized; 114 | this.cardVerifier = params.cardVerifier; 115 | this.accessTokenProvider = params.accessTokenProvider || throwingAccessTokenProvider; 116 | } 117 | 118 | /** 119 | * Generates a {@link RawSignedModel} that represents a card from 120 | * `cardParams`. 121 | * Use this method if you don't need to publish the card right away, for 122 | * example if you need to first send it to your backend server to apply 123 | * additional signature. 124 | * 125 | * @param {INewCardParams} cardParams - New card parameters. 126 | * @returns {RawSignedModel} 127 | */ 128 | generateRawCard(cardParams: INewCardParams): RawSignedModel { 129 | const model = generateRawSigned(this.crypto, cardParams); 130 | 131 | this.modelSigner.sign({ 132 | model, 133 | signerPrivateKey: cardParams.privateKey, 134 | signer: SelfSigner, 135 | extraFields: cardParams.extraFields 136 | }); 137 | 138 | return model; 139 | } 140 | 141 | /** 142 | * Generates a card from `cardParams` and publishes it in the Virgil Cards 143 | * Service. 144 | * @param {INewCardParams} cardParams - New card parameters. 145 | * @returns {Promise} 146 | */ 147 | async publishCard(cardParams: INewCardParams) { 148 | validateCardParams(cardParams); 149 | const tokenContext: ITokenContext = { 150 | service: CARDS_SERVICE__NAME, 151 | identity: cardParams.identity, 152 | operation: 'publish' 153 | }; 154 | const token = await this.accessTokenProvider.getToken(tokenContext); 155 | const rawSignedModel = this.generateRawCard( 156 | Object.assign({}, cardParams, { identity: token.identity() }) 157 | ); 158 | 159 | return await this.publishRawSignedModel(rawSignedModel, tokenContext, token); 160 | } 161 | 162 | /** 163 | * Publishes a previously generated card in the form of 164 | * {@link RawSignedModel} object. 165 | * 166 | * @param {RawSignedModel} rawCard - The card to publish. 167 | * @returns {Promise} 168 | */ 169 | async publishRawCard(rawCard: RawSignedModel) { 170 | assert(rawCard != null && rawCard.contentSnapshot != null, '`rawCard` should not be empty'); 171 | const cardDetails = JSON.parse(rawCard.contentSnapshot) as IRawCardContent; 172 | const tokenContext = getCardServiceTokenContext({ identity: cardDetails.identity, operation: 'publish' }); 173 | const token = await this.accessTokenProvider.getToken(tokenContext); 174 | 175 | return this.publishRawSignedModel(rawCard, tokenContext, token); 176 | } 177 | 178 | /** 179 | * Fetches the card by `cardId` from the Virgil Card Service. 180 | * @param {string} cardId - Id of the card to fetch. 181 | * @returns {Promise} 182 | */ 183 | async getCard(cardId: string): Promise { 184 | const tokenContext = getCardServiceTokenContext({ operation: 'get' }); 185 | 186 | const accessToken = await this.accessTokenProvider.getToken(tokenContext); 187 | const cardWithStatus = await this.tryDo(tokenContext, accessToken, 188 | async (token) => await this.client.getCard(cardId, token.toString())); 189 | 190 | const card = parseRawSignedModel( this.crypto, cardWithStatus.cardRaw, cardWithStatus.isOutdated ); 191 | 192 | if (card.id !== cardId) { 193 | throw new VirgilCardVerificationError('Received invalid card'); 194 | } 195 | 196 | this.validateCards([ card ]); 197 | return card; 198 | } 199 | 200 | /** 201 | * Fetches collection of cards with the given `identity` from the Virgil 202 | * Cards Service. 203 | * @param {string|string[]} identities - Identity or an array of identities of the cards to fetch. 204 | * @returns {Promise} 205 | */ 206 | async searchCards (identities: string|string[]): Promise { 207 | if (!identities) throw new TypeError('Argument `identities` is required'); 208 | 209 | const identitiesArr = Array.isArray(identities) ? identities : [identities]; 210 | if (identitiesArr.length === 0) throw new TypeError('Identities array must not be empty'); 211 | 212 | const tokenContext = getCardServiceTokenContext({ operation: 'search' }); 213 | const accessToken = await this.accessTokenProvider.getToken(tokenContext); 214 | const rawCards = await this.tryDo( 215 | tokenContext, 216 | accessToken, 217 | async (token) => await this.client.searchCards(identitiesArr, token.toString()) 218 | ); 219 | 220 | const cards = rawCards.map(raw => parseRawSignedModel(this.crypto, raw, false)); 221 | const identitiesSet = new Set(identitiesArr); 222 | 223 | if (cards.some(c => !identitiesSet.has(c.identity))) { 224 | throw new VirgilCardVerificationError('Received invalid cards'); 225 | } 226 | 227 | this.validateCards(cards); 228 | return linkedCardList(cards); 229 | } 230 | 231 | /** 232 | * Marks the Virgil Card specified by `cardId` as revoked. Revoked cards will have `isOutdated` 233 | * property set to `true` when retrieved via {@link CardManager.getCard} method. 234 | * Also revoked cards will be absent in the {@link CardManager.searchCards} result. 235 | * @param {string} cardId - Id of the card to revoke. 236 | * @returns {Promise} 237 | */ 238 | async revokeCard(cardId: string): Promise { 239 | if (!cardId) throw new TypeError('Argument `cardId` is required'); 240 | 241 | const tokenContext = getCardServiceTokenContext({ operation: 'revoke' }); 242 | 243 | const accessToken = await this.accessTokenProvider.getToken(tokenContext); 244 | await this.tryDo( 245 | tokenContext, 246 | accessToken, 247 | async (token) => await this.client.revokeCard(cardId, token.toString()) 248 | ); 249 | } 250 | 251 | /** 252 | * Converts the card in the form of {@link RawSignedModel} object to the 253 | * {@link ICard} object. 254 | * 255 | * @see {@link CardManager.exportCard} 256 | * 257 | * @param {RawSignedModel} rawCard - The card to convert. 258 | * @returns {ICard} 259 | */ 260 | importCard (rawCard: RawSignedModel): ICard { 261 | const card = parseRawSignedModel( this.crypto, rawCard ); 262 | this.validateCards([ card ]); 263 | return card; 264 | } 265 | 266 | /** 267 | * Converts the card in the base64 string form to the {@link ICard} object. 268 | * 269 | * @see {@link CardManager.exportCardAsString} 270 | * 271 | * @param {string} str - The string in base64. 272 | * @returns {ICard} 273 | */ 274 | importCardFromString (str: string): ICard { 275 | assert(Boolean(str), '`str` should not be empty'); 276 | return this.importCard( RawSignedModel.fromString(str) ); 277 | } 278 | 279 | /** 280 | * Converts the card in the JSON-serializable object form to the 281 | * {@link ICard} object. 282 | * 283 | * @see {@link CardManager.exportCardAsJson} 284 | * 285 | * @param {IRawSignedModelJson} json 286 | * @returns {ICard} 287 | */ 288 | importCardFromJson (json: IRawSignedModelJson): ICard { 289 | assert(Boolean(json), '`json` should not be empty'); 290 | return this.importCard( RawSignedModel.fromJson(json) ); 291 | } 292 | 293 | /** 294 | * Converts the card in the form of {@link ICard} object to the 295 | * {@link RawSignedModel} object. 296 | * 297 | * @see {@link CardManager.importCard} 298 | * 299 | * @param {ICard} card 300 | * @returns {RawSignedModel} 301 | */ 302 | exportCard (card: ICard): RawSignedModel { 303 | return cardToRawSignedModel(card); 304 | } 305 | 306 | /** 307 | * Converts the card in the form of {@link ICard} object to the string 308 | * in base64 encoding. 309 | * 310 | * @see {@link CardManager.importCardFromString} 311 | * 312 | * @param {ICard} card 313 | * @returns {string} 314 | */ 315 | exportCardAsString (card: ICard): string { 316 | return this.exportCard(card).toString(); 317 | } 318 | 319 | /** 320 | * Converts the card in the form of {@link ICard} object to the 321 | * JSON-serializable object form. 322 | * 323 | * @see {@link CardManager.importCardFromJson} 324 | * 325 | * @param {ICard} card 326 | * @returns {IRawSignedModelJson} 327 | */ 328 | exportCardAsJson (card: ICard): IRawSignedModelJson { 329 | return this.exportCard(card).toJson(); 330 | } 331 | 332 | /** 333 | * @hidden 334 | */ 335 | private async publishRawSignedModel (rawCard: RawSignedModel, context: ITokenContext, accessToken: IAccessToken): Promise { 336 | if (this.signCallback != null) { 337 | rawCard = await this.signCallback(rawCard); 338 | } 339 | 340 | const publishedModel = await this.tryDo( 341 | context, 342 | accessToken, 343 | async token => await this.client.publishCard(rawCard, token.toString()) 344 | ); 345 | 346 | if (rawCard.contentSnapshot !== publishedModel.contentSnapshot) { 347 | throw new VirgilCardVerificationError('Received invalid card'); 348 | } 349 | 350 | const card = parseRawSignedModel(this.crypto, publishedModel); 351 | this.validateCards([ card ]); 352 | return card; 353 | } 354 | 355 | /** 356 | * @hidden 357 | */ 358 | private async tryDo (context: ITokenContext, token: IAccessToken, func: (token: IAccessToken) => Promise): Promise { 359 | try { 360 | return await func(token); 361 | } catch (e) { 362 | if ( 363 | e instanceof VirgilHttpError && 364 | e.httpStatus === 401 && 365 | e.errorCode === ErrorCode.AccessTokenExpired && 366 | this.retryOnUnauthorized 367 | ) { 368 | token = await this.accessTokenProvider.getToken(getCardServiceTokenContext({ 369 | identity: context.identity, 370 | operation: context.operation, 371 | forceReload: true 372 | })); 373 | 374 | return await func(token); 375 | } 376 | 377 | throw e; 378 | } 379 | } 380 | 381 | /** 382 | * Delegates to the {@link CardManager.cardVerifier} to verify the validity 383 | * of the `cards`. 384 | * 385 | * @throws {@link VirgilCardVerificationError} if any of the cards is not 386 | * valid. 387 | * 388 | * @param {ICard[]} cards 389 | */ 390 | private validateCards(cards: ICard[]) { 391 | if (this.cardVerifier == null) return; 392 | 393 | for (const card of cards) { 394 | if (!this.cardVerifier.verifyCard(card)) { 395 | throw new VirgilCardVerificationError('Validation errors have been detected'); 396 | } 397 | } 398 | } 399 | } 400 | 401 | /** 402 | * @hidden 403 | */ 404 | function validateCardParams(params: INewCardParams, validateIdentity: boolean = false) { 405 | assert(params != null, 'Card parameters must be provided'); 406 | assert(params.privateKey != null, 'Card\'s private key is required'); 407 | assert(params.publicKey != null, 'Card\'s public key is required'); 408 | if (validateIdentity) { 409 | assert( 410 | typeof params.identity === 'string' && params.identity !== '', 411 | 'Card\'s identity is required' 412 | ); 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /src/Cards/CardUtils.ts: -------------------------------------------------------------------------------- 1 | import { ICardCrypto } from '../types'; 2 | import { IRawSignature, RawSignedModel } from './RawSignedModel'; 3 | import { ICard, ICardSignature, IExtraData, INewCardParams, IRawCardContent } from './ICard'; 4 | import { getUnixTimestamp } from '../Lib/timestamp'; 5 | import { CardIdByteLength, CardVersion } from './constants'; 6 | 7 | /** 8 | * Converts an {@link ICard} to a {@link RawSignedModel}. 9 | * 10 | * @hidden 11 | * 12 | * @param {ICard} card - The {@link ICard} to convert. 13 | * @returns {RawSignedModel} 14 | */ 15 | export function cardToRawSignedModel (card: ICard): RawSignedModel { 16 | return new RawSignedModel(card.contentSnapshot, card.signatures.slice()); 17 | } 18 | 19 | /** 20 | * Generates a {@link RawSignedModel} from the given `params`. 21 | * 22 | * @hidden 23 | * 24 | * @param {ICardCrypto} crypto - Object implementing the {@link ICardCrypto} 25 | * interface. 26 | * @param {INewCardParams} params - New card parameters. 27 | * @returns {RawSignedModel} 28 | */ 29 | export function generateRawSigned (crypto: ICardCrypto, params: INewCardParams): RawSignedModel { 30 | const { identity, publicKey, previousCardId } = params; 31 | const now = getUnixTimestamp(new Date); 32 | 33 | const details = { 34 | identity: identity!, 35 | previous_card_id: previousCardId, 36 | created_at: now, 37 | version: CardVersion, 38 | public_key: crypto.exportPublicKey(publicKey).toString('base64'), 39 | }; 40 | 41 | return new RawSignedModel(JSON.stringify(details), []); 42 | } 43 | 44 | /** 45 | * Converts the {@link RawSignedModel} into the {@link ICard}. 46 | * 47 | * @hidden 48 | * 49 | * @param {ICardCrypto} crypto - Object implementing the {@link ICardCrypto} 50 | * interface. 51 | * @param {RawSignedModel} model - The model to convert. 52 | * @param {boolean} isOutdated - Boolean indicating whether there is a newer 53 | * Virgil Card replacing the one that `model` represents. 54 | * 55 | * @returns {ICard} 56 | */ 57 | export function parseRawSignedModel (crypto: ICardCrypto, model: RawSignedModel, isOutdated = false): ICard { 58 | const content = JSON.parse(model.contentSnapshot) as IRawCardContent; 59 | const signatures = model.signatures.map(rawSignToCardSign); 60 | 61 | return { 62 | id: generateCardId(crypto, model.contentSnapshot), 63 | publicKey: crypto.importPublicKey({ value: content.public_key, encoding: 'base64' }), 64 | contentSnapshot: model.contentSnapshot, 65 | identity: content.identity, 66 | version: content.version, 67 | createdAt: new Date(content.created_at * 1000), 68 | previousCardId: content.previous_card_id, 69 | signatures, 70 | isOutdated 71 | }; 72 | } 73 | 74 | /** 75 | * Given the array of `cards`, returns another array with outdated cards 76 | * filtered out and the `previousCard` properties of the cards that replace 77 | * the outdated ones being populated with appropriate outdated cards. 78 | * i.e. turns this (A is for Actual, O is for Outdated): 79 | * ``` 80 | * A -> O -> A -> A -> O 81 | * ``` 82 | * into this 83 | * ``` 84 | * A -> A -> A 85 | * | | 86 | * O O 87 | * ``` 88 | * 89 | * @hidden 90 | * 91 | * @param {ICard[]} cards - The cards array to transform. 92 | * @returns {ICard[]} - Transformed array. 93 | */ 94 | export function linkedCardList (cards: ICard[]): ICard[] { 95 | const unsorted: { [key: string]: ICard } = Object.create(null); 96 | 97 | for (const card of cards) { 98 | unsorted[card.id] = card; 99 | } 100 | 101 | for (const card of cards) { 102 | if (card.previousCardId == null) continue; 103 | if (unsorted[card.previousCardId] == null) continue; 104 | 105 | unsorted[card.previousCardId].isOutdated = true; 106 | card.previousCard = unsorted[card.previousCardId]; 107 | delete unsorted[card.previousCardId]; 108 | } 109 | 110 | return Object.keys(unsorted).map(key => unsorted[key]); 111 | } 112 | 113 | /** 114 | * Calculates ID for the VirgilCard from the `snapshot` of its contents. 115 | * 116 | * @hidden 117 | * 118 | * @param {ICardCrypto} crypto - Object implementing the {@link ICardCrypto} 119 | * interface. 120 | * @param {string} snapshot - The VirgilCard's contents snapshot. 121 | * @returns {string} - VirgilCard's ID encoded in HEX. 122 | */ 123 | function generateCardId (crypto: ICardCrypto, snapshot: string): string { 124 | const fingerprint = crypto 125 | .generateSha512({ value: snapshot, encoding: 'utf8' }) 126 | .slice(0, CardIdByteLength); 127 | return fingerprint.toString('hex'); 128 | } 129 | 130 | function rawSignToCardSign ({ snapshot, signature, signer }: IRawSignature): ICardSignature { 131 | return { 132 | signer, 133 | signature, 134 | snapshot, 135 | extraFields: tryParseExtraFields(snapshot) 136 | }; 137 | } 138 | 139 | function tryParseExtraFields(snapshot?: string): IExtraData { 140 | if (snapshot) { 141 | try { 142 | return JSON.parse(snapshot) as IExtraData; 143 | } catch (ignored) {} 144 | } 145 | 146 | return {}; 147 | } 148 | -------------------------------------------------------------------------------- /src/Cards/CardVerifier.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from './ICard'; 2 | import { IPublicKey, ICardCrypto } from '../types'; 3 | import { SelfSigner, VirgilSigner } from './constants'; 4 | 5 | /** 6 | * Interface to be implemented by an object capable of verifying the validity 7 | * of Virgil Cards. 8 | */ 9 | export interface ICardVerifier { 10 | /** 11 | * Verifies the validity of the given `card`. 12 | * @param {ICard} card 13 | * @returns {boolean} 14 | */ 15 | verifyCard(card: ICard): boolean; 16 | } 17 | 18 | /** 19 | * Signature identifier and the public key to be used to verify additional 20 | * signatures of the cards, if any. 21 | */ 22 | export interface IVerifierCredentials { 23 | /** 24 | * String identifying the signature to be verified with `publicKeyBase64`. 25 | */ 26 | signer: string; 27 | 28 | /** 29 | * Public key to be used to verify the signature identified by `signer`. 30 | */ 31 | publicKeyBase64: string; 32 | } 33 | 34 | /** 35 | * The {@link VirgilCardVerifier} constructor options. 36 | */ 37 | export interface IVirgilCardVerifierParams { 38 | /** 39 | * Indicates whether the "self" signature of the cards should be verified. 40 | * Self signature is a signature generated with the card's corresponding 41 | * private key. 42 | */ 43 | verifySelfSignature?: boolean; 44 | 45 | /** 46 | * Indicates whether the "virgil" signature of the cards should be 47 | * verified. Virgil signature is appended to the list of card's signatures 48 | * when the card is published. 49 | */ 50 | verifyVirgilSignature?: boolean; 51 | 52 | /** 53 | * Array of arrays of {@link IVerifierCredentials} objects to be used to 54 | * verify additional signatures of the cards, if any. 55 | * 56 | * If whitelists are provided, the card must have at least one signature 57 | * from each whitelist. 58 | */ 59 | whitelists?: IWhitelist[] 60 | } 61 | 62 | export type IWhitelist = IVerifierCredentials[]; 63 | 64 | const DEFAULTS: IVirgilCardVerifierParams = { 65 | verifySelfSignature: true, 66 | verifyVirgilSignature: true, 67 | whitelists: [] 68 | }; 69 | 70 | const VIRGIL_CARDS_PUBKEY_BASE64 = 'MCowBQYDK2VwAyEAljOYGANYiVq1WbvVvoYIKtvZi2ji9bAhxyu6iV/LF8M='; 71 | 72 | /** 73 | * Class responsible for validating cards by verifying their digital 74 | * signatures. 75 | */ 76 | export class VirgilCardVerifier implements ICardVerifier { 77 | /** 78 | * @see {@link IVirgilCardVerifierParams} 79 | */ 80 | public whitelists: IWhitelist[]; 81 | 82 | /** 83 | * @see {@link IVirgilCardVerifierParams} 84 | */ 85 | public verifySelfSignature: boolean; 86 | 87 | /** 88 | * @see {@link IVirgilCardVerifierParams} 89 | */ 90 | public verifyVirgilSignature: boolean; 91 | 92 | /** 93 | * Public key of the Virgil Cards service used for "virgil" signature 94 | * verification. 95 | */ 96 | public readonly virgilCardsPublicKey: IPublicKey; 97 | 98 | /** 99 | * Initializes a new instance of `VirgilCardVerifier`. 100 | * @param {ICardCrypto} crypto - Object implementing the 101 | * {@link ICardCrypto} interface. 102 | * @param {IVirgilCardVerifierParams} options - Initialization options. 103 | */ 104 | public constructor (private readonly crypto: ICardCrypto, options?: IVirgilCardVerifierParams) { 105 | const params = { ...DEFAULTS, ...(options || {})}; 106 | this.verifySelfSignature = params.verifySelfSignature!; 107 | this.verifyVirgilSignature = params.verifyVirgilSignature!; 108 | this.whitelists = params.whitelists!; 109 | this.virgilCardsPublicKey = crypto.importPublicKey({ value: VIRGIL_CARDS_PUBKEY_BASE64, encoding: 'base64' }); 110 | } 111 | 112 | /** 113 | * Verifies the signatures of the `card`. 114 | * @param {ICard} card 115 | * @returns {boolean} `true` if the signatures to be verified are present 116 | * and valid, otherwise `false`. 117 | */ 118 | public verifyCard(card: ICard): boolean { 119 | if (this.selfValidationFailed(card)) { 120 | return false; 121 | } 122 | if (this.virgilValidationFailed(card)) { 123 | return false; 124 | } 125 | if (!this.whitelists || this.whitelists.length === 0) { 126 | return true; 127 | } 128 | const signers = card.signatures.map(s => s.signer); 129 | 130 | for (const whitelist of this.whitelists) { 131 | 132 | if (whitelist == null || whitelist.length === 0) { 133 | return false; 134 | } 135 | 136 | const intersectedCreds = whitelist.filter(x => signers.indexOf(x.signer) !== -1); 137 | 138 | if (intersectedCreds.length === 0) { 139 | return false; 140 | } 141 | 142 | const isValidForSome = intersectedCreds.some(cred => 143 | this.validateSignerSignature( 144 | card, 145 | this.getPublicKey(cred.publicKeyBase64), 146 | cred.signer 147 | ) 148 | ); 149 | 150 | if (!isValidForSome) { 151 | return false; 152 | } 153 | } 154 | 155 | return true; 156 | } 157 | 158 | private selfValidationFailed (card: ICard) { 159 | return this.verifySelfSignature 160 | && !this.validateSignerSignature(card, card.publicKey, SelfSigner) 161 | } 162 | 163 | private virgilValidationFailed (card: ICard) { 164 | return this.verifyVirgilSignature 165 | && !this.validateSignerSignature(card, this.virgilCardsPublicKey, VirgilSigner); 166 | } 167 | 168 | private getPublicKey(signerPublicKeyBase64: string): IPublicKey { 169 | return this.crypto.importPublicKey({ value: signerPublicKeyBase64, encoding: 'base64' }); 170 | } 171 | 172 | private validateSignerSignature(card: ICard, signerPublicKey: IPublicKey, signer: string): boolean { 173 | const signature = card.signatures.find(s => s.signer === signer); 174 | 175 | if (signature == null) return false; 176 | 177 | const extendedSnapshot = signature.snapshot == null 178 | ? card.contentSnapshot 179 | : card.contentSnapshot + signature.snapshot; 180 | 181 | return this.crypto.verifySignature( 182 | { value: extendedSnapshot, encoding: 'utf8' }, 183 | { value: signature.signature, encoding: 'base64' }, 184 | signerPublicKey 185 | ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Cards/ICard.ts: -------------------------------------------------------------------------------- 1 | import { IPrivateKey, IPublicKey } from '../types'; 2 | 3 | export interface IExtraData { 4 | [key: string]: string; 5 | } 6 | 7 | /** 8 | * Object representation of the card's signature. 9 | */ 10 | export interface ICardSignature { 11 | /** 12 | * Signature identifier. 13 | */ 14 | readonly signer: string; 15 | 16 | /** 17 | * The signature as a string in base64 encoding. 18 | */ 19 | readonly signature: string; 20 | 21 | /** 22 | * Snapshot of the signature's associated `extraFields` as a string in 23 | * UTF-8 encoding. 24 | */ 25 | readonly snapshot?: string; 26 | 27 | /** 28 | * Custom attributes associated with the signature. 29 | */ 30 | readonly extraFields?: IExtraData; 31 | } 32 | 33 | /** 34 | * Object representation of the Virgil Card with the properties encoded in 35 | * content snapshot directly accessible. 36 | */ 37 | export interface ICard { 38 | readonly id: string; 39 | readonly identity: string; 40 | 41 | /** 42 | * The {@link IPublicKey} object representing this card's public key, 43 | */ 44 | readonly publicKey: IPublicKey; 45 | 46 | /** 47 | * Card's version. Currently always equal to "5.0" 48 | */ 49 | readonly version: string; 50 | 51 | /** 52 | * The date the card was created at. 53 | */ 54 | readonly createdAt: Date; 55 | 56 | /** 57 | * List of cards signatures. 58 | */ 59 | readonly signatures: ICardSignature[]; 60 | 61 | /** 62 | * Id of the card this card replaces as outdated, if any. 63 | */ 64 | readonly previousCardId?: string; 65 | 66 | /** 67 | * The content snapshot of the card as a string in UTF-8 encoding. 68 | */ 69 | readonly contentSnapshot: string; 70 | 71 | /** 72 | * Indicates whether this card had been replaced by a newer card. 73 | */ 74 | isOutdated: boolean; 75 | 76 | /** 77 | * The card identified by `previousCardId` of this card, if any. 78 | * 79 | * > Note! This is only populated by {@link CardManager.searchCards} 80 | * method. 81 | */ 82 | previousCard?: ICard; 83 | } 84 | 85 | /** 86 | * Properties encoded in the card's content snapshot. 87 | * @hidden 88 | */ 89 | export interface IRawCardContent { 90 | readonly identity: string; 91 | readonly public_key: string; 92 | readonly version: string; 93 | readonly created_at: number; 94 | readonly previous_card_id?: string; 95 | } 96 | 97 | /** 98 | * Parameters required to create new card. 99 | */ 100 | export interface INewCardParams { 101 | 102 | /** 103 | * The card's corresponding private key. 104 | */ 105 | readonly privateKey: IPrivateKey; 106 | 107 | /** 108 | * The card's public key. 109 | */ 110 | readonly publicKey: IPublicKey; 111 | 112 | /** 113 | * The card's identity. 114 | * 115 | * When publishing a card, the value specified in the "sub" property of 116 | * the JWT takes precedence over this value. 117 | * 118 | * @see {@link CardManager.publishCard} 119 | */ 120 | readonly identity?: string; 121 | 122 | /** 123 | * Id of the card the new card is meant to replace. 124 | */ 125 | readonly previousCardId?: string; 126 | 127 | /** 128 | * Custom attributes to be associated with the "self" signature of the 129 | * new card. 130 | */ 131 | readonly extraFields?: IExtraData; 132 | } 133 | -------------------------------------------------------------------------------- /src/Cards/ModelSigner.ts: -------------------------------------------------------------------------------- 1 | import { RawSignedModel } from './RawSignedModel'; 2 | import { IPrivateKey, ICardCrypto } from '../types'; 3 | import { IExtraData } from './ICard'; 4 | import { SelfSigner } from './constants'; 5 | 6 | /** 7 | * Parameters for the card signature generation. 8 | */ 9 | export interface IRawSignParams { 10 | /** 11 | * The card to generate the signature for in the form of 12 | * {@link RawSignedModel} object. 13 | */ 14 | readonly model: RawSignedModel; 15 | 16 | /** 17 | * The private key to use to generate the signature. 18 | */ 19 | readonly signerPrivateKey: IPrivateKey; 20 | 21 | /** 22 | * Custom attributes to associate with the signature. If provided, these 23 | * will also be signed. 24 | */ 25 | readonly extraFields?: IExtraData; 26 | 27 | /** 28 | * Identifier of the signature. Default is "self". 29 | */ 30 | readonly signer?: string; 31 | } 32 | 33 | /** 34 | * @hidden 35 | */ 36 | interface IFinalSignParams { 37 | readonly model: RawSignedModel; 38 | readonly signerPrivateKey: IPrivateKey; 39 | readonly extraSnapshot?: string; 40 | readonly signer: string; 41 | } 42 | 43 | /** 44 | * Class responsible for generating signatures of the cards. 45 | */ 46 | export class ModelSigner { 47 | 48 | /** 49 | * Initializes a new instance of `ModelSigner`. 50 | * @param {ICardCrypto} crypto - Object implementing the 51 | * {@link ICardCrypto} interface. 52 | */ 53 | public constructor (private readonly crypto: ICardCrypto) {} 54 | 55 | /** 56 | * Generates a new signature based on `rawParams`. 57 | * @param {IRawSignParams} rawParams 58 | */ 59 | public sign (rawParams: IRawSignParams) { 60 | const { model, signerPrivateKey, signer, extraSnapshot } = this.prepareParams(rawParams); 61 | 62 | const signedSnapshot = extraSnapshot != null 63 | ? model.contentSnapshot + extraSnapshot 64 | : model.contentSnapshot; 65 | 66 | const signature = this.crypto.generateSignature({ value: signedSnapshot, encoding: 'utf8' }, signerPrivateKey); 67 | 68 | model.signatures.push({ 69 | signer, 70 | signature: signature.toString('base64'), 71 | snapshot: extraSnapshot 72 | }); 73 | } 74 | 75 | private prepareParams ({ model, signerPrivateKey, extraFields, signer }: IRawSignParams): IFinalSignParams { 76 | signer = signer || SelfSigner; 77 | 78 | let extraSnapshot; 79 | if (extraFields != null) { 80 | extraSnapshot = JSON.stringify(extraFields); 81 | } 82 | 83 | const final: IFinalSignParams = { model, signerPrivateKey, signer, extraSnapshot }; 84 | 85 | this.validate(final); 86 | 87 | return final; 88 | } 89 | 90 | private validate ({ model, signerPrivateKey, signer }: IFinalSignParams) { 91 | if (model == null) { 92 | throw new Error("Model is empty"); 93 | } 94 | 95 | if (signerPrivateKey == null) { 96 | throw new Error("`signerPrivateKey` property is mandatory"); 97 | } 98 | 99 | if (model.signatures != null && model.signatures.some(s => s.signer == signer)) { 100 | throw new Error("The model already has this signature."); 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/Cards/RawSignedModel.ts: -------------------------------------------------------------------------------- 1 | import { base64Decode, base64Encode } from '../Lib/base64'; 2 | 3 | /** 4 | * JSON-serializable representation of Virgil Card as it is stored in the 5 | * Virgil Cards Service. 6 | */ 7 | export interface IRawSignedModelJson { 8 | /** 9 | * The signatures of this card's `content_snapshot`. 10 | */ 11 | readonly signatures: IRawSignature[]; 12 | 13 | /** 14 | * The snapshot of this card's contents as a string in base64 encoding. 15 | */ 16 | readonly content_snapshot: string; 17 | } 18 | 19 | /** 20 | * JSON-serializable representation of the Virgil Card's signature as it is 21 | * stored in the Virgil Cards Service.. 22 | */ 23 | export interface IRawSignature { 24 | /** 25 | * The signer identifier. 26 | */ 27 | readonly signer: string; 28 | 29 | /** 30 | * The signature bytes as a string in base64 encoding. 31 | */ 32 | readonly signature: string; 33 | 34 | /** 35 | * The snapshot of additional attributes associated with the signature 36 | * as a string in base64 encoding. 37 | */ 38 | readonly snapshot?: string; 39 | } 40 | 41 | /** 42 | * Intermediate representation of the Virgil Card with `contentSnapshot` 43 | * and `snapshot`s of the signatures in UTF-8. 44 | */ 45 | export class RawSignedModel { 46 | 47 | /** 48 | * Converts the `str` in base64 encoding into a `RawSignedModel` object. 49 | * 50 | * @param {string} str - Base64 string representation of the card as 51 | * returned by {@RawSignedModel.toString} method. 52 | * 53 | * @returns {RawSignedModel} 54 | */ 55 | public static fromString(str: string) { 56 | const jsonStr = base64Decode(str); 57 | let obj; 58 | try { 59 | obj = JSON.parse(jsonStr); 60 | } catch (error) { 61 | throw new Error('The string to be parsed is in invalid format'); 62 | } 63 | return RawSignedModel.fromJson(obj); 64 | } 65 | 66 | /** 67 | * Converts the `json` serializable object into a `RawSignedModel` object. 68 | * @param {IRawSignedModelJson} json - JSON-serializable object returned by 69 | * {@link RawSignedModel.toJson} method. 70 | * @returns {RawSignedModel} 71 | */ 72 | public static fromJson(json: IRawSignedModelJson) { 73 | const contentSnapshotUtf8 = base64Decode(json.content_snapshot); 74 | const signaturesWithUtf8Snapshots = (json.signatures || []).map(({ signer, signature, snapshot }) => { 75 | if (snapshot) { 76 | return { 77 | signer, 78 | signature, 79 | snapshot: base64Decode(snapshot) 80 | } 81 | } 82 | 83 | return { signer, signature }; 84 | }); 85 | return new RawSignedModel(contentSnapshotUtf8, signaturesWithUtf8Snapshots); 86 | } 87 | 88 | /** 89 | * Initializes a new instance of `RawSignedModel`. 90 | * @param {string} contentSnapshot - The content snapshot in UTF-8. 91 | * @param {IRawSignature[]} signatures - The signatures. If signatures 92 | * themselves have snapshots, those must also be in UTF-8. 93 | */ 94 | constructor( 95 | public readonly contentSnapshot: string, 96 | public readonly signatures: IRawSignature[] 97 | ) { } 98 | 99 | /** 100 | * This is to make it work with `JSON.stringify`, calls 101 | * {@link RawSignedModel.toJson} under the hood. 102 | * @returns {IRawSignedModelJson} 103 | */ 104 | toJSON(): IRawSignedModelJson { 105 | return this.toJson(); 106 | } 107 | 108 | /** 109 | * Returns a JSON-serializable representation of this model in the 110 | * format it is stored in the Virgil Cards Service. (i.e. with 111 | * `contentSnapshot` and `snapshot`s of the signatures as base64 encoded 112 | * strings. 113 | * @returns {IRawSignedModelJson} 114 | */ 115 | toJson(): IRawSignedModelJson { 116 | return { 117 | content_snapshot: base64Encode(this.contentSnapshot), 118 | signatures: this.signatures.map(({ signer, signature, snapshot }) => { 119 | if (snapshot) { 120 | return { 121 | signer, 122 | signature, 123 | snapshot: base64Encode(snapshot) 124 | } 125 | } 126 | return { signer, signature }; 127 | }) 128 | }; 129 | } 130 | 131 | /** 132 | * Serializes this model to string in base64 encoding. 133 | * @returns {string} 134 | */ 135 | toString() { 136 | return base64Encode(JSON.stringify(this)); 137 | } 138 | 139 | /** 140 | * Same as {@link RawSignedModel.toJson}. Please use that instead. 141 | * @returns {IRawSignedModelJson} 142 | */ 143 | exportAsJson() { 144 | return this.toJson(); 145 | } 146 | 147 | /** 148 | * Same as {@link RawSignedModel.toString}. Please use that instead. 149 | * @returns {string} 150 | */ 151 | exportAsString() { 152 | return this.toString(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Cards/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @hidden 3 | */ 4 | export const SelfSigner = "self"; 5 | 6 | /** 7 | * @hidden 8 | */ 9 | export const VirgilSigner = "virgil"; 10 | 11 | /** 12 | * @hidden 13 | */ 14 | export const CardVersion = '5.0'; 15 | 16 | /** 17 | * @hidden 18 | */ 19 | export const CardIdByteLength = 32; 20 | -------------------------------------------------------------------------------- /src/Cards/errors.ts: -------------------------------------------------------------------------------- 1 | import { VirgilError } from '../VirgilError'; 2 | 3 | /** 4 | * Error thrown by {@link CardManager} instances when the card received from 5 | * the network (or imported from string\json) fails verification. 6 | */ 7 | export class VirgilCardVerificationError extends VirgilError { 8 | constructor(m: string) { 9 | super(m, 'CardVerificationError', VirgilCardVerificationError); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Client/CardClient.ts: -------------------------------------------------------------------------------- 1 | import { Connection, IConnection, IProductInfo} from './Connection'; 2 | import { RawSignedModel } from '../Cards/RawSignedModel'; 3 | import { generateErrorFromResponse } from './errors'; 4 | 5 | const PublishEndpoint = '/card/v5'; 6 | const SearchEndpoint = '/card/v5/actions/search'; 7 | const GetCardEndpoint = (cardId: string) => `/card/v5/${cardId}`; 8 | const RevokeCardEndpoint = (cardId: string) => `/card/v5/actions/revoke/${cardId}`; 9 | 10 | /** 11 | * @hidden 12 | */ 13 | export interface ICardResult { 14 | readonly cardRaw: RawSignedModel; 15 | readonly isOutdated: boolean; 16 | } 17 | 18 | /** 19 | * Class responsible for sending requests to the Virgil Cards Service. 20 | * 21 | * @hidden 22 | */ 23 | export class CardClient { 24 | private readonly connection: IConnection; 25 | 26 | /** 27 | * Initializes new instance of `CardClient`. 28 | * @param {IConnection | string} connection - Object implementing the 29 | * {@link IConnection} interface. 30 | */ 31 | public constructor (connection?: IConnection|string, productInfo?: IProductInfo) { 32 | if (typeof connection === 'string') { 33 | this.connection = new Connection(connection, productInfo) 34 | } else if (connection) { 35 | this.connection = connection; 36 | } else { 37 | this.connection = new Connection('https://api.virgilsecurity.com', productInfo); 38 | } 39 | } 40 | 41 | /** 42 | * Issues a request to search cards by the `identity`. 43 | * @param {string[]} identities - Array of identities to search for. 44 | * @param {string} accessToken - A token to authenticate the request. 45 | * @returns {Promise} 46 | */ 47 | public async searchCards (identities: string[], accessToken: string): Promise { 48 | const response = await this.connection.post( SearchEndpoint, accessToken, { identities } ); 49 | if (!response.ok) throw await generateErrorFromResponse(response); 50 | 51 | const cardsJson = await response.json(); 52 | if (cardsJson === null) return []; 53 | 54 | return cardsJson.map(RawSignedModel.fromJson); 55 | } 56 | 57 | /** 58 | * Issues a request to get the card by id. 59 | * @param {string} cardId - Id of the card to fetch. 60 | * @param {string} accessToken - A token to authenticate the request. 61 | * @returns {Promise} 62 | */ 63 | public async getCard (cardId: string, accessToken: string): Promise { 64 | if (!cardId) throw new TypeError('`cardId` should not be empty'); 65 | if (!accessToken) throw new TypeError('`accessToken` should not be empty'); 66 | 67 | const response = await this.connection.get( GetCardEndpoint(cardId), accessToken ); 68 | if (!response.ok) { 69 | throw await generateErrorFromResponse(response); 70 | } 71 | 72 | const isOutdated = response.headers.get('X-Virgil-Is-Superseeded') === 'true'; 73 | 74 | const cardJson = await response.json(); 75 | const cardRaw = RawSignedModel.fromJson(cardJson); 76 | 77 | return { cardRaw, isOutdated }; 78 | } 79 | 80 | /** 81 | * Issues a request to publish the card. 82 | * @param {RawSignedModel} model - Card to publish. 83 | * @param {string} accessToken - A token to authenticate the request. 84 | * @returns {Promise} 85 | */ 86 | public async publishCard (model: RawSignedModel, accessToken: string): Promise { 87 | if (!model) throw new TypeError('`model` should not be empty'); 88 | if (!accessToken) throw new TypeError('`accessToken` should not be empty'); 89 | 90 | const response = await this.connection.post( PublishEndpoint, accessToken, model ); 91 | if (!response.ok) { 92 | throw await generateErrorFromResponse(response); 93 | } 94 | 95 | const cardJson = await response.json(); 96 | return RawSignedModel.fromJson(cardJson); 97 | } 98 | 99 | public async revokeCard (cardId: string, accessToken: string): Promise { 100 | if (!cardId) throw new TypeError('`cardId` should not be empty'); 101 | if (!accessToken) throw new TypeError('`accessToken` should not be empty'); 102 | 103 | const response = await this.connection.post(RevokeCardEndpoint(cardId), accessToken); 104 | if (!response.ok) { 105 | throw await generateErrorFromResponse(response); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Client/Connection.ts: -------------------------------------------------------------------------------- 1 | import { fetch, Headers } from '../Lib/fetch'; 2 | import { VirgilAgent } from './VirgilAgent'; 3 | 4 | /** 5 | * Information about product for SDK usage statistic. 6 | */ 7 | export type IProductInfo = { 8 | product: string; 9 | version: string 10 | } 11 | 12 | /** 13 | * Interface to be implemented by objects capable of making HTTP requests. 14 | * @hidden 15 | */ 16 | export interface IConnection { 17 | get (endpoint: string, accessToken: string): Promise; 18 | post (endpoint: string, accessToken: string, data?: object): Promise; 19 | } 20 | 21 | /** 22 | * Class responsible for making HTTP requests. 23 | * @hidden 24 | */ 25 | export class Connection implements IConnection { 26 | private virgilAgentValue: string 27 | /** 28 | * Initializes a new instance of `Connection`. 29 | * @param {string} prefix - `prefix` will be prepended to the `endpoint` 30 | * argument of request methods. 31 | * @param {VirgilAgentValue} [virgilAgentValue] - optional instance of VirgilAgent for products that wraps 32 | * Virgil SDK 33 | */ 34 | public constructor ( 35 | private readonly prefix: string, 36 | info?: IProductInfo 37 | ) { 38 | if (!info) info = { product: 'sdk', version: process.env.VERSION! } 39 | this.virgilAgentValue = new VirgilAgent(info.product, info.version).value; 40 | } 41 | 42 | /** 43 | * Issues a GET request against the `endpoint`. 44 | * @param {string} endpoint - Endpoint URL relative to the `prefix`. 45 | * @param {string} accessToken - Token to authenticate the request. 46 | * @returns {Promise} 47 | */ 48 | public get (endpoint: string, accessToken: string): Promise { 49 | const headers = this.createHeaders(accessToken); 50 | return this.send(endpoint, 'GET', { headers }); 51 | } 52 | 53 | /** 54 | * Issues a POST request against the `endpoint` sending the `data` as JSON. 55 | * @param {string} endpoint - Endpoint URL relative to the `prefix`. 56 | * @param {string} accessToken - Token to authenticate the request. 57 | * @param {object} data - Response body. 58 | * @returns {Promise} 59 | */ 60 | public post (endpoint: string, accessToken: string, data: object = {}): Promise { 61 | const headers = this.createHeaders(accessToken); 62 | headers.set('Content-Type', 'application/json'); 63 | return this.send(endpoint, 'POST', { 64 | headers: headers, 65 | body: JSON.stringify( data ) 66 | }); 67 | } 68 | 69 | private send (endpoint: string, method: string, params: object): Promise { 70 | return fetch(this.prefix + endpoint, { method, ...params }); 71 | } 72 | 73 | private createHeaders (accessToken: string) { 74 | const headers = new Headers(); 75 | headers.set('Authorization', `Virgil ${accessToken}`); 76 | headers.set('Virgil-Agent', this.virgilAgentValue); 77 | return headers; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Client/VirgilAgent.ts: -------------------------------------------------------------------------------- 1 | import { OS_LIST } from '../Lib/platform/osList'; 2 | import { BROWSER_LIST } from '../Lib/platform/browserList'; 3 | 4 | /** 5 | * Class responsible for tracking which Virgil SDK is being used to make requests, its version, 6 | * browser and platform. 7 | */ 8 | export class VirgilAgent { 9 | 10 | value: string; 11 | private userAgent: string; 12 | /** 13 | * Returns navigator.userAgent string or '' if it's not defined. 14 | * @return {string} user agent string 15 | */ 16 | 17 | private getUserAgent(): string { 18 | if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") { 19 | return navigator.userAgent; 20 | } else { 21 | return ""; 22 | } 23 | } 24 | 25 | /** 26 | * Initializes a new instance of `VirgilAgent`. 27 | * @param {string} product - name of product eg (sdk, brainkey, bpp, keyknox, ratchet, e3kit, purekit) 28 | * argument of request methods. 29 | * @param {string} version - version of the product. 30 | * @param {string} [userAgent] - string with device user agent. Optional 31 | */ 32 | constructor( 33 | product: string, 34 | version: string, 35 | userAgent?: string 36 | ) { 37 | this.userAgent = userAgent || this.getUserAgent(); 38 | this.value = `${product};js;${this.getHeaderValue()};${version}`; 39 | }; 40 | 41 | /** 42 | * Detects device OS 43 | * @returns {string} returns OS if detected or 'other'. 44 | */ 45 | getOsName() { 46 | const os = OS_LIST.find((os) => os.test.some(condition => condition.test(this.userAgent))); 47 | 48 | return os ? os.name : 'other'; 49 | } 50 | 51 | /** 52 | * Detects device browser 53 | * @returns {string} returns browser if detected of 'other'. 54 | */ 55 | getBrowser() { 56 | const browser = BROWSER_LIST.find((browser) => 57 | browser.test.some(condition => condition.test(this.userAgent)) 58 | ); 59 | 60 | return browser ? browser.name : 'other'; 61 | } 62 | 63 | /** 64 | * Detect React Native. 65 | * @return {boolean} true if detects ReactNative . 66 | */ 67 | private isReactNative(): boolean { 68 | return typeof navigator === "object" && navigator.product === "ReactNative"; 69 | } 70 | 71 | /** 72 | * Detect Cordova / PhoneGap / Ionic frameworks on a mobile device. 73 | * @return {boolean} true if detects Ionic. 74 | */ 75 | private isIonic = (): boolean => 76 | typeof window !== "undefined" && 77 | !!("cordova" in window || "phonegap" in window || "PhoneGap" in window) && 78 | /android|ios|iphone|ipod|ipad|iemobile/i.test(this.userAgent); 79 | 80 | /** 81 | * Return information for `virgil-agent` header. 82 | * @return {string} string in format: PRODUCT;FAMILY;PLATFORM;VERSION 83 | */ 84 | private getHeaderValue() { 85 | try { 86 | if (this.isReactNative()) return "ReactNative"; 87 | if (this.isIonic()) return `Ionic/${this.getOsName()}`; 88 | if (!process.browser && typeof global !== 'undefined') { 89 | const majorVersion = process.version.replace(/\.\d+\.\d+$/, '').replace('v', ''); 90 | return `Node${majorVersion}/${process.platform}`; 91 | } 92 | return `${this.getBrowser()}/${this.getOsName()}`; 93 | } catch (e) { 94 | return `Unknown`; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Client/errors.ts: -------------------------------------------------------------------------------- 1 | import { VirgilError } from '../VirgilError'; 2 | 3 | export enum ErrorCode { 4 | AccessTokenExpired = 20304, 5 | Unknown = 0, 6 | } 7 | 8 | /** 9 | * @hidden 10 | */ 11 | export interface IHttpErrorResponseBody { 12 | message: string; 13 | code: number; 14 | } 15 | 16 | /** 17 | * Error thrown by {@link CardManager} when request to the Virgil Cards Service 18 | * fails. 19 | */ 20 | export class VirgilHttpError extends VirgilError { 21 | httpStatus: number; 22 | errorCode: ErrorCode; 23 | 24 | constructor(message: string, status: number, errorCode: number) { 25 | super(message, 'VirgilHttpError', VirgilHttpError); 26 | this.httpStatus = status; 27 | this.errorCode = errorCode as ErrorCode; 28 | } 29 | } 30 | 31 | /** 32 | * Generates error object from response object with HTTP status >= 400 33 | * 34 | * @hidden 35 | * 36 | * @param {Response} response 37 | * @returns {Promise} 38 | */ 39 | export async function generateErrorFromResponse(response: Response) { 40 | if (response.status >= 400 && response.status < 500) { 41 | const reason = await response.json() as IHttpErrorResponseBody; 42 | return new VirgilHttpError(reason.message, response.status, reason.code); 43 | } else { 44 | return new VirgilHttpError(response.statusText, response.status, 0); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Lib/assert.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Test if `condition` is truthy. If it is not, an `Error` is thrown with a 3 | * `message` property equal to `message` parameter. 4 | * @hidden 5 | * @param {boolean} condition 6 | * @param {string} message 7 | */ 8 | export function assert(condition: boolean, message: string) { 9 | if (!condition) { 10 | throw new Error(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Lib/base64.ts: -------------------------------------------------------------------------------- 1 | import base64 from 'base-64'; 2 | 3 | /** 4 | * Decodes the base64 encoded string into a `string`. 5 | * @hidden 6 | * @param {string} input 7 | * @returns {string} 8 | */ 9 | export function base64Decode(input: string): string { 10 | return base64.decode(input); 11 | } 12 | 13 | /** 14 | * Encodes the `input` string into a base64 `string`. 15 | * @hidden 16 | * @param {string} input 17 | * @returns {string} 18 | */ 19 | export function base64Encode(input: string): string { 20 | return base64.encode(input); 21 | } 22 | 23 | /** 24 | * Converts regular base64 encoded string to URL-safe base64 encoded string. 25 | * @hidden 26 | * @param {string} input - Regular base64 encoded string. 27 | * @returns {string} - URL-safe base64 encoded string. 28 | */ 29 | export function base64UrlFromBase64 (input: string) { 30 | input = input.split('=')[0]; 31 | input = input.replace(/\+/g, '-').replace(/\//g, '_'); 32 | return input; 33 | } 34 | 35 | /** 36 | * Converts URL-safe base64 encoded string to regular base64 encoded string. 37 | * @hidden 38 | * @param {string} input - URL-safe base64 encoded string. 39 | * @returns {string} - Regular base64 encoded string. 40 | */ 41 | export function base64UrlToBase64 (input: string) { 42 | input = input.replace(/-/g, '+').replace(/_/g, '/'); 43 | switch (input.length % 4) { 44 | case 0: break; // no padding needed 45 | case 2: 46 | input = input + '=='; 47 | break; 48 | case 3: 49 | input = input + '='; 50 | break; 51 | default: 52 | throw new Error('Invalid base64 string'); 53 | } 54 | return input; 55 | } 56 | 57 | /** 58 | * * Encodes the `input` string into a string using URL-safe base64 encoding. 59 | * 60 | * @hidden 61 | * 62 | * @param {string} input - The input. 63 | * @returns {string} 64 | */ 65 | export function base64UrlEncode (input: string) { 66 | let output = base64Encode(input); 67 | return base64UrlFromBase64(output); 68 | } 69 | 70 | 71 | /** 72 | * Decodes the URL-safe base64-encoded `input` string into a `string`. 73 | * 74 | * @hidden 75 | * 76 | * @param {string} input 77 | * @returns {string} 78 | */ 79 | export function base64UrlDecode (input: string): string { 80 | const str = base64UrlToBase64(input); 81 | return base64Decode(str); 82 | } 83 | -------------------------------------------------------------------------------- /src/Lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import fetchPonyfill from 'fetch-ponyfill'; 2 | const { fetch, Request, Response, Headers } = fetchPonyfill(); 3 | 4 | /** 5 | * @hidden 6 | */ 7 | export { fetch, Request, Response, Headers }; 8 | -------------------------------------------------------------------------------- /src/Lib/platform/browserList.ts: -------------------------------------------------------------------------------- 1 | // Order of browsers matters! Edge, Opera and Chromium have Chrome in User Agent. 2 | 3 | export const BROWSER_LIST = [ 4 | { 5 | test: [/googlebot/i], 6 | name: 'Googlebot' 7 | }, 8 | { 9 | test: [/opera/i, /opr\/|opios/i], 10 | name: 'Opera', 11 | }, 12 | { 13 | test: [/msie|trident/i], 14 | name: 'Internet Explorer', 15 | }, 16 | { 17 | test: [/\sedg/i], 18 | name: 'Microsoft Edge' 19 | }, 20 | { 21 | test: [/firefox|iceweasel|fxios/i], 22 | name: 'Firefox', 23 | }, 24 | { 25 | test: [/chromium/i], 26 | name: 'Chromium' 27 | }, 28 | { 29 | test: [/chrome|crios|crmo/i], 30 | name: 'Chrome', 31 | }, 32 | { 33 | test: [/android/i], 34 | name: 'Android Browser' 35 | }, 36 | { 37 | test: [/playstation 4/i], 38 | name: 'PlayStation 4', 39 | }, 40 | { 41 | test: [/safari|applewebkit/i], 42 | name: 'Safari', 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /src/Lib/platform/osList.ts: -------------------------------------------------------------------------------- 1 | export const OS_LIST = [ 2 | /* Windows Phone */ 3 | { 4 | name: 'Windows Phone', 5 | test: [/windows phone/i], 6 | }, 7 | 8 | /* Windows */ 9 | { 10 | test: [/windows/i], 11 | name: 'Windows' 12 | }, 13 | 14 | /* macOS */ 15 | { 16 | test: [/macintosh/i], 17 | name: 'macOS' 18 | }, 19 | 20 | /* iOS */ 21 | { 22 | test: [/(ipod|iphone|ipad)/i], 23 | name: 'iOS' 24 | }, 25 | 26 | /* Android */ 27 | { 28 | test: [/android/i], 29 | name: 'Android', 30 | }, 31 | 32 | /* Linux */ 33 | { 34 | test: [/linux/i], 35 | name: 'Linux', 36 | }, 37 | 38 | /* Chrome OS */ 39 | { 40 | test: [/CrOS/], 41 | name: 'Chrome OS' 42 | }, 43 | 44 | /* Playstation 4 */ 45 | { 46 | test: [/PlayStation 4/], 47 | name: 'PlayStation 4', 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /src/Lib/timestamp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts javascript date object or timestamp in milliseconds 3 | * to Unix timestamp. 4 | * 5 | * @hidden 6 | * 7 | * @param {Date | number} date - The date or timestamp to convert. 8 | * @returns {number} 9 | */ 10 | export function getUnixTimestamp(date: Date|number): number { 11 | let time; 12 | if (typeof date === 'number') { 13 | time = date; 14 | } else { 15 | time = date.getTime(); 16 | } 17 | return Math.floor(time / 1000); 18 | } 19 | 20 | /** 21 | * Adds the given number of seconds to the given date. 22 | * 23 | * @hidden 24 | * 25 | * @param {Date | number} date - The date to add seconds to. 26 | * If `date` is a `number` it is treated as a timestamp in milliseconds. 27 | * @param {number} seconds - The number of seconds to add. 28 | * @returns {Date} - The new date. 29 | */ 30 | export function addSeconds (date: Date|number, seconds: number): Date { 31 | if (typeof date === 'number') { 32 | return new Date(date + seconds * 1000); 33 | } 34 | 35 | return new Date(date.getTime() + seconds * 1000); 36 | } 37 | -------------------------------------------------------------------------------- /src/Storage/KeyEntryStorage/IKeyEntryStorage.ts: -------------------------------------------------------------------------------- 1 | import { IStorageAdapter } from '../adapters/IStorageAdapter'; 2 | 3 | /** 4 | * User-defined metadata stored with {@link IKeyEntry}. 5 | */ 6 | export type KeyEntryMeta = { [key: string]: string }; 7 | 8 | /** 9 | * Object representing {@link KeyEntryStorage} configuration options. 10 | * This combines the {@link IStorageAdapterConfig} with the ability to 11 | * specify the adapter itself. All three properties are mutually exclusive. 12 | */ 13 | export interface IKeyEntryStorageConfig { 14 | /** 15 | * Node.js only. File system path to the folder where the entries will be 16 | * persisted. If it's a relative path, it is resolved using 17 | * {@link `path.resolve`|https://nodejs.org/dist/latest/docs/api/path.html#path_path_resolve_paths}. 18 | */ 19 | dir?: string; 20 | 21 | /** 22 | * Browser only. Name of the IndexedDB database where the entries will be 23 | * persisted. 24 | */ 25 | name?: string; 26 | 27 | /** 28 | * Object implementing the `IStorageAdapter` interface. 29 | */ 30 | adapter?: IStorageAdapter; 31 | } 32 | 33 | /** 34 | * Interface of a single entry in {@link IKeyEntryStorage}. 35 | */ 36 | export interface IKeyEntry { 37 | name: string; 38 | value: string; 39 | meta?: KeyEntryMeta; 40 | creationDate: Date; 41 | modificationDate: Date; 42 | } 43 | 44 | /** 45 | * Parameters of {@link KeyEntryStorage.save} method. 46 | */ 47 | export interface ISaveKeyEntryParams { 48 | name: string; 49 | value: string; 50 | meta?: KeyEntryMeta; 51 | } 52 | 53 | /** 54 | * Parameters of {@link KeyEntryStorage.update} method. 55 | * Although both `value` and `meta` are marked as optional, at least one of 56 | * them must be provided, or else a `TypeError` will be thrown. 57 | */ 58 | export interface IUpdateKeyEntryParams { 59 | /** 60 | * Name of the key entry to update. 61 | * > Note! The name itself cannot be changed. 62 | */ 63 | name: string; 64 | 65 | /** 66 | * New value of the key entry. Optional. If ommited, the original 67 | * value remains intact. 68 | */ 69 | value?: string; 70 | 71 | /** 72 | * New metadata of the key entry. Optional. If provided, overwrites 73 | * the previous metadata completely. If ommited, the original metadata 74 | * remains intact. 75 | */ 76 | meta?: KeyEntryMeta; 77 | } 78 | 79 | /** 80 | * Interface to be implemented by object capable of persisting 81 | * private keys data and metadata. 82 | */ 83 | export interface IKeyEntryStorage { 84 | /** 85 | * Saves key entry represented by `params`. 86 | * @param {ISaveKeyEntryParams} params - New key entry parameters. 87 | * @returns {Promise} Promise resolved with stored entry, 88 | * or rejected with {@link PrivateKeyExistsError} error in case entry 89 | * with the same name already exists. 90 | */ 91 | save (params: ISaveKeyEntryParams): Promise; 92 | 93 | /** 94 | * Retrieves key entry by name. 95 | * @param {string} name - Name of the key entry to retrieve. 96 | * @returns {Promise} Promise resolved with stored entry 97 | * or `null` if entry with the given name does not exist. 98 | */ 99 | load (name: string): Promise; 100 | 101 | /** 102 | * Checks if key entry exists by name. 103 | * @param {string} name - Name of the entry to check. 104 | * @returns {Promise} - Promise resolved with `true` 105 | * if key entry exists, or `false` otherwise. 106 | */ 107 | exists (name: string): Promise; 108 | 109 | /** 110 | * Removes key entry by name. 111 | * @param {string} name - Name of the key entry to remove. 112 | * @returns {Promise} - Promise resolved with `true` 113 | * if key entry was removed, or `false` otherwise. 114 | */ 115 | remove (name: string): Promise; 116 | 117 | /** 118 | * Retrieves a collection of all stored entries. 119 | * @returns {Promise} - Promise resolved with array 120 | * of all stored entries. 121 | */ 122 | list (): Promise; 123 | 124 | /** 125 | * Updates the value or metadata (or both) of the key entry identified 126 | * by `params.name`. 127 | * @param {IUpdateKeyEntryParams} params - Paremeters identifying the 128 | * entry to update and new value or metadata (or both). 129 | */ 130 | update (params: IUpdateKeyEntryParams): Promise; 131 | 132 | /** 133 | * Removes all of the stored entries. 134 | * @returns {Promise} 135 | */ 136 | clear (): Promise; 137 | } 138 | -------------------------------------------------------------------------------- /src/Storage/KeyEntryStorage/KeyEntryStorage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IKeyEntry, 3 | IKeyEntryStorage, 4 | IKeyEntryStorageConfig, 5 | ISaveKeyEntryParams, 6 | IUpdateKeyEntryParams, 7 | } from './IKeyEntryStorage'; 8 | import { DefaultStorageAdapter } from '../adapters/DefaultStorageAdapter'; 9 | import { IStorageAdapter, IStorageAdapterConfig } from '../adapters/IStorageAdapter'; 10 | import { InvalidKeyEntryError, KeyEntryAlreadyExistsError, KeyEntryDoesNotExistError } from './errors'; 11 | 12 | const DEFAULTS: IStorageAdapterConfig = { 13 | dir: '.virgil_key_entries', 14 | name: 'VirgilKeyEntries' 15 | }; 16 | 17 | const VALUE_KEY = 'value'; 18 | const CREATION_DATE_KEY = 'creationDate'; 19 | const MODIFICATION_DATE_KEY = 'modificationDate'; 20 | 21 | export { IKeyEntry, IKeyEntryStorage, IKeyEntryStorageConfig, ISaveKeyEntryParams, IUpdateKeyEntryParams }; 22 | 23 | /** 24 | * Class responsible for persisting private key bytes with optional 25 | * user-defined metadata. 26 | */ 27 | export class KeyEntryStorage implements IKeyEntryStorage { 28 | private adapter: IStorageAdapter; 29 | 30 | /** 31 | * Initializes a new instance of `KeyEntryStorage`. 32 | * 33 | * @param {IKeyEntryStorageConfig} config - Instance configuration. 34 | */ 35 | constructor (config: IKeyEntryStorageConfig | string = {}) { 36 | this.adapter = resolveAdapter(config); 37 | } 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | exists(name: string): Promise { 43 | validateName(name); 44 | return this.adapter.exists(name); 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | load(name: string): Promise { 51 | validateName(name); 52 | return this.adapter.load(name).then(data => { 53 | if (data == null) { 54 | return null; 55 | } 56 | return deserializeKeyEntry(data); 57 | }); 58 | } 59 | 60 | /** 61 | * @inheritDoc 62 | */ 63 | remove(name: string): Promise { 64 | validateName(name); 65 | return this.adapter.remove(name); 66 | } 67 | 68 | /** 69 | * @inheritDoc 70 | */ 71 | save({ name, value, meta }: ISaveKeyEntryParams): Promise { 72 | validateNameProperty(name); 73 | validateValueProperty(value); 74 | 75 | const keyEntry = { 76 | name: name, 77 | value: value, 78 | meta: meta, 79 | creationDate: new Date(), 80 | modificationDate: new Date() 81 | }; 82 | 83 | return this.adapter.store(name, serializeKeyEntry(keyEntry)) 84 | .then(() => keyEntry) 85 | .catch(error => { 86 | if (error && error.name === 'StorageEntryAlreadyExistsError') { 87 | throw new KeyEntryAlreadyExistsError(name); 88 | } 89 | 90 | throw error; 91 | }); 92 | } 93 | 94 | /** 95 | * @inheritDoc 96 | */ 97 | list (): Promise { 98 | return this.adapter.list() 99 | .then(entries => entries.map(entry => deserializeKeyEntry(entry))); 100 | } 101 | 102 | /** 103 | * @inheritDoc 104 | */ 105 | update ({ name, value, meta }: IUpdateKeyEntryParams): Promise { 106 | validateNameProperty(name); 107 | if (!(value || meta)) { 108 | throw new TypeError( 109 | 'Invalid argument. Either `value` or `meta` property is required.' 110 | ); 111 | } 112 | 113 | return this.adapter.load(name) 114 | .then(data => { 115 | if (data === null) { 116 | throw new KeyEntryDoesNotExistError(name) 117 | } 118 | 119 | const entry = deserializeKeyEntry(data); 120 | const updatedEntry = Object.assign(entry,{ 121 | value: value || entry.value, 122 | meta: meta || entry.meta, 123 | modificationDate: new Date() 124 | }); 125 | return this.adapter.update(name, serializeKeyEntry(updatedEntry)) 126 | .then(() => updatedEntry); 127 | }); 128 | } 129 | 130 | /** 131 | * @inheritDoc 132 | */ 133 | clear () { 134 | return this.adapter.clear(); 135 | } 136 | } 137 | 138 | function serializeKeyEntry (keyEntry: IKeyEntry) { 139 | return JSON.stringify(keyEntry); 140 | } 141 | 142 | function deserializeKeyEntry (data: string): IKeyEntry { 143 | try { 144 | return JSON.parse( 145 | data, 146 | (key, value) => { 147 | if (key === CREATION_DATE_KEY || key === MODIFICATION_DATE_KEY) { 148 | return new Date(value); 149 | } 150 | return value; 151 | } 152 | ); 153 | } catch (error) { 154 | throw new InvalidKeyEntryError(); 155 | } 156 | } 157 | 158 | function resolveAdapter (config: IKeyEntryStorageConfig|string) { 159 | if (typeof config === 'string') { 160 | return new DefaultStorageAdapter({ dir: config, name: config }); 161 | } 162 | 163 | const { adapter, ...rest } = config; 164 | if (adapter != null) { 165 | return adapter; 166 | } 167 | 168 | return new DefaultStorageAdapter({ ...DEFAULTS, ...rest }); 169 | } 170 | 171 | const requiredArg = (name: string) => (value: any) => { 172 | if (!value) throw new TypeError(`Argument '${name}' is required.`); 173 | }; 174 | const requiredProp = (name: string) => (value: any) => { 175 | if (!value) throw new TypeError(`Invalid argument. Property ${name} is required`) 176 | }; 177 | 178 | const validateName = requiredArg('name'); 179 | const validateNameProperty = requiredProp('name'); 180 | const validateValueProperty = requiredProp('value'); 181 | -------------------------------------------------------------------------------- /src/Storage/KeyEntryStorage/errors.ts: -------------------------------------------------------------------------------- 1 | import { VirgilError } from '../../VirgilError'; 2 | 3 | /** 4 | * Error thrown when the value loaded from persistent storage cannot be 5 | * parsed as a {@link IKeyEntry} object. 6 | */ 7 | export class InvalidKeyEntryError extends VirgilError { 8 | constructor(message: string = 'Loaded key entry was in invalid format.') { 9 | super(message, 'InvalidKeyEntryError', InvalidKeyEntryError); 10 | } 11 | } 12 | 13 | /** 14 | * Error thrown from {@link KeyEntryStorage.save} method when saving a 15 | * a key entry with the name that already exists in store. 16 | */ 17 | export class KeyEntryAlreadyExistsError extends VirgilError { 18 | constructor(name?: string) { 19 | super( 20 | `Key entry ${ name ? 'named ' + name : 'with same name' }already exists`, 21 | 'KeyEntryAlreadyExistsError', 22 | KeyEntryAlreadyExistsError 23 | ); 24 | } 25 | } 26 | 27 | /** 28 | * Error thrown from {@link KeyEntryStorage.update} method when updating 29 | * a key entry that doesn't exist in store. 30 | */ 31 | export class KeyEntryDoesNotExistError extends VirgilError { 32 | constructor(name: string) { 33 | super( 34 | `Key entry ${ name ? 'named ' + name : 'with the given name'} does not exist.`, 35 | 'KeyEntryDoesNotExistError', 36 | KeyEntryDoesNotExistError 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Storage/KeyStorage.ts: -------------------------------------------------------------------------------- 1 | import StorageAdapter from './adapters/FileSystemStorageAdapter'; 2 | import { IStorageAdapter, IStorageAdapterConfig } from './adapters/IStorageAdapter'; 3 | import { PrivateKeyExistsError } from './errors'; 4 | 5 | export interface IKeyStorageConfig { 6 | /** 7 | * Path to folder where to store the data (Node.js only). 8 | */ 9 | dir?: string; 10 | 11 | /** 12 | * Name of the IndexedDB database where to store the data (Browsers only). 13 | */ 14 | name?: string; 15 | 16 | /** 17 | * The {@link IStorageAdapter} implementation to be used as a storage backend. 18 | */ 19 | adapter?: IStorageAdapter; 20 | } 21 | 22 | const DEFAULTS: IStorageAdapterConfig = { 23 | dir: '.virgil_keys', 24 | name: 'VirgilKeys' 25 | }; 26 | 27 | /** 28 | * Class representing a storage container for private key data. 29 | * Use this class if you need to load the keys stored with 30 | * version 4.x of this library. For new code, use the 31 | * {@link PrivateKeyStorage} instead. 32 | * 33 | * @deprecated since version 5.0 34 | */ 35 | export class KeyStorage { 36 | private adapter: IStorageAdapter; 37 | 38 | constructor (config: IKeyStorageConfig | string = {}) { 39 | console.log('Warning! `KeyStorage` is deprecated. Use `PrivateKeyStorage` instead.'); 40 | 41 | this.adapter = resolveAdapter(config); 42 | } 43 | 44 | /** 45 | * Checks whether a private key data with the given name exist in persistent storage. 46 | * @param {string} name - Name to check. 47 | * @returns {Promise} - True if key data exist, otherwise false. 48 | */ 49 | exists(name: string): Promise { 50 | validateName(name); 51 | return this.adapter.exists(name); 52 | } 53 | 54 | /** 55 | * Loads the private key data by the given name. 56 | * @param {string} name - Name of key data to load. 57 | * @returns {Promise} - Private key data as a string, 58 | * or null if there is no data for the given name. 59 | */ 60 | load(name: string): Promise { 61 | validateName(name); 62 | return this.adapter.load(name); 63 | } 64 | 65 | /** 66 | * Removes the private key data stored under the given name from persistent storage. 67 | * @param {string} name - Name of the key data to remove. 68 | * @returns {Promise} - True if the key has been removed, otherwise false. 69 | */ 70 | remove(name: string): Promise { 71 | validateName(name); 72 | return this.adapter.remove(name); 73 | } 74 | 75 | /** 76 | * Persists the private key data under the given name. 77 | * @param {string} name - Name of the key data. 78 | * @param {string} data - The key data. 79 | * @returns {Promise} 80 | */ 81 | save(name: string, data: string): Promise { 82 | validateName(name); 83 | return this.adapter.store(name, data) 84 | .catch(error => { 85 | if (error && error.code === 'EEXIST') { 86 | return Promise.reject(new PrivateKeyExistsError()); 87 | } 88 | 89 | return Promise.reject(error); 90 | }); 91 | } 92 | } 93 | 94 | function resolveAdapter (config: IKeyStorageConfig|string) { 95 | if (typeof config === 'string') { 96 | return new StorageAdapter({ dir: config, name: config }); 97 | } 98 | 99 | const { adapter, ...rest } = config; 100 | if (adapter != null) { 101 | return adapter; 102 | } 103 | 104 | return new StorageAdapter({ ...DEFAULTS, ...rest }); 105 | } 106 | 107 | function validateName (name: string) { 108 | if (!name) throw new TypeError('Argument `name` is required.'); 109 | } 110 | 111 | function validateData (data: Buffer) { 112 | if (!data) throw new TypeError('Argument `data` is required.'); 113 | } 114 | -------------------------------------------------------------------------------- /src/Storage/PrivateKeyStorage.ts: -------------------------------------------------------------------------------- 1 | import { IPrivateKey, IPrivateKeyExporter } from '../types'; 2 | import { IKeyEntryStorage } from './KeyEntryStorage/IKeyEntryStorage'; 3 | import { KeyEntryStorage } from './KeyEntryStorage/KeyEntryStorage'; 4 | import { PrivateKeyExistsError } from './errors'; 5 | 6 | /** 7 | * Interface of a single entry in {@link PrivateKeyStorage}. 8 | */ 9 | export interface IPrivateKeyEntry { 10 | privateKey: IPrivateKey, 11 | meta?: { [key: string]: string } 12 | } 13 | 14 | /** 15 | * Class responsible for storage of private keys. 16 | */ 17 | export class PrivateKeyStorage { 18 | 19 | /** 20 | * Initializes a new instance of `PrivateKeyStorage`. 21 | * @param {IPrivateKeyExporter} privateKeyExporter - Object responsible for 22 | * exporting private key bytes from `IPrivateKey` objects and importing 23 | * private key bytes into `IPrivateKey` objects. 24 | * @param {IKeyEntryStorage} keyEntryStorage - Object responsible for 25 | * persistence of private keys data. 26 | */ 27 | constructor ( 28 | private privateKeyExporter: IPrivateKeyExporter, 29 | private keyEntryStorage: IKeyEntryStorage = new KeyEntryStorage() 30 | ) {} 31 | 32 | /** 33 | * Persists the given `privateKey` and `meta` under the given `name`. 34 | * If an entry with the same name already exists rejects the returned 35 | * Promise with {@link PrivateKeyExistsError} error. 36 | * 37 | * @param {string} name - Name of the private key. 38 | * @param {IPrivateKey} privateKey - The private key object. 39 | * @param {Object} [meta] - Optional metadata to store with the key. 40 | * 41 | * @returns {Promise} 42 | */ 43 | async store (name: string, privateKey: IPrivateKey, meta?: { [key: string]: string }) { 44 | const privateKeyData = this.privateKeyExporter.exportPrivateKey(privateKey); 45 | try { 46 | await this.keyEntryStorage.save({ name, value: privateKeyData.toString('base64'), meta }); 47 | } catch (error: any) { 48 | if (error && error.name === 'KeyEntryAlreadyExistsError') { 49 | throw new PrivateKeyExistsError(`Private key with the name ${name} already exists.`); 50 | } 51 | 52 | throw error; 53 | } 54 | } 55 | 56 | /** 57 | * Retrieves the private key with the given `name` from persistent storage. 58 | * If private with the given name does not exist, resolves the returned 59 | * Promise with `null`. 60 | * 61 | * @param {string} name - Name of the private key to load. 62 | * @returns {Promise} 63 | */ 64 | async load (name: string): Promise { 65 | const keyEntry = await this.keyEntryStorage.load(name); 66 | if (keyEntry === null) { 67 | return null; 68 | } 69 | const privateKey = this.privateKeyExporter.importPrivateKey(keyEntry.value); 70 | return { 71 | privateKey, 72 | meta: keyEntry.meta 73 | }; 74 | } 75 | 76 | /** 77 | * Removes the private key entry with the given `name` from persistent 78 | * storage. 79 | * 80 | * @param {string} name - Name of the private key to remove. 81 | * @returns {Promise} 82 | */ 83 | async delete (name: string): Promise { 84 | await this.keyEntryStorage.remove(name); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Storage/adapters/DefaultStorageAdapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * {@link FileSystemStorageAdapter} or {@link IndexedDbStorageAdapter} 3 | * depending on the target platform (node or browser). 4 | */ 5 | export { default as DefaultStorageAdapter } from './FileSystemStorageAdapter'; 6 | -------------------------------------------------------------------------------- /src/Storage/adapters/FileSystemStorageAdapter.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import crypto from 'crypto'; 4 | import mkdirp from 'mkdirp'; 5 | import rimraf from 'rimraf'; 6 | import { IStorageAdapter, IStorageAdapterConfig } from './IStorageAdapter'; 7 | import { StorageEntryAlreadyExistsError } from './errors'; 8 | 9 | const NO_SUCH_FILE = 'ENOENT'; 10 | const FILE_EXISTS = 'EEXIST'; 11 | 12 | /** 13 | * Implementation of {@link IStorageAdapter} that uses file system for 14 | * persistence. For use in Node.js. 15 | */ 16 | export default class FileSystemStorageAdapter implements IStorageAdapter { 17 | 18 | private config: IStorageAdapterConfig; 19 | 20 | /** 21 | * Initializes a new instance of `FileSystemStorageAdapter`. 22 | * @param {IStorageAdapterConfig} config - Configuration options. 23 | * Currently only `dir` is supported and must be a file system path 24 | * to the folder where the data will be stored. 25 | */ 26 | constructor (config: IStorageAdapterConfig) { 27 | this.config = config; 28 | 29 | mkdirp.sync(path.resolve(this.config.dir)); 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | store(key: string, data: string): Promise { 36 | return new Promise((resolve, reject) => { 37 | const file = this.resolveFilePath(key); 38 | 39 | fs.writeFile(file, data, { flag: 'wx' }, err => { 40 | if (err && err.code === FILE_EXISTS) { 41 | return reject(new StorageEntryAlreadyExistsError()); 42 | } 43 | 44 | resolve(); 45 | }); 46 | }); 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | load(key: string): Promise { 53 | return Promise.resolve().then(() => { 54 | const filename = this.resolveFilePath(key); 55 | return readFileAsync(filename); 56 | }); 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | exists(key: string): Promise { 63 | return new Promise((resolve, reject) => { 64 | const file = this.resolveFilePath(key); 65 | 66 | fs.access(file, err => { 67 | if (err) { 68 | if (err.code === NO_SUCH_FILE) { 69 | return resolve(false); 70 | } 71 | return reject(err); 72 | } 73 | 74 | resolve(true); 75 | }); 76 | }); 77 | } 78 | 79 | /** 80 | * @inheritDoc 81 | */ 82 | remove(key: string): Promise { 83 | return new Promise((resolve, reject) => { 84 | const file = this.resolveFilePath(key); 85 | fs.unlink(file, err => { 86 | if (err) { 87 | if (err.code === NO_SUCH_FILE) { 88 | return resolve(false); 89 | } 90 | 91 | return reject(err); 92 | } 93 | 94 | resolve(true); 95 | }); 96 | }); 97 | } 98 | 99 | /** 100 | * @inheritDoc 101 | */ 102 | update (key: string, data: string): Promise { 103 | return new Promise((resolve, reject) => { 104 | const file = this.resolveFilePath(key); 105 | fs.writeFile(file, data, { flag: 'w' }, err => { 106 | if (err) { 107 | return reject(err); 108 | } 109 | 110 | resolve(); 111 | }); 112 | }); 113 | } 114 | 115 | /** 116 | * @inheritDoc 117 | */ 118 | clear (): Promise { 119 | return new Promise((resolve, reject) => { 120 | rimraf(this.config.dir!, err => { 121 | if (err) { 122 | return reject(err); 123 | } 124 | resolve(); 125 | }); 126 | }); 127 | } 128 | 129 | /** 130 | * @inheritDoc 131 | */ 132 | list (): Promise { 133 | return new Promise((resolve, reject) => { 134 | fs.readdir(this.config.dir, (err, files) => { 135 | if (err) { 136 | return reject(err); 137 | } 138 | 139 | Promise.all( 140 | files.map(filename => 141 | readFileAsync(path.resolve(this.config.dir!, filename)) 142 | ) 143 | ).then(contents => { 144 | const entries = contents.filter(content => content !== null) as string[]; 145 | 146 | resolve(entries); 147 | }).catch(reject); 148 | }); 149 | }); 150 | } 151 | 152 | private resolveFilePath (key: string): string { 153 | return path.resolve(this.config.dir!, this.hash(key)); 154 | } 155 | 156 | private hash (data: string): string { 157 | return crypto 158 | .createHash('sha256') 159 | .update(data) 160 | .digest('hex'); 161 | } 162 | } 163 | 164 | function readFileAsync (filename: string): Promise { 165 | return new Promise((resolve, reject) => { 166 | fs.readFile(filename, (err, data) => { 167 | if (err) { 168 | if (err.code === NO_SUCH_FILE) { 169 | return resolve(null as any); 170 | } 171 | 172 | return reject(err); 173 | } 174 | 175 | resolve(data.toString('utf8')); 176 | }); 177 | }); 178 | } 179 | -------------------------------------------------------------------------------- /src/Storage/adapters/IStorageAdapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration options of {@link FileSystemStorageAdapter} and 3 | * {@link IndexedDbStorageAdapter}. 4 | */ 5 | export interface IStorageAdapterConfig { 6 | dir: string; 7 | name: string; 8 | } 9 | 10 | /** 11 | * Interface to be implemented by objects capable of storing binary data 12 | * in persistent storage. 13 | */ 14 | export interface IStorageAdapter { 15 | /** 16 | * Saves the `data` under the `key` in persistent storage. If the key 17 | * already exists, throws {@link StorageEntryAlreadyExistsError}. 18 | * @param {string} key - The key. 19 | * @param {string} data - The data. 20 | * @returns {Promise} 21 | */ 22 | store (key: string, data: string): Promise; 23 | 24 | /** 25 | * Retieves the data for the `key`. 26 | * @param {string} key - The key. 27 | * @returns {Promise} - Promise resolved with the 28 | * data for the given key, or null if there's no data. 29 | */ 30 | load (key: string): Promise; 31 | 32 | /** 33 | * Checks if the data for the `key` exists. 34 | * @param {string} key - The key to check. 35 | * @returns {Promise} - Promise resolved with `true` 36 | * if data for the `key` exists, or `false` otherwise. 37 | */ 38 | exists (key: string): Promise; 39 | 40 | /** 41 | * Removes the data for the `key`. 42 | * @param {string} key - The key to remove. 43 | * @returns {Promise} Promise resolved with `true` 44 | * if data have been removed, or `false` otherwise. 45 | */ 46 | remove (key: string): Promise; 47 | 48 | /** 49 | * Sets the new data for the `key`. 50 | * @param {string} key - The key to update. 51 | * @param {string} data - The new data. 52 | */ 53 | update (key: string, data: string): Promise; 54 | 55 | /** 56 | * Removes all of the entries from persistent storage. 57 | * @returns {Promise} 58 | */ 59 | clear (): Promise; 60 | 61 | /** 62 | * Retrieves a collection of all of the values in persistent storage. 63 | * @returns {Promise} 64 | */ 65 | list (): Promise; 66 | } 67 | -------------------------------------------------------------------------------- /src/Storage/adapters/errors.ts: -------------------------------------------------------------------------------- 1 | import { VirgilError } from '../../VirgilError'; 2 | 3 | /** 4 | * Error thrown by {@link IStorageAdapter.store} method when saving a value 5 | * with a key that already exists in store. 6 | */ 7 | export class StorageEntryAlreadyExistsError extends VirgilError { 8 | constructor(key?: string) { 9 | super( 10 | `Storage entry ${ key ? 'with key ' + name : 'with the given key' }already exists`, 11 | 'StorageEntryAlreadyExistsError', 12 | StorageEntryAlreadyExistsError 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Storage/adapters/indexedDb/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare var indexedDB: IDBFactory; 2 | declare var webkitIndexedDB: IDBFactory; 3 | declare var mozIndexedDB: IDBFactory; 4 | declare var OIndexedDB: IDBFactory; 5 | declare var msIndexedDB: IDBFactory; 6 | declare var openDatabase: Function; 7 | -------------------------------------------------------------------------------- /src/Storage/adapters/indexedDb/isIndexedDbValid.ts: -------------------------------------------------------------------------------- 1 | // Some code originally from localForage 2 | // See: https://github.com/localForage/localForage/blob/master/src/utils/isIndexedDBValid.js 3 | /** 4 | * @hidden 5 | * @returns {boolean} 6 | */ 7 | export function isIndexedDbValid() { 8 | // We mimic PouchDB here 9 | // Following #7085 (see https://github.com/pouchdb/pouchdb/issues/7085) 10 | // buggy idb versions (typically Safari < 10.1) are considered valid. 11 | 12 | // On Firefox SecurityError is thrown while referencing indexedDB if cookies 13 | // are not allowed. `typeof indexedDB` also triggers the error. 14 | try { 15 | // some outdated implementations of IDB that appear on Samsung 16 | // and HTC Android devices <4.4 are missing IDBKeyRange 17 | return typeof indexedDB !== 'undefined' && typeof IDBKeyRange !== 'undefined'; 18 | } catch (e) { 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Storage/errors.ts: -------------------------------------------------------------------------------- 1 | import { VirgilError } from '../VirgilError'; 2 | 3 | /** 4 | * Error thrown from {@link PrivateKeyStorage.save} method when saving 5 | * a private key with a name that already exists in store. 6 | */ 7 | export class PrivateKeyExistsError extends VirgilError { 8 | constructor(message: string = 'Private key with same name already exists') { 9 | super(message, 'PrivateKeyExistsError', PrivateKeyExistsError); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/VirgilError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom error class for errors specific to Virgil SDK. 3 | */ 4 | export class VirgilError extends Error { 5 | name: string; 6 | constructor(m: string, name: string = 'VirgilError', DerivedClass: any = VirgilError) { 7 | super(m); 8 | Object.setPrototypeOf(this, DerivedClass.prototype); 9 | this.name = name; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/declarations.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as Chai from "chai"; 4 | import AssertStatic = Chai.AssertStatic; 5 | import SinonSpy = sinon.SinonSpy; 6 | import SinonSpyCall = sinon.SinonSpyCall; 7 | import SinonStatic = sinon.SinonStatic; 8 | 9 | interface ExtendedAssert extends AssertStatic { 10 | notCalled(spy: SinonSpy): void; 11 | called(spy: SinonSpy): void; 12 | calledOnce(spy: SinonSpy): void; 13 | calledTwice(spy: SinonSpy): void; 14 | calledThrice(spy: SinonSpy): void; 15 | callCount(spy: SinonSpy, count: number): void; 16 | callOrder(...spies: SinonSpy[]): void; 17 | calledOn(spyOrSpyCall: SinonSpy | SinonSpyCall, obj: any): void; 18 | alwaysCalledOn(spy: SinonSpy, obj: any): void; 19 | calledWith(spyOrSpyCall: SinonSpy | SinonSpyCall, ...args: any[]): void; 20 | alwaysCalledWith(spy: SinonSpy, ...args: any[]): void; 21 | neverCalledWith(spy: SinonSpy, ...args: any[]): void; 22 | calledWithExactly(spyOrSpyCall: SinonSpy | SinonSpyCall, ...args: any[]): void; 23 | alwaysCalledWithExactly(spy: SinonSpy, ...args: any[]): void; 24 | calledWithMatch(spyOrSpyCall: SinonSpy | SinonSpyCall, ...args: any[]): void; 25 | alwaysCalledWithMatch(spy: SinonSpy, ...args: any[]): void; 26 | neverCalledWithMatch(spy: SinonSpy, ...args: any[]): void; 27 | calledWithNew(spyOrSpyCall: SinonSpy | SinonSpyCall): void; 28 | threw(spyOrSpyCall: SinonSpy | SinonSpyCall): void; 29 | threw(spyOrSpyCall: SinonSpy | SinonSpyCall, exception: string): void; 30 | threw(spyOrSpyCall: SinonSpy | SinonSpyCall, exception: any): void; 31 | alwaysThrew(spy: SinonSpy): void; 32 | alwaysThrew(spy: SinonSpy, exception: string): void; 33 | alwaysThrew(spy: SinonSpy, exception: any): void; 34 | } 35 | 36 | declare var assert: ExtendedAssert; 37 | 38 | declare var sinon: SinonStatic; 39 | 40 | declare module '*.json' { 41 | const value: { [key: string]: any }; 42 | export default value; 43 | } 44 | 45 | declare module NodeJS { 46 | interface Process { 47 | browser?: boolean; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import './unit/base64.test'; 2 | import './unit/CachingJwtProvider.test'; 3 | import './unit/CallbackJwtProvider.test'; 4 | import './unit/CardClient.test'; 5 | import './unit/ConstAccesTokenProvider.test'; 6 | import './unit/GeneratorJwtProvider.test'; 7 | import './unit/StorageAdapter.test'; 8 | import './unit/KeyStorage.test'; 9 | import './unit/KeyEntryStorage.test'; 10 | import './unit/PrivateKeyStorage.test'; 11 | import './unit/JwtGenerator.test'; 12 | import './unit/VirgilAgent.test'; 13 | import './integration/CardManager.test'; 14 | import './integration/CardVerifier.test'; 15 | import './integration/Jwt.test'; 16 | import './integration/ModelSigner.test'; 17 | import './integration/RawSignedModel.test'; 18 | -------------------------------------------------------------------------------- /src/__tests__/integration/CardVerifier.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // @ts-nocheck 4 | import { initCrypto, VirgilCrypto, VirgilCardCrypto } from 'virgil-crypto'; 5 | import { VirgilCardVerifier, RawSignedModel } from '../..'; 6 | import { parseRawSignedModel } from '../../Cards/CardUtils'; 7 | 8 | import { compatData } from './data'; 9 | 10 | const init = (cardAsString: string) => { 11 | const crypto = new VirgilCrypto(); 12 | const cardCrypto = new VirgilCardCrypto(crypto); 13 | const importedRawSignedModel = RawSignedModel.fromString(cardAsString); 14 | const importedCard = parseRawSignedModel(cardCrypto, importedRawSignedModel); 15 | return { 16 | crypto, 17 | cardCrypto, 18 | importedCard, 19 | verifier: new VirgilCardVerifier(cardCrypto) 20 | }; 21 | }; 22 | 23 | describe('VirgilCardVerifier', () => { 24 | before(async () => { 25 | await initCrypto(); 26 | }); 27 | 28 | it ('verifies imported card without checking signatures (STC-10)', () => { 29 | const { verifier, importedCard } = init(compatData['STC-10.as_string']); 30 | 31 | verifier.verifySelfSignature = false; 32 | verifier.verifyVirgilSignature = false; 33 | 34 | assert.isTrue(verifier.verifyCard(importedCard)); 35 | }); 36 | 37 | it ('verifies imported card checking self signature (STC-10)', () => { 38 | const { verifier, importedCard } = init(compatData['STC-10.as_string']); 39 | 40 | verifier.verifySelfSignature = true; 41 | verifier.verifyVirgilSignature = false; 42 | 43 | assert.isTrue(verifier.verifyCard(importedCard)); 44 | }); 45 | 46 | it ('verifies imported card checking virgil signatures (STC-10)', () => { 47 | const { verifier, importedCard } = init(compatData['STC-10.as_string']); 48 | 49 | verifier.verifySelfSignature = true; 50 | verifier.verifyVirgilSignature = true; 51 | 52 | assert.isTrue(verifier.verifyCard(importedCard)); 53 | }); 54 | 55 | it ('verifies imported card with good key in whitelist (STC-10)', () => { 56 | const { verifier, importedCard, crypto } = init(compatData['STC-10.as_string']); 57 | 58 | const privateKey1 = crypto.importPrivateKey(compatData['STC-10.private_key1_base64']); 59 | const publicKey1 = crypto.extractPublicKey(privateKey1); 60 | const publicKey1Base64 = crypto.exportPublicKey(publicKey1).toString('base64'); 61 | 62 | verifier.verifySelfSignature = true; 63 | verifier.verifyVirgilSignature = true; 64 | verifier.whitelists = [ 65 | [ 66 | { signer: 'extra', publicKeyBase64: publicKey1Base64} 67 | ] 68 | ]; 69 | 70 | assert.isTrue(verifier.verifyCard(importedCard)); 71 | }); 72 | 73 | it ('verifies imported card with good and bad key in whitelist (STC-10)', () => { 74 | const { verifier, importedCard, crypto } = init(compatData['STC-10.as_string']); 75 | 76 | // good key 77 | const privateKey1 = crypto.importPrivateKey(compatData['STC-10.private_key1_base64']); 78 | const publicKey1 = crypto.extractPublicKey(privateKey1); 79 | const publicKey1Base64 = crypto.exportPublicKey(publicKey1).toString('base64'); 80 | 81 | // bad key, but irrelevant 82 | const { publicKey: publicKey2 } = crypto.generateKeys(); 83 | const publicKey2Base64 = crypto.exportPublicKey(publicKey2).toString('base64'); 84 | 85 | verifier.verifySelfSignature = true; 86 | verifier.verifyVirgilSignature = true; 87 | verifier.whitelists = [ 88 | [ 89 | { signer: 'extra', publicKeyBase64: publicKey2Base64 }, 90 | { signer: 'extra', publicKeyBase64: publicKey1Base64 } 91 | ] 92 | ]; 93 | 94 | assert.isTrue(verifier.verifyCard(importedCard)); 95 | }); 96 | 97 | it ('does not verify imported card with bad key in whitelist (STC-10)', () => { 98 | const { verifier, importedCard, crypto } = init(compatData['STC-10.as_string']); 99 | 100 | // good key 101 | const privateKey1 = crypto.importPrivateKey(compatData['STC-10.private_key1_base64']); 102 | const publicKey1 = crypto.extractPublicKey(privateKey1); 103 | const publicKey1Base64 = crypto.exportPublicKey(publicKey1).toString('base64'); 104 | 105 | // bad key, but irrelevant 106 | const { publicKey: publicKey2 } = crypto.generateKeys(); 107 | const publicKey2Base64 = crypto.exportPublicKey(publicKey2).toString('base64'); 108 | 109 | // bad key, should fail 110 | const { publicKey: publicKey3 } = crypto.generateKeys(); 111 | const publicKey3Base64 = crypto.exportPublicKey(publicKey3).toString('base64'); 112 | 113 | verifier.verifySelfSignature = true; 114 | verifier.verifyVirgilSignature = true; 115 | verifier.whitelists = [ 116 | [ 117 | { signer: 'extra', publicKeyBase64: publicKey2Base64 }, 118 | { signer: 'extra', publicKeyBase64: publicKey1Base64 } 119 | ], 120 | [ 121 | { signer: 'bad', publicKeyBase64: publicKey3Base64 } 122 | ] 123 | ]; 124 | 125 | assert.isFalse(verifier.verifyCard(importedCard)); 126 | }); 127 | 128 | it ('does not verify imported card without self signature (STC-11)', () => { 129 | const { verifier, importedCard } = init(compatData['STC-11.as_string']); 130 | 131 | verifier.verifyVirgilSignature = false; 132 | verifier.verifySelfSignature = true; 133 | 134 | assert.isFalse(verifier.verifyCard(importedCard)); 135 | }); 136 | 137 | it ('does not verify imported card without virgil signature (STC-12)', () => { 138 | const { verifier, importedCard } = init(compatData['STC-12.as_string']); 139 | 140 | verifier.verifyVirgilSignature = true; 141 | verifier.verifySelfSignature = false; 142 | 143 | assert.isFalse(verifier.verifyCard(importedCard)); 144 | }); 145 | 146 | it ('does not verify imported card with invalid virgil signature (STC-14)', () => { 147 | const { verifier, importedCard } = init(compatData['STC-14.as_string']); 148 | 149 | verifier.verifyVirgilSignature = true; 150 | verifier.verifySelfSignature = false; 151 | 152 | assert.isFalse(verifier.verifyCard(importedCard)); 153 | }); 154 | 155 | it ('does not verify imported card with invalid self signature (STC-15)', () => { 156 | const { verifier, importedCard } = init(compatData['STC-15.as_string']); 157 | 158 | verifier.verifyVirgilSignature = false; 159 | verifier.verifySelfSignature = true; 160 | 161 | assert.isFalse(verifier.verifyCard(importedCard)); 162 | }); 163 | 164 | it ('does not verify imported card with only bad key in whitelist (STC-16)', () => { 165 | const { verifier, importedCard, crypto } = init(compatData['STC-16.as_string']); 166 | 167 | // bad key, should fail 168 | const { publicKey } = crypto.generateKeys(); 169 | const publicKeyBase64 = crypto.exportPublicKey(publicKey).toString('base64'); 170 | 171 | verifier.verifySelfSignature = true; 172 | verifier.verifyVirgilSignature = true; 173 | verifier.whitelists = [ 174 | [ 175 | { signer: 'extra', publicKeyBase64: publicKeyBase64 } 176 | ] 177 | ]; 178 | 179 | assert.isFalse(verifier.verifyCard(importedCard)); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /src/__tests__/integration/Jwt.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // @ts-nocheck 4 | import { initCrypto, VirgilCrypto, VirgilAccessTokenSigner } from 'virgil-crypto'; 5 | import { 6 | CallbackJwtProvider, 7 | ConstAccessTokenProvider, 8 | ITokenContext, 9 | Jwt, 10 | JwtGenerator, 11 | JwtVerifier 12 | } from '../..'; 13 | import { base64UrlEncode } from '../../Lib/base64'; 14 | 15 | import { compatData } from './data'; 16 | 17 | const initJwtVerifier = (apiKeyId: string, apiKeyPublicKey: string) => { 18 | const crypto = new VirgilCrypto(); 19 | return new JwtVerifier({ 20 | accessTokenSigner: new VirgilAccessTokenSigner(crypto), 21 | apiKeyId: apiKeyId, 22 | apiPublicKey: crypto.importPublicKey(apiKeyPublicKey) 23 | }); 24 | }; 25 | 26 | const initJwtGenerator = (appId: string, apiKeyId: string, apiKeyPrivateKey: string) => { 27 | const crypto = new VirgilCrypto(); 28 | return new JwtGenerator({ 29 | appId: appId, 30 | apiKeyId: apiKeyId, 31 | apiKey: crypto.importPrivateKey(apiKeyPrivateKey), 32 | accessTokenSigner: new VirgilAccessTokenSigner(crypto) 33 | }); 34 | }; 35 | 36 | describe('JWT compatibility', () => { 37 | before(async () => { 38 | await initCrypto(); 39 | }); 40 | 41 | describe('JwtVerifier', () => { 42 | it('verifies imported JWT (STC-22)', () => { 43 | const verifier = initJwtVerifier( 44 | compatData['STC-22.api_key_id'], 45 | compatData['STC-22.api_public_key_base64'] 46 | ); 47 | const jwtString = compatData['STC-22.jwt']; 48 | const jwt = Jwt.fromString(jwtString); 49 | 50 | assert.isOk(jwt, 'jwt parsed successfully'); 51 | assert.equal(jwt.identity(), 'some_identity', 'jwt identity is correct'); 52 | assert.equal( 53 | jwt.appId(), 54 | '13497c3c795e3a6c32643b0a76957b70d2332080762469cdbec89d6390e6dbd7', 55 | 'jwt app id is correct' 56 | ); 57 | assert.equal(jwt.toString(), jwtString, 'jwt serialized successfully'); 58 | assert.isTrue(jwt.isExpired(), 'jwt is expired'); 59 | assert.isTrue(verifier.verifyToken(jwt), 'jwt is verified'); 60 | }); 61 | }); 62 | 63 | describe('JwtGenerator', () => { 64 | it('generates valid JWT (STC-23)', () => { 65 | const verifier = initJwtVerifier( 66 | compatData['STC-23.api_key_id'], 67 | compatData['STC-23.api_public_key_base64'] 68 | ); 69 | const generator = initJwtGenerator( 70 | compatData['STC-23.app_id'], 71 | compatData['STC-23.api_key_id'], 72 | compatData['STC-23.api_private_key_base64'] 73 | ); 74 | 75 | const jwt = generator.generateToken('user@example.com'); 76 | assert.isOk(jwt, 'jwt generated successfully'); 77 | assert.equal(jwt.identity(), 'user@example.com', 'jwt identity is correct'); 78 | assert.equal(jwt.appId(), compatData['STC-23.app_id'], 'jwt app id is correct'); 79 | assert.isFalse(jwt.isExpired(), 'jwt is not expired'); 80 | assert.isTrue(verifier.verifyToken(jwt), 'jwt is verified'); 81 | }); 82 | }); 83 | 84 | describe('CallbackJwtProvider', () => { 85 | it ('invokes callback correctly', () => { 86 | const generator = initJwtGenerator( 87 | compatData['STC-23.app_id'], 88 | compatData['STC-23.api_key_id'], 89 | compatData['STC-23.api_private_key_base64'] 90 | ); 91 | const jwt = generator.generateToken('stub'); 92 | const callback = sinon.stub().returns(Promise.resolve(jwt.toString())); 93 | const provider = new CallbackJwtProvider(callback); 94 | const tokenContext: ITokenContext = { service: 'stub', identity: 'stub', operation: 'stub' }; 95 | 96 | assert.isFulfilled( 97 | provider.getToken(tokenContext) 98 | .then(token => { 99 | assert.equal(token.toString(), jwt.toString(), 'token is unmodified'); 100 | assert.calledOnce(callback); 101 | assert.calledWithExactly(callback, tokenContext); 102 | }) 103 | ); 104 | }); 105 | 106 | it ('rejects when receives invalid token from callback (STC-24)', () => { 107 | const callback = sinon.stub().returns(Promise.resolve('invalid-token')); 108 | const provider = new CallbackJwtProvider(callback); 109 | const tokenContext: ITokenContext = { service: 'stub', identity: 'stub', operation: 'stub' }; 110 | 111 | 112 | assert.isRejected( 113 | provider.getToken(tokenContext), 114 | /Wrong JWT/ 115 | ); 116 | }); 117 | }); 118 | 119 | describe('ConstJwtProvider', () => { 120 | it ('returns the constant token (STC-37)', () => { 121 | const generator = initJwtGenerator( 122 | compatData['STC-23.app_id'], 123 | compatData['STC-23.api_key_id'], 124 | compatData['STC-23.api_private_key_base64'] 125 | ); 126 | const jwt = generator.generateToken('stub'); 127 | const provider = new ConstAccessTokenProvider(jwt); 128 | const tokenContext: ITokenContext = { service: 'stub', identity: 'stub', operation: 'stub' }; 129 | 130 | assert.isFulfilled( 131 | provider.getToken(tokenContext) 132 | .then(token => { 133 | assert.equal(token, jwt); 134 | }) 135 | ); 136 | }); 137 | }); 138 | 139 | describe('Jwt', () => { 140 | it ('can be constructed from string (STC-28)', () => { 141 | const tokenString = compatData['STC-28.jwt']; 142 | const jwt = Jwt.fromString(tokenString); 143 | 144 | assert.equal(jwt.identity(), compatData['STC-28.jwt_identity'], 'identity is correct'); 145 | assert.equal(jwt.appId(), compatData['STC-28.jwt_app_id'], 'app id is correct'); 146 | assert.equal(jwt.body.iss, compatData['STC-28.jw_issuer'], 'issuer is correct'); 147 | assert.equal(jwt.body.sub, compatData['STC-28.jwt_subject'], 'subject is correct'); 148 | assert.deepEqual( 149 | jwt.body.ada, 150 | JSON.parse(compatData['STC-28.jwt_additional_data']), 151 | 'additional data is correct' 152 | ); 153 | assert.equal(jwt.body.exp, Number(compatData['STC-28.jwt_expires_at']), 'expiresAt is correct'); 154 | assert.equal(jwt.body.iat, Number(compatData['STC-28.jwt_issued_at']), 'issuedAt is correct'); 155 | assert.equal(jwt.header.alg, compatData['STC-28.jwt_algorithm'], 'algorithm is correct'); 156 | assert.equal(jwt.header.kid, compatData['STC-28.jwt_api_key_id'], 'key id is correct'); 157 | assert.equal(jwt.header.cty, compatData['STC-28.jwt_content_type'], 'content type is correct'); 158 | assert.equal(jwt.header.typ, compatData['STC-28.jwt_type'], 'type is correct'); 159 | assert.equal( 160 | jwt.signature, 161 | compatData['STC-28.jwt_signature_base64'], 162 | 'type is correct' 163 | ); 164 | assert.isTrue(jwt.isExpired(), 'jwt is expired'); 165 | assert.equal(jwt.toString(), tokenString, 'string representation is correct'); 166 | }); 167 | 168 | it ('can be constructed from string (STC-29)', () => { 169 | const tokenString = compatData['STC-29.jwt']; 170 | const jwt = Jwt.fromString(tokenString); 171 | 172 | assert.equal(jwt.identity(), compatData['STC-29.jwt_identity'], 'identity is correct'); 173 | assert.equal(jwt.appId(), compatData['STC-29.jwt_app_id'], 'app id is correct'); 174 | assert.equal(jwt.body.iss, compatData['STC-29.jw_issuer'], 'issuer is correct'); 175 | assert.equal(jwt.body.sub, compatData['STC-29.jwt_subject'], 'subject is correct'); 176 | assert.deepEqual( 177 | jwt.body.ada, 178 | JSON.parse(compatData['STC-29.jwt_additional_data']), 179 | 'additional data is correct' 180 | ); 181 | assert.equal(jwt.body.exp, Number(compatData['STC-29.jwt_expires_at']), 'expiresAt is correct'); 182 | assert.equal(jwt.body.iat, Number(compatData['STC-29.jwt_issued_at']), 'issuedAt is correct'); 183 | assert.equal(jwt.header.alg, compatData['STC-29.jwt_algorithm'], 'algorithm is correct'); 184 | assert.equal(jwt.header.kid, compatData['STC-29.jwt_api_key_id'], 'key id is correct'); 185 | assert.equal(jwt.header.cty, compatData['STC-29.jwt_content_type'], 'content type is correct'); 186 | assert.equal(jwt.header.typ, compatData['STC-29.jwt_type'], 'type is correct'); 187 | assert.equal( 188 | jwt.signature, 189 | compatData['STC-29.jwt_signature_base64'], 190 | 'type is correct' 191 | ); 192 | assert.isFalse(jwt.isExpired(), 'jwt is not expired'); 193 | assert.equal(jwt.toString(), tokenString, 'string representation is correct'); 194 | }); 195 | 196 | it ('does not mutate the string representation', () => { 197 | const header = { 198 | alg: "VEDS512", 199 | cty: "virgil-jwt;v=1", 200 | kid: "987654321fedcba987654321fedcba00", 201 | typ: "JWT" 202 | }; 203 | const body = { 204 | exp: Date.now() + 20 * 60 * 1000, 205 | iat: Date.now(), 206 | iss: "virgil-123456789abcdef123456789abcdef00", 207 | sub: "test-identity" 208 | }; 209 | const signature = 'does not matter in this test'; 210 | const generatedJwt = new Jwt(header, body, signature); 211 | 212 | // When converting to stirng Jwt stringifies header and body without spaces, 213 | // we stringify with spaces here to ensure the string representation is different 214 | const tokenString = `${ 215 | base64UrlEncode(JSON.stringify(header, null, 2)) 216 | }.${ 217 | base64UrlEncode(JSON.stringify(body, null, 2)) 218 | }.${ 219 | base64UrlEncode(signature) 220 | }`; 221 | 222 | assert.notEqual( 223 | generatedJwt.toString(), 224 | tokenString, 225 | 'generated string representation is different from manually-constructed one' 226 | ); 227 | 228 | const jwtFromString = Jwt.fromString(tokenString); 229 | assert.equal(jwtFromString.toString(), tokenString, 'string representation must never change'); 230 | }); 231 | }); 232 | }); 233 | -------------------------------------------------------------------------------- /src/__tests__/integration/ModelSigner.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { initCrypto, VirgilCrypto, VirgilCardCrypto } from 'virgil-crypto'; 3 | import { ModelSigner } from '../../Cards/ModelSigner'; 4 | import { SelfSigner } from '../../Cards/constants'; 5 | import { generateRawSigned } from '../../Cards/CardUtils'; 6 | 7 | const init = () => { 8 | const crypto = new VirgilCrypto(); 9 | const cardCrypto = new VirgilCardCrypto(crypto); 10 | return { 11 | crypto, 12 | cardCrypto, 13 | signer: new ModelSigner(cardCrypto) 14 | }; 15 | }; 16 | 17 | describe('ModelSigner', () => { 18 | before(async () => { 19 | await initCrypto(); 20 | }); 21 | 22 | it ('adds self signature (STC-8)', () => { 23 | const { crypto, cardCrypto, signer } = init(); 24 | const keyPair = crypto.generateKeys(); 25 | const model = generateRawSigned(cardCrypto, { 26 | privateKey: keyPair.privateKey, 27 | publicKey: keyPair.publicKey, 28 | identity: 'user@example.com' 29 | }); 30 | 31 | signer.sign({ model, signerPrivateKey: keyPair.privateKey }); 32 | const selfSignature = model.signatures[0]; 33 | assert.isOk(selfSignature, 'signature appended'); 34 | assert.equal(selfSignature.signer, SelfSigner, 'signature has `self` signer type'); 35 | assert.notOk(selfSignature.snapshot, 'signature does not have snapshot'); 36 | assert.isTrue( 37 | crypto.verifySignature(model.contentSnapshot, selfSignature.signature, keyPair.publicKey), 38 | 'self signature is valid' 39 | ); 40 | }); 41 | 42 | it ('throws when self-signing twice (STC-8)', () => { 43 | const { crypto, cardCrypto, signer } = init(); 44 | const keyPair = crypto.generateKeys(); 45 | const model = generateRawSigned(cardCrypto, { 46 | privateKey: keyPair.privateKey, 47 | publicKey: keyPair.publicKey, 48 | identity: 'user@example.com' 49 | }); 50 | 51 | signer.sign({ model, signerPrivateKey: keyPair.privateKey }); 52 | 53 | assert.throws(() => { 54 | signer.sign({ model, signerPrivateKey: keyPair.privateKey }); 55 | }, Error); 56 | }); 57 | 58 | it ('add signature with custom signer type (STC-8)', () => { 59 | const { crypto, cardCrypto, signer } = init(); 60 | const keyPair = crypto.generateKeys(); 61 | const model = generateRawSigned(cardCrypto, { 62 | privateKey: keyPair.privateKey, 63 | publicKey: keyPair.publicKey, 64 | identity: 'user@example.com' 65 | }); 66 | const keyPair2 = crypto.generateKeys(); 67 | 68 | signer.sign({ model, signerPrivateKey: keyPair2.privateKey, signer: 'test' }); 69 | 70 | const testSignature = model.signatures[0]; 71 | assert.isOk(testSignature, 'signature appended'); 72 | assert.equal(testSignature.signer, 'test', 'signature has `test` signer type'); 73 | assert.notOk(testSignature.snapshot, 'signature does not have snapshot'); 74 | assert.isTrue( 75 | crypto.verifySignature(model.contentSnapshot, testSignature.signature, keyPair2.publicKey), 76 | 'test signature is valid' 77 | ); 78 | }); 79 | 80 | it ('throws when signing with the same signer type twice (STC-8)', () => { 81 | const { crypto, cardCrypto, signer } = init(); 82 | const keyPair = crypto.generateKeys(); 83 | const model = generateRawSigned(cardCrypto, { 84 | privateKey: keyPair.privateKey, 85 | publicKey: keyPair.publicKey, 86 | identity: 'user@example.com' 87 | }); 88 | const keyPair2 = crypto.generateKeys(); 89 | 90 | signer.sign({ model, signerPrivateKey: keyPair2.privateKey, signer: 'test' }); 91 | 92 | assert.throws(() => { 93 | signer.sign({ model, signerPrivateKey: keyPair2.privateKey, signer: 'test' }); 94 | }, Error); 95 | }); 96 | 97 | it ('adds self signature with additional data (STC-9)', () => { 98 | const { crypto, cardCrypto, signer } = init(); 99 | const keyPair = crypto.generateKeys(); 100 | const model = generateRawSigned(cardCrypto, { 101 | privateKey: keyPair.privateKey, 102 | publicKey: keyPair.publicKey, 103 | identity: 'user@example.com' 104 | }); 105 | const extraFields = { 106 | custom: 'value' 107 | }; 108 | 109 | signer.sign({ model, signerPrivateKey: keyPair.privateKey, extraFields }); 110 | const selfSignature = model.signatures[0]; 111 | assert.isOk(selfSignature, 'signature appended'); 112 | assert.equal(selfSignature.signer, SelfSigner, 'signature has `self` signer type'); 113 | assert.isOk(selfSignature.snapshot, 'signature does has snapshot'); 114 | assert.isTrue( 115 | crypto.verifySignature( 116 | model.contentSnapshot + selfSignature.snapshot, 117 | selfSignature.signature, 118 | keyPair.publicKey 119 | ), 120 | 'self signature is valid' 121 | ); 122 | }); 123 | 124 | it ('add signature with custom signer type and additional data (STC-9)', () => { 125 | const { crypto, cardCrypto, signer } = init(); 126 | const keyPair = crypto.generateKeys(); 127 | const model = generateRawSigned(cardCrypto, { 128 | privateKey: keyPair.privateKey, 129 | publicKey: keyPair.publicKey, 130 | identity: 'user@example.com' 131 | }); 132 | const keyPair2 = crypto.generateKeys(); 133 | const extraFields = { 134 | custom: 'value' 135 | }; 136 | 137 | signer.sign({ model, signerPrivateKey: keyPair2.privateKey, signer: 'test', extraFields }); 138 | 139 | const testSignature = model.signatures[0]; 140 | assert.isOk(testSignature, 'signature appended'); 141 | assert.equal(testSignature.signer, 'test', 'signature has `test` signer type'); 142 | assert.isOk(testSignature.snapshot, 'signature does not have snapshot'); 143 | assert.isTrue( 144 | crypto.verifySignature( 145 | model.contentSnapshot + testSignature.snapshot, 146 | testSignature.signature, 147 | keyPair2.publicKey 148 | ), 149 | 'test signature is valid' 150 | ); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /src/__tests__/integration/RawSignedModel.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { initCrypto, VirgilCrypto, VirgilCardCrypto } from 'virgil-crypto'; 4 | import { RawSignedModel, IRawSignedModelJson } from '../../Cards/RawSignedModel'; 5 | import { parseRawSignedModel } from '../../Cards/CardUtils'; 6 | 7 | import { compatData } from './data'; 8 | 9 | const initCardCrypto = () => new VirgilCardCrypto(new VirgilCrypto()); 10 | 11 | describe('RawSignedModel', () => { 12 | before(async () => { 13 | await initCrypto(); 14 | }); 15 | 16 | it('imports raw signed model from string (STC-1)', () => { 17 | const rawSignedModelString = compatData['STC-1.as_string']; 18 | const imported = RawSignedModel.fromString(rawSignedModelString); 19 | assert.isOk(imported, 'imported successfully'); 20 | }); 21 | 22 | it('parses imported raw signed model (STC-1)', () => { 23 | const rawSignedModelString = compatData['STC-1.as_string']; 24 | const cardCrypto = initCardCrypto(); 25 | const imported = RawSignedModel.fromString(rawSignedModelString); 26 | 27 | const parsed = parseRawSignedModel(cardCrypto, imported); 28 | assert.isOk(parsed, 'parsed successfully'); 29 | assert.equal(parsed.identity, 'test'); 30 | assert.equal( 31 | Buffer.from(cardCrypto.exportPublicKey(parsed.publicKey)).toString('base64'), 32 | 'MCowBQYDK2VwAyEA6d9bQQFuEnU8vSmx9fDo0Wxec42JdNg4VR4FOr4/BUk=' 33 | ); 34 | assert.equal(parsed.version, '5.0'); 35 | assert.equal(parsed.createdAt.getTime(), 1515686245 * 1000); 36 | assert.isNotOk(parsed.previousCardId); 37 | assert.equal(parsed.signatures.length, 0); 38 | }); 39 | 40 | it('re-exports raw signed model (STC-1)', () => { 41 | const rawSignedModelString = compatData['STC-1.as_string']; 42 | const rawSignedModelJson: IRawSignedModelJson = JSON.parse(compatData['STC-1.as_json']); 43 | const imported = RawSignedModel.fromString(rawSignedModelString); 44 | 45 | const reExported = imported.toJson(); 46 | 47 | assert.deepEqual(reExported, rawSignedModelJson); 48 | assert.isFalse('previous_card_id' in reExported); 49 | }); 50 | 51 | it('imports raw signed model from string (STC-2)', () => { 52 | const rawSignedModelString = compatData['STC-2.as_string']; 53 | const imported = RawSignedModel.fromString(rawSignedModelString); 54 | assert.isOk(imported, 'imported successfully'); 55 | }); 56 | 57 | it('parses imported raw signed model (STC-2)', () => { 58 | const rawSignedModelString = compatData['STC-2.as_string']; 59 | const cardCrypto = initCardCrypto(); 60 | const imported = RawSignedModel.fromString(rawSignedModelString); 61 | 62 | const parsed = parseRawSignedModel(cardCrypto, imported); 63 | 64 | assert.isOk(parsed, 'parsed successfully'); 65 | assert.equal(parsed.identity, 'test'); 66 | assert.equal( 67 | Buffer.from(cardCrypto.exportPublicKey(parsed.publicKey)).toString('base64'), 68 | 'MCowBQYDK2VwAyEA6d9bQQFuEnU8vSmx9fDo0Wxec42JdNg4VR4FOr4/BUk=' 69 | ); 70 | assert.equal(parsed.version, '5.0'); 71 | assert.equal(parsed.createdAt.getTime(), 1515686245 * 1000); 72 | assert.equal( 73 | parsed.previousCardId, 74 | 'a666318071274adb738af3f67b8c7ec29d954de2cabfd71a942e6ea38e59fff9' 75 | ); 76 | assert.equal(parsed.signatures.length, 3); 77 | }); 78 | 79 | it('re-exports raw signed model (STC-2)', () => { 80 | const rawSignedModelString = compatData['STC-2.as_string']; 81 | const rawSignedModelJson: IRawSignedModelJson = JSON.parse(compatData['STC-2.as_json']); 82 | const imported = RawSignedModel.fromString(rawSignedModelString); 83 | 84 | const reExported = imported.toJson(); 85 | 86 | assert.deepEqual(reExported, rawSignedModelJson); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/__tests__/integration/data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "STC-1.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEUxTmpnMk1qUTFMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3ZFdKc2FXTmZhMlY1SWpvaVRVTnZkMEpSV1VSTE1sWjNRWGxGUVRaa09XSlJVVVoxUlc1Vk9IWlRiWGc1WmtSdk1GZDRaV00wTWtwa1RtYzBWbEkwUms5eU5DOUNWV3M5SWl3aWRtVnljMmx2YmlJNklqVXVNQ0o5Iiwic2lnbmF0dXJlcyI6W119", 3 | "STC-1.as_json": "{\"content_snapshot\":\"eyJjcmVhdGVkX2F0IjoxNTE1Njg2MjQ1LCJpZGVudGl0eSI6InRlc3QiLCJwdWJsaWNfa2V5IjoiTUNvd0JRWURLMlZ3QXlFQTZkOWJRUUZ1RW5VOHZTbXg5ZkRvMFd4ZWM0MkpkTmc0VlI0Rk9yNC9CVWs9IiwidmVyc2lvbiI6IjUuMCJ9\",\"signatures\":[]}", 4 | "STC-2.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEUxTmpnMk1qUTFMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3Y21WMmFXOTFjMTlqWVhKa1gybGtJam9pWVRZMk5qTXhPREEzTVRJM05HRmtZamN6T0dGbU0yWTJOMkk0WXpkbFl6STVaRGsxTkdSbE1tTmhZbVprTnpGaE9UUXlaVFpsWVRNNFpUVTVabVptT1NJc0luQjFZbXhwWTE5clpYa2lPaUpOUTI5M1FsRlpSRXN5Vm5kQmVVVkJObVE1WWxGUlJuVkZibFU0ZGxOdGVEbG1SRzh3VjNobFl6UXlTbVJPWnpSV1VqUkdUM0kwTDBKVmF6MGlMQ0oyWlhKemFXOXVJam9pTlM0d0luMD0iLCJzaWduYXR1cmVzIjpbeyJzaWduYXR1cmUiOiJNRkV3RFFZSllJWklBV1VEQkFJREJRQUVRTlhndWliWTFjRENmbnVKaFRLK2pYL1F2NnY1aTVUenFRczNlMWZXbGJpc2RVV1loK3MxMGdzTGtoZjgzd09xcm04WlhVQ3BqZ2tKbjgzVERhS1laUTg9Iiwic2lnbmVyIjoic2VsZiJ9LHsic2lnbmF0dXJlIjoiTUZFd0RRWUpZSVpJQVdVREJBSURCUUFFUU5YZ3VpYlkxY0RDZm51SmhUSytqWC9RdjZ2NWk1VHpxUXMzZTFmV2xiaXNkVVdZaCtzMTBnc0xraGY4M3dPcXJtOFpYVUNwamdrSm44M1REYUtZWlE4PSIsInNpZ25lciI6InZpcmdpbCJ9LHsic2lnbmF0dXJlIjoiTUZFd0RRWUpZSVpJQVdVREJBSURCUUFFUUNBM08zNVJrK2RvUlBIa0hoSkpLSnlGeHoyQVBEWk9TQlppNlFobUk3QlAzeVRiNjVnUll3dTBIdE5OWWRNUnNFcVZqOUlFS2h0RGVsZjRTS3BiSndvPSIsInNpZ25lciI6ImV4dHJhIn1dfQ==", 5 | "STC-2.as_json": "{\"content_snapshot\":\"eyJjcmVhdGVkX2F0IjoxNTE1Njg2MjQ1LCJpZGVudGl0eSI6InRlc3QiLCJwcmV2aW91c19jYXJkX2lkIjoiYTY2NjMxODA3MTI3NGFkYjczOGFmM2Y2N2I4YzdlYzI5ZDk1NGRlMmNhYmZkNzFhOTQyZTZlYTM4ZTU5ZmZmOSIsInB1YmxpY19rZXkiOiJNQ293QlFZREsyVndBeUVBNmQ5YlFRRnVFblU4dlNteDlmRG8wV3hlYzQySmROZzRWUjRGT3I0L0JVaz0iLCJ2ZXJzaW9uIjoiNS4wIn0=\",\"signatures\":[{\"signature\":\"MFEwDQYJYIZIAWUDBAIDBQAEQNXguibY1cDCfnuJhTK+jX/Qv6v5i5TzqQs3e1fWlbisdUWYh+s10gsLkhf83wOqrm8ZXUCpjgkJn83TDaKYZQ8=\",\"signer\":\"self\"},{\"signature\":\"MFEwDQYJYIZIAWUDBAIDBQAEQNXguibY1cDCfnuJhTK+jX/Qv6v5i5TzqQs3e1fWlbisdUWYh+s10gsLkhf83wOqrm8ZXUCpjgkJn83TDaKYZQ8=\",\"signer\":\"virgil\"},{\"signature\":\"MFEwDQYJYIZIAWUDBAIDBQAEQCA3O35Rk+doRPHkHhJJKJyFxz2APDZOSBZi6QhmI7BP3yTb65gRYwu0HtNNYdMRsEqVj9IEKhtDelf4SKpbJwo=\",\"signer\":\"extra\"}]}", 6 | "STC-3.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEUxTmpnMk1qUTFMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3ZFdKc2FXTmZhMlY1SWpvaVRVTnZkMEpSV1VSTE1sWjNRWGxGUVRaa09XSlJVVVoxUlc1Vk9IWlRiWGc1WmtSdk1GZDRaV00wTWtwa1RtYzBWbEkwUms5eU5DOUNWV3M5SWl3aWRtVnljMmx2YmlJNklqVXVNQ0o5Iiwic2lnbmF0dXJlcyI6W119", 7 | "STC-3.as_json": "{\"content_snapshot\":\"eyJjcmVhdGVkX2F0IjoxNTE1Njg2MjQ1LCJpZGVudGl0eSI6InRlc3QiLCJwdWJsaWNfa2V5IjoiTUNvd0JRWURLMlZ3QXlFQTZkOWJRUUZ1RW5VOHZTbXg5ZkRvMFd4ZWM0MkpkTmc0VlI0Rk9yNC9CVWs9IiwidmVyc2lvbiI6IjUuMCJ9\",\"signatures\":[]}", 8 | "STC-3.card_id": "3ed75603b6edbb8af38c187bdd884dbcda412a6ffb30b49dcbcfc04834b6258d", 9 | "STC-3.public_key_base64": "MCowBQYDK2VwAyEA6d9bQQFuEnU8vSmx9fDo0Wxec42JdNg4VR4FOr4/BUk=", 10 | "STC-4.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEUxTmpnMk1qUTFMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3ZFdKc2FXTmZhMlY1SWpvaVRVTnZkMEpSV1VSTE1sWjNRWGxGUVRaa09XSlJVVVoxUlc1Vk9IWlRiWGc1WmtSdk1GZDRaV00wTWtwa1RtYzBWbEkwUms5eU5DOUNWV3M5SWl3aWRtVnljMmx2YmlJNklqVXVNQ0o5Iiwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUZFd0RRWUpZSVpJQVdVREJBSURCUUFFUUp1VHhsUTdyK1JHMlA4RDEyT0ZPZGdQc0lEbVpNZDRVQk1JRzFjMUFtcW0vb2Mxd1JVems3Y2N6MVJiVFdFdDJYUCsxR2JrRjBaNnM2RllmMVFFVVFJPSIsInNpZ25lciI6InNlbGYifSx7InNpZ25hdHVyZSI6Ik1GRXdEUVlKWUlaSUFXVURCQUlEQlFBRVFKdVR4bFE3citSRzJQOEQxMk9GT2RnUHNJRG1aTWQ0VUJNSUcxYzFBbXFtL29jMXdSVXprN2NjejFSYlRXRXQyWFArMUdia0YwWjZzNkZZZjFRRVVRST0iLCJzaWduZXIiOiJ2aXJnaWwifSx7InNpZ25hdHVyZSI6Ik1GRXdEUVlKWUlaSUFXVURCQUlEQlFBRVFFdmpzVUZnekdnZFFITzVYcXhLYmJETjZjeWVZQk5Bb0c5R29oSVY1RGlvbEdtVVVYQURxRmlRbDN3SXNtelhkOCtjMTh1VXY5TXJXeUQxeE1OSEhRVT0iLCJzaWduZXIiOiJleHRyYSJ9XX0=", 11 | "STC-4.as_json": "{\"content_snapshot\":\"eyJjcmVhdGVkX2F0IjoxNTE1Njg2MjQ1LCJpZGVudGl0eSI6InRlc3QiLCJwdWJsaWNfa2V5IjoiTUNvd0JRWURLMlZ3QXlFQTZkOWJRUUZ1RW5VOHZTbXg5ZkRvMFd4ZWM0MkpkTmc0VlI0Rk9yNC9CVWs9IiwidmVyc2lvbiI6IjUuMCJ9\",\"signatures\":[{\"signature\":\"MFEwDQYJYIZIAWUDBAIDBQAEQJuTxlQ7r+RG2P8D12OFOdgPsIDmZMd4UBMIG1c1Amqm/oc1wRUzk7ccz1RbTWEt2XP+1GbkF0Z6s6FYf1QEUQI=\",\"signer\":\"self\"},{\"signature\":\"MFEwDQYJYIZIAWUDBAIDBQAEQJuTxlQ7r+RG2P8D12OFOdgPsIDmZMd4UBMIG1c1Amqm/oc1wRUzk7ccz1RbTWEt2XP+1GbkF0Z6s6FYf1QEUQI=\",\"signer\":\"virgil\"},{\"signature\":\"MFEwDQYJYIZIAWUDBAIDBQAEQEvjsUFgzGgdQHO5XqxKbbDN6cyeYBNAoG9GohIV5DiolGmUUXADqFiQl3wIsmzXd8+c18uUv9MrWyD1xMNHHQU=\",\"signer\":\"extra\"}]}", 12 | "STC-4.card_id": "3ed75603b6edbb8af38c187bdd884dbcda412a6ffb30b49dcbcfc04834b6258d", 13 | "STC-4.public_key_base64": "MCowBQYDK2VwAyEA6d9bQQFuEnU8vSmx9fDo0Wxec42JdNg4VR4FOr4/BUk=", 14 | "STC-4.signature_self_base64": "MFEwDQYJYIZIAWUDBAIDBQAEQJuTxlQ7r+RG2P8D12OFOdgPsIDmZMd4UBMIG1c1Amqm/oc1wRUzk7ccz1RbTWEt2XP+1GbkF0Z6s6FYf1QEUQI=", 15 | "STC-4.signature_virgil_base64": "MFEwDQYJYIZIAWUDBAIDBQAEQJuTxlQ7r+RG2P8D12OFOdgPsIDmZMd4UBMIG1c1Amqm/oc1wRUzk7ccz1RbTWEt2XP+1GbkF0Z6s6FYf1QEUQI=", 16 | "STC-4.signature_extra_base64": "MFEwDQYJYIZIAWUDBAIDBQAEQEvjsUFgzGgdQHO5XqxKbbDN6cyeYBNAoG9GohIV5DiolGmUUXADqFiQl3wIsmzXd8+c18uUv9MrWyD1xMNHHQU=", 17 | "STC-22.jwt": "eyJhbGciOiJWRURTNTEyIiwiY3R5IjoidmlyZ2lsLWp3dDt2PTEiLCJraWQiOiI4ZTYyNjQzNjc0ZDEwMGI4YTUyNjQ4NDc1YzIzYjhiYjg2YTFmMTE5ZTg5OTcwY2M2M2NkY2NjMDkyMGRmMmVhMmQ4Y2I3ZDZiNTRjNGE5ZTI4NGIyY2NmNWFjYjIyZWEzN2VmZWZjYjZlNDI3NGM0YjA0ZWQ3MjVlNDQ1NGNhMSIsInR5cCI6IkpXVCJ9.eyJhZGEiOnsidXNlcm5hbWUiOiJzb21lX3VzZXJuYW1lIn0sImV4cCI6MTUxODUxMzkwOSwiaWF0IjoxNTE4NTEzMzA5LCJpc3MiOiJ2aXJnaWwtMTM0OTdjM2M3OTVlM2E2YzMyNjQzYjBhNzY5NTdiNzBkMjMzMjA4MDc2MjQ2OWNkYmVjODlkNjM5MGU2ZGJkNyIsInN1YiI6ImlkZW50aXR5LXNvbWVfaWRlbnRpdHkifQ.MFEwDQYJYIZIAWUDBAIDBQAEQDMCvFjAaCBE5oLbFY9CAC-HPiPkBVr3qSZChbwRaRZADb6RZuljSpEvzoiqNGTxgUgqUiCefH-Tkb89tvjSdAY", 18 | "STC-22.api_public_key_base64": "MCowBQYDK2VwAyEAY9J8et78SmmE4GOm7f8fdew/eQB5YxKkwZeO3Z1IBt4=", 19 | "STC-22.api_key_id": "8e62643674d100b8a52648475c23b8bb86a1f119e89970cc63cdccc0920df2ea2d8cb7d6b54c4a9e284b2ccf5acb22ea37efefcb6e4274c4b04ed725e4454ca1", 20 | "STC-23.api_public_key_base64": "MCowBQYDK2VwAyEAY9J8et78SmmE4GOm7f8fdew/eQB5YxKkwZeO3Z1IBt4=", 21 | "STC-23.api_key_id": "8e62643674d100b8a52648475c23b8bb86a1f119e89970cc63cdccc0920df2ea2d8cb7d6b54c4a9e284b2ccf5acb22ea37efefcb6e4274c4b04ed725e4454ca1", 22 | "STC-23.app_id": "13497c3c795e3a6c32643b0a76957b70d2332080762469cdbec89d6390e6dbd7", 23 | "STC-23.api_private_key_base64": "MC4CAQAwBQYDK2VwBCIEIMxSaUI1QizmMvSPyTc26YsK21clGXKEztNKZ7NtrATP", 24 | "STC-10.private_key1_base64": "MC4CAQAwBQYDK2VwBCIEIHjOZk2swG5hi+3pQ2akLSBQbiabyNZDCVnqYtoM7QkF", 25 | "STC-10.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKMlpYSnphVzl1SWpvaU5TNHdJaXdpY0hWaWJHbGpYMnRsZVNJNklrMURiM2RDVVZsRVN6SldkMEY1UlVGM2FGZFpLekpFVVU5WlRGSnNURzFpZGtsRWNtcHllRUU1ZG01QlV5dG5kblJZVFZkTmNtNUJUamx6UFNJc0ltbGtaVzUwYVhSNUlqb2lhV1JsYm5ScGRIa2lMQ0pqY21WaGRHVmtYMkYwSWpveE5URTVNVE16TmpRd2ZRPT0iLCJzaWduYXR1cmVzIjpbeyJzaWduYXR1cmUiOiJNRkV3RFFZSllJWklBV1VEQkFJREJRQUVRQVVGbjFWVUpEZXZPS24raTBKZEl0S0kyQlA2MEcxR3dOa085cVRXcUU2eWlFUGxFcGhqZU1wUDJcL256V0l3MG5icFdaOUsxUkR1aTc3NXZTQ0VYSWcwPSIsInNpZ25lciI6InNlbGYifSx7InNpZ25hdHVyZSI6Ik1GRXdEUVlKWUlaSUFXVURCQUlEQlFBRVFDOFVEekRCOU84QTJUQ2FlR011Wlwvbm03eFVBSW5sUmtTeFhGbWx1MFZRSHdMRStkWXpJYzU5VTZraXV5YjJZa2ZvTUxtR2pXMUp2Qk5PTGJpTHMrQVk9Iiwic2lnbmVyIjoiZXh0cmEifSx7InNpZ25hdHVyZSI6Ik1GRXdEUVlKWUlaSUFXVURCQUlEQlFBRVFLVFwvTUdTVGk1TEdROTNkTkhkMlpUR0poeEdMQnN0cGpvbHNhZzAzdnFOQVNWUFBSN3A0cU9GQU9zUFVhXC9ieGJBTWEyMW5aWFlxQ1RqUmhRcnRwd3c4PSIsInNpZ25lciI6InZpcmdpbCJ9XX0=", 26 | "STC-11.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEUxTmpnMk1qUTFMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3ZFdKc2FXTmZhMlY1SWpvaVRVTnZkMEpSV1VSTE1sWjNRWGxGUVRaa09XSlJVVVoxUlc1Vk9IWlRiWGc1WmtSdk1GZDRaV00wTWtwa1RtYzBWbEkwUms5eU5DOUNWV3M5SWl3aWRtVnljMmx2YmlJNklqVXVNQ0o5Iiwic2lnbmF0dXJlcyI6W119", 27 | "STC-12.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEUxTmpnMk1qUTFMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3ZFdKc2FXTmZhMlY1SWpvaVRVTnZkMEpSV1VSTE1sWjNRWGxGUVRaa09XSlJVVVoxUlc1Vk9IWlRiWGc1WmtSdk1GZDRaV00wTWtwa1RtYzBWbEkwUms5eU5DOUNWV3M5SWl3aWRtVnljMmx2YmlJNklqVXVNQ0o5Iiwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUZFd0RRWUpZSVpJQVdVREJBSURCUUFFUUp1VHhsUTdyK1JHMlA4RDEyT0ZPZGdQc0lEbVpNZDRVQk1JRzFjMUFtcW0vb2Mxd1JVems3Y2N6MVJiVFdFdDJYUCsxR2JrRjBaNnM2RllmMVFFVVFJPSIsInNpZ25lciI6InNlbGYifV19", 28 | "STC-14.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEUxTmpnMk1qUTFMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3ZFdKc2FXTmZhMlY1SWpvaVRVTnZkMEpSV1VSTE1sWjNRWGxGUVRaa09XSlJVVVoxUlc1Vk9IWlRiWGc1WmtSdk1GZDRaV00wTWtwa1RtYzBWbEkwUms5eU5DOUNWV3M5SWl3aWRtVnljMmx2YmlJNklqVXVNQ0o5Iiwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUZFd0RRWUpZSVpJQVdVREJBSURCUUFFUUp1VHhsUTdyK1JHMlA4RDEyT0ZPZGdQc0lEbVpNZDRVQk1JRzFjMUFtcW0vb2Mxd1JVems3Y2N6MVJiVFdFdDJYUCsxR2JrRjBaNnM2RllmMVFFVVFJPSIsInNpZ25lciI6InZpcmdpbCJ9XX0=", 29 | "STC-15.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEUxTmpnMk1qUTFMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3ZFdKc2FXTmZhMlY1SWpvaVRVTnZkMEpSV1VSTE1sWjNRWGxGUVRaa09XSlJVVVoxUlc1Vk9IWlRiWGc1WmtSdk1GZDRaV00wTWtwa1RtYzBWbEkwUms5eU5DOUNWV3M5SWl3aWRtVnljMmx2YmlJNklqVXVNQ0o5Iiwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUZFd0RRWUpZSVpJQVdVREJBSURCUUFFUUd2Qk05Y2VGVGhnQnBrQm9RZnd0WWtxdGt1TWltWERLOHF4TWpWaGxQdWZocHptaG55VytCVXJ1L3hWTHBQSURsNlZnVGpsRUt1NGtIcVpRV2JpOXd3PSIsInNpZ25lciI6InNlbGYifV19", 30 | "STC-16.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEUxTmpnMk1qUTFMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3ZFdKc2FXTmZhMlY1SWpvaVRVTnZkMEpSV1VSTE1sWjNRWGxGUVRaa09XSlJVVVoxUlc1Vk9IWlRiWGc1WmtSdk1GZDRaV00wTWtwa1RtYzBWbEkwUms5eU5DOUNWV3M5SWl3aWRtVnljMmx2YmlJNklqVXVNQ0o5Iiwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUZFd0RRWUpZSVpJQVdVREJBSURCUUFFUUp1VHhsUTdyK1JHMlA4RDEyT0ZPZGdQc0lEbVpNZDRVQk1JRzFjMUFtcW0vb2Mxd1JVems3Y2N6MVJiVFdFdDJYUCsxR2JrRjBaNnM2RllmMVFFVVFJPSIsInNpZ25lciI6InNlbGYifSx7InNpZ25hdHVyZSI6Ik1GRXdEUVlKWUlaSUFXVURCQUlEQlFBRVFKdVR4bFE3citSRzJQOEQxMk9GT2RnUHNJRG1aTWQ0VUJNSUcxYzFBbXFtL29jMXdSVXprN2NjejFSYlRXRXQyWFArMUdia0YwWjZzNkZZZjFRRVVRST0iLCJzaWduZXIiOiJ2aXJnaWwifSx7InNpZ25hdHVyZSI6Ik1GRXdEUVlKWUlaSUFXVURCQUlEQlFBRVFKdjNzemdyY3o2cW90aUUyUTJWdktoaDF2UFZCNnBPUzlaWG5YeVBOb2tadVFpRkZvbXUwVEh3UFJ5a0JmWmtEUE9xcG5tZ0lIM1o2SmpRcEpobjZnaz0iLCJzaWduZXIiOiJleHRyYSJ9XX0=", 31 | "STC-16.public_key1_base64": "MCowBQYDK2VwAyEAvwjZPGOnEdPORvse7vlaDYooR1IbJW1yiYmVALD7vxQ=", 32 | "STC-28.jwt": "eyJhbGciOiJWRURTNTEyIiwiY3R5IjoidmlyZ2lsLWp3dDt2PTEiLCJraWQiOiJlMGU0ZTYxZDYzZDQyN2QxODVkNTNjY2NiYTQ3YjY2NTdhMjA2OWYwMTdlNGVkNWQwODgwMjg0NmFhMWFiYTE4YzM1ZTQwYjNiZjRmZmZiZmNjN2Q1NTQ4YzMyODFmY2YyNGQzZDRkMjFiZjg1ZTU1YmI5NGE4ZDA1ODI5ZGJjNiIsInR5cCI6IkpXVCJ9.eyJhZGEiOnsidXNlcm5hbWUiOiJzb21lX3VzZXJuYW1lIn0sImV4cCI6MTUxODUxMzQzMSwiaWF0IjoxNTE4NTEzMzExLCJpc3MiOiJ2aXJnaWwtMDA4ODlhNjAyNmVkOTM1Y2VlY2NkNjk1NWQ4Nzc4ZmMwNWVlMzUzODAzOGM2ZmVlZTBhZTdlYjFjMGQ3ZGI0YiIsInN1YiI6ImlkZW50aXR5LXNvbWVfaWRlbnRpdHkifQ.MFEwDQYJYIZIAWUDBAIDBQAEQJact35eWOqNMu7JfLrk5vGYDK88q41-78jnrVtmOnG5hKolb3wkp4LfI-Uqej6_bDo_wXN3TRg7oCl_chCVjA4", 33 | "STC-28.jwt_identity": "some_identity", 34 | "STC-28.jwt_app_id": "00889a6026ed935ceeccd6955d8778fc05ee3538038c6feee0ae7eb1c0d7db4b", 35 | "STC-28.jw_issuer": "virgil-00889a6026ed935ceeccd6955d8778fc05ee3538038c6feee0ae7eb1c0d7db4b", 36 | "STC-28.jwt_subject": "identity-some_identity", 37 | "STC-28.jwt_additional_data": "{\"username\":\"some_username\"}", 38 | "STC-28.jwt_expires_at": "1518513431", 39 | "STC-28.jwt_issued_at": "1518513311", 40 | "STC-28.jwt_algorithm": "VEDS512", 41 | "STC-28.jwt_api_key_id": "e0e4e61d63d427d185d53cccba47b6657a2069f017e4ed5d08802846aa1aba18c35e40b3bf4fffbfcc7d5548c3281fcf24d3d4d21bf85e55bb94a8d05829dbc6", 42 | "STC-28.jwt_content_type": "virgil-jwt;v=1", 43 | "STC-28.jwt_type": "JWT", 44 | "STC-28.jwt_signature_base64": "MFEwDQYJYIZIAWUDBAIDBQAEQJact35eWOqNMu7JfLrk5vGYDK88q41+78jnrVtmOnG5hKolb3wkp4LfI+Uqej6/bDo/wXN3TRg7oCl/chCVjA4=", 45 | "STC-29.jwt": "eyJhbGciOiJWRURTNTEyIiwia2lkIjoiZTc5NTNiODc0ODQ3YTM2NGJmZGQyNDdhYzFiMjc4YTYiLCJ0eXAiOiJKV1QiLCJjdHkiOiJ2aXJnaWwtand0O3Y9MSJ9.eyJpc3MiOiJ2aXJnaWwtODg2MzBhZWVmYjFhNDllNWEwYzQ5MWE5NGVkMzZmMzc1MGEwNWVhMWIwYTNkNDczMTlhYjgwNzg2ZmUxOWIxNyIsInN1YiI6ImlkZW50aXR5LXNvbWVfaWRlbnRpdHkiLCJpYXQiOjE1NTAwNzI4NTAsImV4cCI6MTcwNzc1Mjg1MCwiYWRhIjp7InVzZXJuYW1lIjoic29tZV91c2VybmFtZSJ9fQ.MFEwDQYJYIZIAWUDBAIDBQAEQE_mAKrJgiCyyNpIcl6hXm4e8Tpg17q3OhDAvoEpmMDelrbZnAw-Y2kI8TtzBDSHcYVFBEIGCpzRjn-_D0WfpgQ", 46 | "STC-29.jwt_identity": "some_identity", 47 | "STC-29.jwt_app_id": "88630aeefb1a49e5a0c491a94ed36f3750a05ea1b0a3d47319ab80786fe19b17", 48 | "STC-29.jw_issuer": "virgil-88630aeefb1a49e5a0c491a94ed36f3750a05ea1b0a3d47319ab80786fe19b17", 49 | "STC-29.jwt_subject": "identity-some_identity", 50 | "STC-29.jwt_additional_data": "{\"username\":\"some_username\"}", 51 | "STC-29.jwt_expires_at": "1707752850", 52 | "STC-29.jwt_issued_at": "1550072850", 53 | "STC-29.jwt_algorithm": "VEDS512", 54 | "STC-29.jwt_api_key_id": "e7953b874847a364bfdd247ac1b278a6", 55 | "STC-29.jwt_content_type": "virgil-jwt;v=1", 56 | "STC-29.jwt_type": "JWT", 57 | "STC-29.jwt_signature_base64": "MFEwDQYJYIZIAWUDBAIDBQAEQE/mAKrJgiCyyNpIcl6hXm4e8Tpg17q3OhDAvoEpmMDelrbZnAw+Y2kI8TtzBDSHcYVFBEIGCpzRjn+/D0WfpgQ=", 58 | "STC-34.private_key_base64": "MC4CAQAwBQYDK2VwBCIEIDsKxdR3vYOQpyk+fxssPu3Cgt4lRdzuzl0DS5AKGFDx", 59 | "STC-34.public_key_base64": "MCowBQYDK2VwAyEAL45H8kBgBNXSuo4RCmbPOZbKunck1UQm/xOLbaPSIB4=", 60 | "STC-34.self_signature_snapshot_base64": "eyJpbmZvIjoic29tZV9hZGRpdGlvbmFsX2luZm8ifQ==", 61 | "STC-34.content_snapshot_base64": "eyJjcmVhdGVkX2F0IjoxNTE4NTEzMzExLCJpZGVudGl0eSI6InRlc3QiLCJwdWJsaWNfa2V5IjoiTUNvd0JRWURLMlZ3QXlFQUw0NUg4a0JnQk5YU3VvNFJDbWJQT1piS3VuY2sxVVFtL3hPTGJhUFNJQjQ9IiwidmVyc2lvbiI6IjUuMCJ9", 62 | "STC-34.as_string": "eyJjb250ZW50X3NuYXBzaG90IjoiZXlKamNtVmhkR1ZrWDJGMElqb3hOVEU0TlRFek16RXhMQ0pwWkdWdWRHbDBlU0k2SW5SbGMzUWlMQ0p3ZFdKc2FXTmZhMlY1SWpvaVRVTnZkMEpSV1VSTE1sWjNRWGxGUVV3ME5VZzRhMEpuUWs1WVUzVnZORkpEYldKUVQxcGlTM1Z1WTJzeFZWRnRMM2hQVEdKaFVGTkpRalE5SWl3aWRtVnljMmx2YmlJNklqVXVNQ0o5Iiwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUZFd0RRWUpZSVpJQVdVREJBSURCUUFFUUZtQVJieDJoTTZ2cFUzM1NKNXhPTzBNRkpzUG03MFUwNGwwWGtsVXpqdkJGNzhMSjU0YjA0ajNIb2lweE8vdVdyTHpLM09ZZ2VaOGNUYlhzTDkyeUFVPSIsInNpZ25lciI6InNlbGYiLCJzbmFwc2hvdCI6ImV5SnBibVp2SWpvaWMyOXRaVjloWkdScGRHbHZibUZzWDJsdVptOGlmUT09In1dfQ==" 63 | } 64 | -------------------------------------------------------------------------------- /src/__tests__/integration/data/index.ts: -------------------------------------------------------------------------------- 1 | import compatData_ from './data.json'; 2 | // fancy way to import json that works in both node.js and the browser 3 | export const compatData = compatData_; 4 | -------------------------------------------------------------------------------- /src/__tests__/unit/CachingJwtProvider.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { initCrypto, VirgilCrypto } from 'virgil-crypto'; 3 | import { addSeconds, getUnixTimestamp } from '../../Lib/timestamp'; 4 | import { GetJwtCallback, Jwt } from '../../Auth/Jwt'; 5 | import { CachingJwtProvider } from '../../Auth/AccessTokenProviders'; 6 | 7 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 8 | 9 | describe ('CachingJwtProvider', async () => { 10 | let virgilCrypto: VirgilCrypto; 11 | 12 | await initCrypto(); 13 | virgilCrypto = new VirgilCrypto(); 14 | 15 | const generateJwt = (expiresAt: Date): Jwt => { 16 | return new Jwt( 17 | { 18 | alg: 'stub', 19 | typ: 'stub', 20 | cty: 'stub', 21 | kid: 'stub' 22 | }, 23 | { 24 | iss: 'stub', 25 | sub: 'stub', 26 | iat: getUnixTimestamp(new Date), 27 | exp: getUnixTimestamp(expiresAt) 28 | }, 29 | virgilCrypto.getRandomBytes(16).toString('base64') 30 | ); 31 | }; 32 | 33 | describe ('constructor', () => { 34 | it ('throws when renewJwtFn is not a function', () => { 35 | assert.throws(() => { 36 | new CachingJwtProvider({} as GetJwtCallback); 37 | }, /must be a function/); 38 | }); 39 | 40 | it ('accepts initial token value as Jwt instance', () => { 41 | const initialJwt = generateJwt(addSeconds(new Date, 60)); 42 | const getJwtCallback = sinon.stub().rejects('should have used initial token'); 43 | 44 | const provider = new CachingJwtProvider(getJwtCallback, initialJwt); 45 | 46 | return assert.eventually.deepEqual( 47 | provider.getToken({ service: 'stub', operation: 'stub' }), 48 | initialJwt 49 | ); 50 | }); 51 | 52 | it ('accepts initial token value as stirng', () => { 53 | const initialJwt = generateJwt(addSeconds(new Date, 60)); 54 | const getJwtCallback = sinon.stub().rejects('should have used inital token'); 55 | 56 | const provider = new CachingJwtProvider(getJwtCallback, initialJwt.toString()); 57 | 58 | return assert.eventually.deepEqual( 59 | provider.getToken({ service: 'stub', operation: 'stub' }), 60 | initialJwt 61 | ); 62 | }); 63 | 64 | it ('throws when initial token string is malformed', () => { 65 | const initialJwt = 'not_a_jwt'; 66 | const getJwtCallback = sinon.stub(); 67 | 68 | assert.throws(() => { 69 | new CachingJwtProvider(getJwtCallback, initialJwt); 70 | }, /Wrong JWT/); 71 | }); 72 | 73 | it ('throws Error when initialToken is neither a Jwt nor a string', () => { 74 | const initialJwt = 1; 75 | const getJwtCallback = sinon.stub(); 76 | 77 | assert.throws(() => { 78 | new CachingJwtProvider(getJwtCallback, initialJwt as any); 79 | }, 'Expected "initialToken" to be a string or an instance of Jwt, got number'); 80 | }); 81 | }); 82 | 83 | describe ('getToken', () => { 84 | it ('works with synchronous callback', () => { 85 | const expectedJwt = generateJwt(addSeconds(new Date, 60)); 86 | const getJwtCallback = sinon.stub().returns(expectedJwt); 87 | 88 | const provider = new CachingJwtProvider(getJwtCallback); 89 | 90 | return assert.eventually.deepEqual( 91 | provider.getToken({ service: 'stub', operation: 'stub' }), 92 | expectedJwt 93 | ); 94 | }); 95 | 96 | it ('works with asynchronous callback', () => { 97 | const expectedJwt = generateJwt(addSeconds(new Date, 60)); 98 | const getJwtCallback = sinon.stub().returns(Promise.resolve(expectedJwt)); 99 | 100 | const provider = new CachingJwtProvider(getJwtCallback); 101 | 102 | return assert.eventually.deepEqual( 103 | provider.getToken({ service: 'stub', operation: 'stub' }), 104 | expectedJwt 105 | ); 106 | }); 107 | 108 | it ('caches the token for subsequent calls', () => { 109 | const getJwtCallback = sinon.stub().returns(generateJwt(addSeconds(new Date, 60))); 110 | 111 | const provider = new CachingJwtProvider(getJwtCallback); 112 | return assert.eventually.equal( 113 | Promise.all( 114 | new Array(10).fill(0).map(() => 115 | provider.getToken({ service: 'stub', operation: 'stub' }) 116 | ) 117 | ).then(() => getJwtCallback.callCount), 118 | 1, 119 | 'the user-provided callback is called only once' 120 | ); 121 | }); 122 | 123 | it ('gets new token when the cached one expires', () => { 124 | const getJwtCallback = sinon.stub(); 125 | // token that has less than 5 seconds left to live should be considered expired 126 | const expiredToken = generateJwt(addSeconds(new Date, 1)); 127 | const freshToken = generateJwt(addSeconds(new Date, 60)); 128 | getJwtCallback.onCall(0).returns(expiredToken); 129 | getJwtCallback.onCall(1).returns(freshToken); 130 | 131 | const provider = new CachingJwtProvider(getJwtCallback); 132 | 133 | return assert.eventually.equal( 134 | provider.getToken({ service: 'stub', operation: 'stub' }) 135 | .then(() => provider.getToken({ service: 'stub', operation: 'stub' })) 136 | .then(() => getJwtCallback.callCount), 137 | 2, 138 | 'the user-provided callback is called twice' 139 | ); 140 | }); 141 | 142 | it ('converts string tokens to Jwt instances', () => { 143 | const expectedJwt = generateJwt(addSeconds(new Date, 60)); 144 | const getJwtCallback = sinon.stub().returns(expectedJwt.toString()); 145 | 146 | const provider = new CachingJwtProvider(getJwtCallback); 147 | 148 | return provider.getToken({ service: 'stub', operation: 'stub' }).then(actual => { 149 | const actualJwt = actual as Jwt; 150 | assert.deepEqual(actualJwt.header, expectedJwt.header); 151 | assert.deepEqual(actualJwt.body, expectedJwt.body); 152 | assert.equal(actualJwt.signature, expectedJwt.signature); 153 | }); 154 | }); 155 | 156 | it ('rejects if the token string is malformed', () => { 157 | const getJwtCallback = sinon.stub().returns('no_a_jwt'); 158 | 159 | const provider = new CachingJwtProvider(getJwtCallback); 160 | 161 | return assert.isRejected( 162 | provider.getToken({ service: 'stub', operation: 'stub' }), 163 | /Wrong JWT/ 164 | ); 165 | }); 166 | 167 | it ('gets new token when inital token expires (STC-40)', function () { 168 | this.timeout(15000); 169 | const initialToken = generateJwt(addSeconds(new Date, 10)); 170 | const freshToken = generateJwt(addSeconds(new Date, 120)); 171 | const getJwtCallback = sinon.stub().resolves(freshToken); 172 | 173 | const provider = new CachingJwtProvider(getJwtCallback, initialToken); 174 | 175 | return provider.getToken({ service: 'stub', operation: 'stub' }) 176 | .then(token => { 177 | assert.deepEqual(token, initialToken); 178 | return sleep(3000); 179 | }) 180 | .then(() => provider.getToken({ service: 'stub', operation: 'stub' })) 181 | .then(token => { 182 | assert.deepEqual(token, initialToken); 183 | return sleep(9000); 184 | }) 185 | .then(() => provider.getToken({ service: 'stub', operation: 'stub' })) 186 | .then(token => { 187 | assert.deepEqual(token, freshToken); 188 | assert.equal(getJwtCallback.callCount, 1); 189 | }); 190 | }); 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /src/__tests__/unit/CallbackJwtProvider.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { initCrypto, VirgilCrypto } from 'virgil-crypto'; 4 | import { CallbackJwtProvider } from '../../Auth/AccessTokenProviders'; 5 | import { GetJwtCallback, Jwt } from '../../Auth/Jwt'; 6 | import { getUnixTimestamp } from '../../Lib/timestamp'; 7 | 8 | describe ('CallbackJwtProvider', () => { 9 | let virgilCrypto: VirgilCrypto; 10 | 11 | before(async () => { 12 | await initCrypto(); 13 | virgilCrypto = new VirgilCrypto(); 14 | }); 15 | 16 | const generateJwt = () => { 17 | return new Jwt( 18 | { 19 | alg: 'stub', 20 | typ: 'stub', 21 | cty: 'stub', 22 | kid: 'stub' 23 | }, 24 | { 25 | iss: 'stub', 26 | sub: 'stub', 27 | iat: getUnixTimestamp(new Date), 28 | exp: getUnixTimestamp(new Date(Date.now() + 10000)) 29 | }, 30 | virgilCrypto.getRandomBytes(16).toString('base64') 31 | ); 32 | }; 33 | 34 | describe ('constructor', () => { 35 | it ('throws when getJwtFn is not a function', () => { 36 | assert.throws(() => { 37 | new CallbackJwtProvider({} as GetJwtCallback); 38 | }, /must be a function/); 39 | }); 40 | }); 41 | 42 | describe ('getToken', () => { 43 | it ('works with synchronous callback', () => { 44 | const expectedJwt = generateJwt(); 45 | const getJwtCallback = sinon.stub().returns(expectedJwt); 46 | 47 | const provider = new CallbackJwtProvider(getJwtCallback); 48 | 49 | return assert.eventually.deepEqual( 50 | provider.getToken({ service: 'stub', operation: 'stub' }), 51 | expectedJwt 52 | ); 53 | }); 54 | 55 | it ('works with asynchronous callback', () => { 56 | const expectedJwt = generateJwt(); 57 | const getJwtCallback = sinon.stub().returns(Promise.resolve(expectedJwt)); 58 | 59 | const provider = new CallbackJwtProvider(getJwtCallback); 60 | 61 | return assert.eventually.deepEqual( 62 | provider.getToken({ service: 'stub', operation: 'stub' }), 63 | expectedJwt 64 | ); 65 | }); 66 | 67 | it ('converts string tokens to Jwt instances', () => { 68 | const expectedJwt = generateJwt(); 69 | const getJwtCallback = sinon.stub().returns(expectedJwt.toString()); 70 | 71 | const provider = new CallbackJwtProvider(getJwtCallback); 72 | 73 | return provider.getToken({ service: 'stub', operation: 'stub' }).then(actual => { 74 | const actualJwt = actual as Jwt; 75 | assert.deepEqual(actualJwt.header, expectedJwt.header); 76 | assert.deepEqual(actualJwt.body, expectedJwt.body); 77 | assert.equal(actualJwt.signature, expectedJwt.signature); 78 | }); 79 | }); 80 | 81 | it ('rejects if the token string is malformed', () => { 82 | const getJwtCallback = sinon.stub().returns('no_a_jwt'); 83 | 84 | const provider = new CallbackJwtProvider(getJwtCallback); 85 | 86 | return assert.isRejected( 87 | provider.getToken({ service: 'stub', operation: 'stub' }), 88 | /Wrong JWT/ 89 | ); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/__tests__/unit/CardClient.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { CardClient } from '../../Client/CardClient'; 4 | import { Response } from '../../Lib/fetch'; 5 | import { IConnection } from '../../Client/Connection'; 6 | import { RawSignedModel } from '../../Cards/RawSignedModel'; 7 | 8 | describe('CardClient', () => { 9 | describe('publishCard', () => { 10 | it ('rejects with custom error on http 401', () => { 11 | const connectionStub = { 12 | get: sinon.stub().throws(), 13 | post: sinon.stub().returns( 14 | Promise.resolve( 15 | new Response( 16 | JSON.stringify({ code: '40100', message: 'Invalid token' }), 17 | { status: 401, statusText: 'Unauthorized' } 18 | ) 19 | ) 20 | ) 21 | }; 22 | 23 | const client = new CardClient(connectionStub as IConnection); 24 | 25 | return assert.isRejected( 26 | client.publishCard({} as RawSignedModel, 'not_a_valid_jwt'), 27 | 'Invalid token' 28 | ); 29 | }); 30 | 31 | it ('rejects with custom error on http 400', () => { 32 | const connectionStub = { 33 | get: sinon.stub().throws(), 34 | post: sinon.stub().returns( 35 | Promise.resolve( 36 | new Response( 37 | JSON.stringify({ code: '40000', message: 'Invalid identity' }), 38 | { status: 400, statusText: 'BadRequest' } 39 | ) 40 | ) 41 | ) 42 | }; 43 | const client = new CardClient(connectionStub as IConnection); 44 | 45 | return assert.isRejected( 46 | client.publishCard({} as RawSignedModel, 'valid_jwt'), 47 | 'Invalid identity' 48 | ); 49 | }); 50 | }); 51 | 52 | describe('getCard', () => { 53 | it ('rejects with custom error on http 401', () => { 54 | const connectionStub = { 55 | get: sinon.stub().returns( 56 | Promise.resolve( 57 | new Response( 58 | JSON.stringify({ code: '40100', message: 'Invalid token' }), 59 | { status: 401, statusText: 'Unauthorized' } 60 | ) 61 | ) 62 | ), 63 | post: sinon.stub().throws() 64 | }; 65 | 66 | const client = new CardClient(connectionStub as IConnection); 67 | 68 | return assert.isRejected( 69 | client.getCard('valid_card_id','not_a_valid_jwt'), 70 | 'Invalid token' 71 | ); 72 | }); 73 | }); 74 | 75 | describe('searchCards', () => { 76 | it ('rejects with custom error on http 401', () => { 77 | const connectionStub = { 78 | get: sinon.stub().throws(), 79 | post: sinon.stub().returns( 80 | Promise.resolve( 81 | new Response( 82 | JSON.stringify({ code: '40100', message: 'Invalid token' }), 83 | { status: 401, statusText: 'Unauthorized' } 84 | ) 85 | ) 86 | ) 87 | }; 88 | 89 | const client = new CardClient(connectionStub as IConnection); 90 | 91 | return assert.isRejected( 92 | client.searchCards(['valid_card_identity'],'not_a_valid_jwt'), 93 | 'Invalid token' 94 | ); 95 | }); 96 | }); 97 | 98 | describe('revokeCard', () => { 99 | it ('rejects with custom error on http 401', () => { 100 | const connectionStub = { 101 | post: sinon.stub().returns( 102 | Promise.resolve( 103 | new Response( 104 | JSON.stringify({ code: '40100', message: 'Invalid token' }), 105 | { status: 401, statusText: 'Unauthorized' } 106 | ) 107 | ) 108 | ), 109 | get: sinon.stub().throws() 110 | }; 111 | 112 | const client = new CardClient(connectionStub as IConnection); 113 | 114 | return assert.isRejected( 115 | client.revokeCard('valid_card_id','not_a_valid_jwt'), 116 | 'Invalid token' 117 | ); 118 | }); 119 | 120 | it ('makes request with correct endpoint url and method', async () => { 121 | const connectionStub = { 122 | post: sinon.stub().returns(Promise.resolve(new Response(null, { status: 200, statusText: 'OK' }))), 123 | get: sinon.stub().throws() 124 | }; 125 | const client = new CardClient(connectionStub as IConnection); 126 | 127 | await client.revokeCard('test_card_id', 'test_jwt'); 128 | 129 | assert.isTrue(connectionStub.post.calledOnce); 130 | assert.equal(connectionStub.post.getCall(0).args[0], '/card/v5/actions/revoke/test_card_id'); 131 | assert.equal(connectionStub.post.getCall(0).args[1], 'test_jwt'); 132 | }); 133 | 134 | it ('rejects when card_id is not provided', () => { 135 | const connectionStub = { 136 | post: sinon.stub(), 137 | get: sinon.stub() 138 | }; 139 | 140 | const client = new CardClient(connectionStub as IConnection); 141 | 142 | return assert.isRejected( 143 | client.revokeCard(undefined as any, 'jwt'), 144 | '`cardId` should not be empty' 145 | ); 146 | }); 147 | 148 | it ('rejects when jwt is not provided', () => { 149 | const connectionStub = { 150 | post: sinon.stub(), 151 | get: sinon.stub() 152 | }; 153 | 154 | const client = new CardClient(connectionStub as IConnection); 155 | 156 | return assert.isRejected( 157 | client.revokeCard('card_id', undefined as any), 158 | '`accessToken` should not be empty' 159 | ); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/__tests__/unit/ConstAccesTokenProvider.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { ConstAccessTokenProvider } from '../../Auth/AccessTokenProviders'; 3 | 4 | describe ('ConstAccessTokenProvider', () => { 5 | describe ('constructor', () => { 6 | it ('throws when accessToken is null or undefined', () => { 7 | assert.throws(() => { 8 | new ConstAccessTokenProvider(undefined as any); 9 | }, TypeError); 10 | }); 11 | }); 12 | 13 | describe ('getToken', () => { 14 | it ('returns a Promise fulfilled with the access token', () => { 15 | const expectedAccessToken = { 'stub': 'stub', identity: () => 'stub' }; 16 | 17 | const provider = new ConstAccessTokenProvider(expectedAccessToken); 18 | 19 | return assert.eventually.deepEqual( 20 | provider.getToken({ service: 'stub', operation: 'stub' }), 21 | expectedAccessToken 22 | ); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/__tests__/unit/GeneratorJwtProvider.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { GeneratorJwtProvider } from '../../Auth/AccessTokenProviders'; 3 | 4 | describe ('GeneratorJwtProvider', () => { 5 | describe ('constructor', () => { 6 | it ('throws when jwtGenerator is null or undefined', () => { 7 | assert.throws(() => { 8 | new GeneratorJwtProvider(undefined as any); 9 | }, TypeError); 10 | }); 11 | }); 12 | 13 | describe ('getToken', () => { 14 | it ('delegates to generator', () => { 15 | const expectedAccessToken = { 'stub': 'stub' }; 16 | const generatorStub = { 17 | generateToken: sinon.stub().returns(expectedAccessToken) 18 | }; 19 | 20 | const provider = new GeneratorJwtProvider(generatorStub as any); 21 | 22 | assert.eventually.deepEqual( 23 | provider.getToken({ service: 'stub', operation: 'stub' }), 24 | expectedAccessToken 25 | ); 26 | }); 27 | 28 | it ('passes identity from context to generator', () => { 29 | const generatorStub = { 30 | generateToken: sinon.stub() 31 | }; 32 | 33 | const provider = new GeneratorJwtProvider(generatorStub as any); 34 | 35 | assert.isFulfilled( 36 | provider.getToken({ service: 'stub', operation: 'stub', identity: 'fake_identity' }) 37 | .then(() => assert.calledWith(generatorStub.generateToken, 'fake_identity')) 38 | ); 39 | }); 40 | 41 | it ('passes defaultIdentity to generator if none is provided in the context', () => { 42 | const generatorStub = { 43 | generateToken: sinon.stub() 44 | }; 45 | 46 | const provider = new GeneratorJwtProvider( 47 | generatorStub as any, 48 | undefined, 49 | 'fake_default_identity' 50 | ); 51 | 52 | assert.isFulfilled( 53 | provider.getToken({ service: 'stub', operation: 'stub' }) 54 | .then(() => assert.calledWith(generatorStub.generateToken, 'fake_default_identity')) 55 | ); 56 | }); 57 | 58 | it ('passes empty string as identity to generator if none is provided', () => { 59 | const generatorStub = { 60 | generateToken: sinon.stub() 61 | }; 62 | 63 | const provider = new GeneratorJwtProvider(generatorStub as any); // no defaultIdentity 64 | 65 | assert.isFulfilled( 66 | provider.getToken({ service: 'stub', operation: 'stub' }) // no context identity 67 | .then(() => assert.calledWith(generatorStub.generateToken, '')) 68 | ); 69 | }); 70 | 71 | it ('passes additional data to generator', () => { 72 | const generatorStub = { 73 | generateToken: sinon.stub() 74 | }; 75 | 76 | const provider = new GeneratorJwtProvider(generatorStub as any, { additional: 'data' }); 77 | 78 | assert.isFulfilled( 79 | provider.getToken({ service: 'stub', operation: 'stub', identity: 'fake_identity' }) 80 | .then(() => assert.calledWith( 81 | generatorStub.generateToken, 82 | 'fake_identity', 83 | { additional: 'data' } 84 | ) 85 | ) 86 | ); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/__tests__/unit/JwtGenerator.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-nocheck 3 | import { initCrypto, VirgilCrypto, VirgilAccessTokenSigner } from 'virgil-crypto'; 4 | import { JwtGenerator } from '../../Auth/JwtGenerator'; 5 | import { JwtContentType, VirgilContentType, IssuerPrefix, SubjectPrefix } from '../../Auth/jwt-constants'; 6 | import { getUnixTimestamp } from '../../Lib/timestamp'; 7 | 8 | describe('JwtGenerator', () => { 9 | let virgilCrypto: VirgilCrypto; 10 | let accessTokenSigner: VirgilAccessTokenSigner; 11 | 12 | before(async () => { 13 | await initCrypto(); 14 | virgilCrypto = new VirgilCrypto(); 15 | accessTokenSigner = new VirgilAccessTokenSigner(virgilCrypto); 16 | }); 17 | 18 | describe('constructor', () => { 19 | it('throws when options are not provided', () => { 20 | assert.throws(() => { 21 | new JwtGenerator(null as any); 22 | }, 'JwtGenerator options must be provided'); 23 | }); 24 | 25 | it('throws when API Key ID is not provided', () => { 26 | assert.throws(() => { 27 | new JwtGenerator({ 28 | apiKey: virgilCrypto.generateKeys().privateKey, 29 | appId: 'irrelevant', 30 | accessTokenSigner 31 | } as any); 32 | }, '`apiKeyId` is required'); 33 | }); 34 | 35 | it('throws when API Key is not provided', () => { 36 | assert.throws(() => { 37 | new JwtGenerator({ 38 | apiKeyId: 'irrelevant', 39 | appId: 'irrelevant', 40 | accessTokenSigner 41 | } as any); 42 | }, '`apiKey` is required'); 43 | }); 44 | 45 | it('throws when App ID is not provided', () => { 46 | assert.throws(() => { 47 | new JwtGenerator({ 48 | apiKey: virgilCrypto.generateKeys().privateKey, 49 | apiKeyId: 'irrelevant', 50 | accessTokenSigner 51 | } as any); 52 | }, '`appId` is required'); 53 | }); 54 | 55 | it('throws when accessTokenSigner is not provided', () => { 56 | assert.throws(() => { 57 | new JwtGenerator({ 58 | apiKey: virgilCrypto.generateKeys().privateKey, 59 | apiKeyId: 'irrelevant', 60 | appId: 'irrelevant' 61 | } as any); 62 | }, '`accessTokenSigner` is required'); 63 | }); 64 | 65 | it('uses "millisecondsToLive" value provided in options', () => { 66 | const expectedMillisecondsToLive = 99999; 67 | const generator = new JwtGenerator({ 68 | millisecondsToLive: expectedMillisecondsToLive, 69 | apiKey: virgilCrypto.generateKeys().privateKey, 70 | apiKeyId: 'irrelevant', 71 | appId: 'irrelevant', 72 | accessTokenSigner 73 | }); 74 | 75 | assert.equal(generator.millisecondsToLive, expectedMillisecondsToLive); 76 | }); 77 | 78 | it('uses 20 minutes by default for "millisecondsToLive"', () => { 79 | const expectedMillisecondsToLive = 20 * 60 * 1000; 80 | const generator = new JwtGenerator({ 81 | apiKey: virgilCrypto.generateKeys().privateKey, 82 | apiKeyId: 'irrelevant', 83 | appId: 'irrelevant', 84 | accessTokenSigner 85 | }); 86 | 87 | assert.equal(generator.millisecondsToLive, expectedMillisecondsToLive); 88 | }); 89 | }); 90 | 91 | describe('generateToken', () => { 92 | it('throws when identity is not provided', () => { 93 | const generator = new JwtGenerator({ 94 | apiKey: virgilCrypto.generateKeys().privateKey, 95 | apiKeyId: 'irrelevant', 96 | appId: 'irrelevant', 97 | accessTokenSigner 98 | }); 99 | 100 | assert.throws(() => { 101 | generator.generateToken(undefined as any); 102 | }, '`identity` is required'); 103 | }); 104 | 105 | it('generates a valid JWT', () => { 106 | const keypair = virgilCrypto.generateKeys(); 107 | const generator = new JwtGenerator({ 108 | apiKey: keypair.privateKey, 109 | apiKeyId: Buffer.from(keypair.privateKey.identifier).toString('base64'), 110 | appId: 'my_app_id', 111 | accessTokenSigner 112 | }); 113 | 114 | const expectedIdentity = 'test_identity'; 115 | const expectedAda = { additional: 'data' }; 116 | const token = generator.generateToken(expectedIdentity, expectedAda); 117 | 118 | assert.isDefined(token.header, 'JWT has header'); 119 | assert.isDefined(token.body, 'JWT has body'); 120 | assert.isDefined(token.signature, 'JWT has signature'); 121 | 122 | assert.equal(token.header.alg, accessTokenSigner.getAlgorithm(), 'algorithm is correct'); 123 | assert.equal(token.header.kid, Buffer.from(keypair.privateKey.identifier).toString('base64'), 'key id is correct'); 124 | assert.equal(token.header.typ, JwtContentType, 'token type is correct'); 125 | assert.equal(token.header.cty, VirgilContentType, 'content type is correct'); 126 | 127 | assert.equal(token.body.iss, IssuerPrefix + 'my_app_id', 'issuer is correct'); 128 | assert.equal(token.body.sub, SubjectPrefix + 'test_identity', 'subject is correct'); 129 | assert.equal(token.body.iat, getUnixTimestamp(Date.now()), 'issued at is correct'); 130 | assert.equal( 131 | token.body.exp, 132 | getUnixTimestamp(Date.now() + generator.millisecondsToLive), 133 | 'expires at is correct' 134 | ); 135 | assert.deepEqual(token.body.ada, expectedAda, 'additional data is correct'); 136 | 137 | assert.isTrue( 138 | virgilCrypto.verifySignature(token.unsignedData, token.signature!, keypair.publicKey), 139 | 'signature is correct' 140 | ); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/__tests__/unit/KeyEntryStorage.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import base64 from 'base-64'; 3 | import { SinonStubbedInstance } from 'sinon'; 4 | import { KeyEntryStorage } from '../..'; 5 | import FileSystemStorageAdapter from '../../Storage/adapters/FileSystemStorageAdapter'; 6 | import { 7 | InvalidKeyEntryError, 8 | KeyEntryDoesNotExistError, 9 | KeyEntryAlreadyExistsError 10 | } from '../../Storage/KeyEntryStorage/errors'; 11 | 12 | describe ('KeyEntryStorage', () => { 13 | let storage: KeyEntryStorage; 14 | let adapterStub: SinonStubbedInstance; 15 | 16 | beforeEach(() => { 17 | adapterStub = sinon.createStubInstance(FileSystemStorageAdapter); 18 | storage = new KeyEntryStorage({ 19 | adapter: adapterStub 20 | }); 21 | }); 22 | 23 | describe ('exists', () => { 24 | it ('throws if name is empty', () => { 25 | assert.throws(() => { 26 | // should throw synchronously 27 | storage.exists(''); 28 | }, TypeError); 29 | }); 30 | 31 | it ('queries the adapter', () => { 32 | adapterStub.exists.resolves(true); 33 | return storage.exists('one').then(exists => { 34 | assert.isTrue(exists); 35 | assert.isTrue(adapterStub.exists.withArgs('one').calledOnce); 36 | }); 37 | }); 38 | }); 39 | 40 | describe ('save', () => { 41 | it ('throws if `name` is empty', () => { 42 | assert.throws(() => { 43 | storage.save({ name: '', value: '' }); 44 | }, TypeError); 45 | }); 46 | 47 | it ('throws if `params.value` is empty', () => { 48 | assert.throws(() => { 49 | storage.save({ name: 'test_entry', value: undefined! }); 50 | }, TypeError); 51 | }); 52 | 53 | it ('serializes entry to json and forwards to adapter', () => { 54 | adapterStub.store.resolves(); 55 | const expectedName = 'one'; 56 | const expectedValue = 'one'; 57 | const expectedMeta = { meta: 'data' }; 58 | 59 | return storage.save({ 60 | name: expectedName, 61 | value: expectedValue, 62 | meta: expectedMeta 63 | }).then(() => { 64 | assert.equal(adapterStub.store.firstCall.args[0], expectedName); 65 | assert.isTrue(typeof adapterStub.store.firstCall.args[1] === 'string'); 66 | 67 | const storedObject = JSON.parse(adapterStub.store.firstCall.args[1]); 68 | assert.equal(storedObject.name, expectedName); 69 | assert.equal(storedObject.value, expectedValue); 70 | assert.deepEqual(storedObject.meta, expectedMeta); 71 | }); 72 | }); 73 | 74 | it ('throws `PrivateKeyExistsError` if entry with the same name already exists', () => { 75 | adapterStub.store.rejects({ name: 'StorageEntryAlreadyExistsError' }); 76 | return assert.isRejected( 77 | storage.save({ name: 'one', value: 'one' }), 78 | KeyEntryAlreadyExistsError 79 | ); 80 | }); 81 | 82 | it ('re-throws unexpected errors from adapter', () => { 83 | adapterStub.store.rejects({ code: 'UNKNOWN', message: 'unknown error' }); 84 | return assert.isRejected( 85 | storage.save({ name: 'one', value: 'one' }), 86 | /unknown error/ 87 | ); 88 | }); 89 | 90 | it ('sets creationDate and modificationDate properties', () => { 91 | adapterStub.store.resolves(); 92 | 93 | return storage.save({ name: 'test', value: 'test' }) 94 | .then(entry => { 95 | assert.approximately(entry.creationDate.getTime(), Date.now(), 1000); 96 | assert.equal(entry.modificationDate.getTime(), entry.creationDate.getTime()); 97 | }); 98 | }); 99 | }); 100 | 101 | describe ('load', () => { 102 | it ('throws if name is empty', () => { 103 | assert.throws(() => { 104 | // should throw synchronously 105 | storage.load(''); 106 | }, TypeError); 107 | }); 108 | 109 | it ('returns null if entry is not found', () => { 110 | adapterStub.load.withArgs('any').resolves(null); 111 | return assert.becomes(storage.load('any'), null); 112 | }); 113 | 114 | it ('de-serializes entry returned from adapter', () => { 115 | adapterStub.load.withArgs('one') 116 | .resolves('{"name":"one","value":"b25l"}'); // "b25l" === base64("one") 117 | return storage.load('one').then(loadedEntry => { 118 | assert.isNotNull(loadedEntry); 119 | assert.equal(loadedEntry!.name, 'one'); 120 | assert.isTrue(loadedEntry!.value === 'b25l'); 121 | }); 122 | }); 123 | 124 | it ('throws if loaded entry is not a valid JSON', () => { 125 | adapterStub.load.withArgs('one').resolves('not_json'); 126 | return assert.isRejected( 127 | storage.load('one'), 128 | InvalidKeyEntryError 129 | ); 130 | }); 131 | }); 132 | 133 | describe ('list', () => { 134 | it ('de-serializes entries returned from adapter', () => { 135 | adapterStub.list.resolves([ 136 | '{"name":"one","value":"b25l"}', // "b25l" === base64("one") 137 | '{"name":"two","value":"dHdv"}', // "dHdv" === base64("two") 138 | '{"name":"three","value":"dGhyZWU="}' // "dGhyZWU=" === base64("three") 139 | ]); 140 | 141 | const expectedEntries: { name: string; value: string; }[] = [ 142 | { name: 'one', value: 'b25l' }, 143 | { name: 'two', value: 'dHdv' }, 144 | { name: 'three', value: 'dGhyZWU=' }, 145 | ]; 146 | 147 | return storage.list().then(loadedEntries => { 148 | assert.equal(loadedEntries.length, 3); 149 | assert.sameDeepMembers(loadedEntries, expectedEntries); 150 | }); 151 | }); 152 | 153 | it ('throws if any of the entries is not a valid json', () => { 154 | adapterStub.list.resolves([ 155 | 'not_json', 156 | '{"name":"three","value":"dGhyZWU="}' // "dGhyZWU=" === base64("three") 157 | ]); 158 | 159 | return assert.isRejected( 160 | storage.list(), 161 | InvalidKeyEntryError 162 | ); 163 | }); 164 | }); 165 | 166 | describe ('update', () => { 167 | it ('throws if `name` is empty', () => { 168 | assert.throws(() => { 169 | storage.update({ name: '', value: '' }); 170 | }, TypeError); 171 | }); 172 | 173 | it ('throws if both `value` and `meta` are empty', () => { 174 | assert.throws(() => { 175 | storage.update({ name: 'test_entry', value: undefined! }); 176 | }, TypeError); 177 | }); 178 | 179 | it ('throws if entry does not exist', () => { 180 | adapterStub.load.withArgs('one').resolves(null); 181 | return assert.isRejected( 182 | storage.update({ name: 'one', value: 'one' }), 183 | KeyEntryDoesNotExistError 184 | ); 185 | }); 186 | 187 | it ('serializes and forwards updated entry to adapter', () => { 188 | adapterStub.load 189 | .withArgs('one') 190 | .resolves('{"name":"one","value":"b25l"}'); // b25l = base64(one) 191 | adapterStub.update.resolves(); 192 | 193 | const expectedValue = 'another'; 194 | const expectedMeta = { 'foo': 'bar' }; 195 | 196 | return storage.update({ name: 'one', value: expectedValue, meta: expectedMeta }) 197 | .then(() => { 198 | assert.equal(adapterStub.update.firstCall.args[0], 'one'); 199 | assert.isTrue(typeof adapterStub.update.firstCall.args[1] === 'string'); 200 | 201 | const actualEntry = JSON.parse(adapterStub.update.firstCall.args[1]); 202 | assert.equal(actualEntry.name, 'one'); 203 | assert.equal(actualEntry.value, expectedValue); 204 | assert.deepEqual(actualEntry.meta, expectedMeta); 205 | assert.isDefined(actualEntry.modificationDate); 206 | }); 207 | }); 208 | 209 | it ('updates modification date', () => { 210 | adapterStub.load 211 | .withArgs('one') 212 | .resolves( 213 | '{"name":"one","value":"b25l","modificationDate":"2018-07-27T10:58:21.713Z"}' 214 | ); // b25l = base64(one) 215 | adapterStub.update.resolves(); 216 | 217 | return storage.update({ name: 'one', value: 'another' }) 218 | .then(updatedEntry => { 219 | assert.notEqual(updatedEntry.modificationDate, new Date("2018-07-27T10:58:21.713Z")); 220 | }); 221 | }); 222 | 223 | it ('does not overwrite original meta if new meta is not provided', () => { 224 | adapterStub.load 225 | .withArgs('one') 226 | .resolves( 227 | '{"name":"one","value":"b25l","meta":{"original":"meta"}}' 228 | ); // b25l = base64(one) 229 | adapterStub.update.resolves(); 230 | return storage.update({ name: 'one', value: 'another' }) 231 | .then(updatedEntry => { 232 | assert.deepEqual(updatedEntry.meta, { original: 'meta' }); 233 | assert.equal(updatedEntry.value, 'another'); 234 | }); 235 | }); 236 | 237 | it ('does not overwrite original value if new value is not provided', () => { 238 | adapterStub.load 239 | .withArgs('one') 240 | .resolves( 241 | '{"name":"one","value":"b25l","meta":{"original":"meta"}}' 242 | ); // b25l = base64(one) 243 | adapterStub.update.resolves(); 244 | return storage.update({ name: 'one', meta: { updated: 'meta' } }) 245 | .then(updatedEntry => { 246 | assert.equal(updatedEntry.value, 'b25l'); 247 | assert.deepEqual(updatedEntry.meta, { updated: 'meta' }); 248 | }); 249 | }); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /src/__tests__/unit/KeyStorage.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { SinonStubbedInstance } from 'sinon'; 3 | import { KeyStorage } from '../../Storage/KeyStorage'; 4 | import FileSystemStorageAdapter from '../../Storage/adapters/FileSystemStorageAdapter'; 5 | import { PrivateKeyExistsError } from '../../Storage/errors'; 6 | 7 | describe ('KeyStorage', () => { 8 | let storage: Private; 9 | let adapterSub: SinonStubbedInstance; 10 | 11 | beforeEach(() => { 12 | adapterSub = sinon.createStubInstance(FileSystemStorageAdapter); 13 | storage = new KeyStorage({ 14 | adapter: adapterSub 15 | }); 16 | }); 17 | 18 | describe ('exists', () => { 19 | it ('throws if name is empty', () => { 20 | assert.throws(() => { 21 | // should throw synchronously 22 | storage.exists(''); 23 | }, TypeError); 24 | }); 25 | 26 | it ('queries the adapter', () => { 27 | adapterSub.exists.resolves(true); 28 | return storage.exists('one').then(exists => { 29 | assert.isTrue(exists); 30 | assert.isTrue(adapterSub.exists.withArgs('one').calledOnce); 31 | }); 32 | }); 33 | }); 34 | 35 | describe ('save', () => { 36 | it ('throws if name is empty', () => { 37 | assert.throws(() => { 38 | storage.save('', 'one'); 39 | }, TypeError); 40 | }); 41 | 42 | it ('throws if data is empty', () => { 43 | assert.throws(() => { 44 | storage.save('one', null!); 45 | }, TypeError); 46 | }); 47 | 48 | it ('serializes entry to json and forwards to adapter', () => { 49 | adapterSub.store.resolves(); 50 | const expectedName = 'one'; 51 | const expectedValue = 'one'; 52 | 53 | return storage.save(expectedName, expectedValue).then(() => { 54 | assert.equal(adapterSub.store.firstCall.args[0], expectedName); 55 | assert.equal(adapterSub.store.firstCall.args[1], expectedValue); 56 | }); 57 | }); 58 | 59 | it ('throws `PrivateKeyExistsError` if entry with the same name already exists', () => { 60 | adapterSub.store.rejects({ code: 'EEXIST' }); 61 | return assert.isRejected( 62 | storage.save('one', 'one'), 63 | PrivateKeyExistsError 64 | ); 65 | }); 66 | 67 | it ('re-throws unexpected errors from adapter', () => { 68 | adapterSub.store.rejects({ code: 'UNKNOWN', message: 'unknown error' }); 69 | return assert.isRejected( 70 | storage.save('one', 'one'), 71 | /unknown error/ 72 | ); 73 | }); 74 | }); 75 | 76 | describe ('load', () => { 77 | it ('throws if name is empty', () => { 78 | assert.throws(() => { 79 | // should throw synchronously 80 | storage.load(''); 81 | }, TypeError); 82 | }); 83 | 84 | it ('returns null if entry is not found', () => { 85 | adapterSub.load.withArgs('any').resolves(null); 86 | return assert.becomes(storage.load('any'), null); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/__tests__/unit/PrivateKeyStorage.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { KeyEntryStorage, PrivateKeyStorage } from '../..'; 3 | import { SinonStubbedInstance } from 'sinon'; 4 | import { IPrivateKeyExporter } from '../../types'; 5 | import { IKeyEntryStorage } from '../../Storage/KeyEntryStorage/IKeyEntryStorage'; 6 | import { PrivateKeyExistsError } from '../../Storage/errors'; 7 | import { IPrivateKey } from '../../types'; 8 | import { NodeBuffer } from "@virgilsecurity/data-utils"; 9 | 10 | describe ('PrivateKeyStorage', () => { 11 | let privateKeyStorage: PrivateKeyStorage; 12 | let storageBackendStub: SinonStubbedInstance; 13 | let privateKeyExporterStub: SinonStubbedInstance; 14 | 15 | beforeEach(() => { 16 | storageBackendStub = sinon.createStubInstance(KeyEntryStorage); 17 | privateKeyExporterStub = { 18 | exportPrivateKey: sinon.stub(), 19 | importPrivateKey: sinon.stub() 20 | }; 21 | console.log(storageBackendStub) 22 | console.log(privateKeyExporterStub) 23 | privateKeyStorage = new PrivateKeyStorage(privateKeyExporterStub, storageBackendStub); 24 | console.log(privateKeyStorage) 25 | }); 26 | 27 | describe ('store', () => { 28 | it ('exports private key data before saving', () => { 29 | const privateKey = Buffer.from('private_key'); 30 | privateKeyExporterStub.exportPrivateKey.returns(privateKey); 31 | storageBackendStub.save.resolves(); 32 | return privateKeyStorage.store('test', {} as IPrivateKey, { meta: 'data' }) 33 | .then(() => { 34 | assert.isTrue(storageBackendStub.save.calledOnce); 35 | const entry = storageBackendStub.save.firstCall.args[0]; 36 | assert.equal(entry.name, 'test'); 37 | assert.equal(entry.value, privateKey.toString('base64')); 38 | assert.deepEqual(entry.meta, { meta: 'data' }); 39 | }); 40 | }); 41 | 42 | it ('throws if private key with the same name already exists', () => { 43 | privateKeyExporterStub.exportPrivateKey.returns(Buffer.from('private_key')); 44 | storageBackendStub.save.rejects({ name: 'KeyEntryAlreadyExistsError' }); 45 | return assert.isRejected( 46 | privateKeyStorage.store('test', {} as IPrivateKey), 47 | PrivateKeyExistsError 48 | ); 49 | }); 50 | }); 51 | 52 | describe ('load', () => { 53 | it ('imports private key data before returning', () => { 54 | const thePrivateKey = {}; 55 | privateKeyExporterStub.importPrivateKey.returns(thePrivateKey as IPrivateKey); 56 | storageBackendStub.load.withArgs('test').resolves({ 57 | name: 'test', 58 | value: Buffer.from('private_key') as unknown as string, 59 | creationDate: new Date(), 60 | modificationDate: new Date(), 61 | meta: { meta: 'data' } 62 | }); 63 | 64 | return privateKeyStorage.load('test').then(loadedEntry => { 65 | assert.isNotNull(loadedEntry); 66 | assert.strictEqual(loadedEntry!.privateKey, thePrivateKey); 67 | assert.deepEqual(loadedEntry!.meta, { meta: 'data' }); 68 | }); 69 | }); 70 | 71 | it ('returns null if entry is not found', () => { 72 | storageBackendStub.load.withArgs('test').resolves(null); 73 | return assert.becomes(privateKeyStorage.load('test'), null); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/__tests__/unit/StorageAdapter.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import StorageAdapter from '../../Storage/adapters/FileSystemStorageAdapter'; 3 | import { StorageEntryAlreadyExistsError } from '../../Storage/adapters/errors'; 4 | 5 | describe ('StorageAdapter', () => { 6 | let storage: StorageAdapter; 7 | 8 | beforeEach(() => { 9 | storage = new StorageAdapter({ 10 | dir: '.virgil_keys_test', 11 | name: 'VirgilKeysTest' 12 | }); 13 | }); 14 | 15 | afterEach(() => { 16 | return storage.clear(); 17 | }); 18 | 19 | it('store and get the key', () => { 20 | const expected = 'one'; 21 | return assert.eventually.equal( 22 | storage.store('first', expected) 23 | .then(() => storage.load('first')) 24 | .then(value => { 25 | return value != null && value === expected 26 | }), 27 | true, 28 | 'loaded key is identical to the saved one' 29 | ); 30 | }); 31 | 32 | it('throws when saving key with existing name', () => { 33 | return assert.isRejected( 34 | storage.store('first', 'one') 35 | .then(() => storage.store('first', 'two')), 36 | StorageEntryAlreadyExistsError, 37 | 'already exists' 38 | ); 39 | }); 40 | 41 | it('returns false when removing non-existent key', () => { 42 | return assert.eventually.isFalse( 43 | storage.remove('first') 44 | ); 45 | }); 46 | 47 | it('returns true when removing existent key', () => { 48 | return assert.eventually.isTrue( 49 | storage.store('first', 'one') 50 | .then(() => storage.remove('first')) 51 | ); 52 | }); 53 | 54 | it('store remove store', () => { 55 | return assert.eventually.isTrue( 56 | storage.store('first', 'one') 57 | .then(() => storage.remove('first')) 58 | .then(() => storage.store('first', 'two')) 59 | .then(() => storage.load('first')) 60 | .then(value => value != null && value === 'two') 61 | ); 62 | }); 63 | 64 | it ('remove item twice', () => { 65 | return assert.eventually.isNull( 66 | storage.remove('first') 67 | .then(() => storage.remove('first')) 68 | .then(() => storage.load('first')) 69 | ); 70 | }); 71 | 72 | it('store two items', () => { 73 | const oneExpected = 'one'; 74 | const twoExpected = 'two'; 75 | return assert.becomes( 76 | Promise.all([ 77 | storage.store('first', oneExpected), 78 | storage.store('second', twoExpected) 79 | ]).then(() => Promise.all([ 80 | storage.load('first'), 81 | storage.load('second') 82 | ]) 83 | ).then(([ one, two ]) => { 84 | return [ 85 | one != null && one === oneExpected, 86 | two != null && two === twoExpected 87 | ] 88 | }), 89 | [ true, true ] 90 | ); 91 | }); 92 | 93 | it('store remove three items', () => { 94 | return assert.eventually.isNull( 95 | Promise.all([ 96 | storage.store('first', 'one'), 97 | storage.store('second', 'two'), 98 | storage.store('third', 'three') 99 | ]).then(() => storage.remove('second')) 100 | .then(() => Promise.all([ 101 | assert.eventually.isTrue( 102 | storage.load('first') 103 | .then(first => first != null && first === 'one') 104 | ), 105 | assert.eventually.isNull(storage.load('second')), 106 | assert.eventually.isTrue( 107 | storage.load('third') 108 | .then(third => third != null && third === 'three') 109 | ) 110 | ]) 111 | ) 112 | .then(() => storage.remove('first')) 113 | .then(() => Promise.all([ 114 | assert.eventually.isNull(storage.load('first')), 115 | assert.eventually.isTrue( 116 | storage.load('third') 117 | .then(third => third != null && third === 'three') 118 | ) 119 | ]) 120 | ) 121 | .then(() => storage.remove('third')) 122 | .then(() => storage.load('third')) 123 | ); 124 | }); 125 | 126 | it('store empty value', () => { 127 | return assert.eventually.isTrue( 128 | storage.store('first', '') 129 | .then(() => storage.load('first')) 130 | .then(value => value != null && value === '') 131 | ); 132 | }); 133 | 134 | it('store with weird keys', () => { 135 | return assert.becomes( 136 | Promise.all([ 137 | storage.store(' ', 'space'), 138 | storage.store('=+!@#$%^&*()-_\\|;:\'",./<>?[]{}~`', 'control'), 139 | storage.store( 140 | '\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341', 141 | 'ten' 142 | ), 143 | storage.store('\0', 'null'), 144 | storage.store('\0\0', 'double null'), 145 | storage.store('\0A', 'null A'), 146 | storage.store('', 'zero') 147 | ]).then(() => { 148 | return Promise.all([ 149 | storage.load(' '), 150 | storage.load('=+!@#$%^&*()-_\\|;:\'",./<>?[]{}~`'), 151 | storage.load( 152 | '\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341'), 153 | storage.load('\0'), 154 | storage.load('\0\0'), 155 | storage.load('\0A'), 156 | storage.load('') 157 | ]); 158 | }) 159 | .then(values => values.map(value => value && value.toString())), 160 | [ 'space', 'control', 'ten', 'null', 'double null', 'null A', 'zero' ] 161 | ); 162 | }); 163 | 164 | it('exists with non-existent key', () => { 165 | return assert.eventually.isFalse( 166 | storage.exists('non-existent') 167 | ); 168 | }); 169 | 170 | it('exists with existent key', () => { 171 | return assert.eventually.isTrue( 172 | storage.store('existent', 'my value') 173 | .then(() => storage.exists('existent')) 174 | ); 175 | }); 176 | 177 | it('list returns empty array if storage is empty', () => { 178 | return assert.becomes( 179 | storage.list().then(entries => entries.length), 180 | 0 181 | ); 182 | }); 183 | 184 | it('list returns array of all values', () => { 185 | const expectedEntries = [ 186 | 'one', 187 | 'two', 188 | 'three' 189 | ]; 190 | 191 | return Promise.all([ 192 | storage.store('one', 'one'), 193 | storage.store('two', 'two'), 194 | storage.store('three', 'three') 195 | ]).then(() => 196 | storage.list() 197 | ).then(entries => { 198 | assert.sameDeepMembers(entries, expectedEntries); 199 | }) 200 | }); 201 | 202 | it('update with existing key', () => { 203 | return storage.store('one', 'one') 204 | .then(() => storage.update('one', 'another_one')) 205 | .then(() => storage.load('one')) 206 | .then(result => { 207 | assert.isNotNull(result); 208 | assert.equal(result!.toString(), 'another_one'); 209 | }); 210 | }); 211 | 212 | it('update with non-existing key creates new entry', () => { 213 | return storage.update('nonexistent', 'value') 214 | .then(() => storage.load('nonexistent')) 215 | .then(result => { 216 | assert.isNotNull(result); 217 | assert.equal(result!.toString(), 'value'); 218 | }); 219 | }); 220 | }); 221 | -------------------------------------------------------------------------------- /src/__tests__/unit/VirgilAgent.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { VirgilAgent } from "../../Client/VirgilAgent"; 3 | import data from './data/userAgents.json'; 4 | 5 | describe('VirgilAgent', () => { 6 | it('Should return header value in proper format', () => { 7 | const headerValue = new VirgilAgent('sdk', '5.0.0').value; 8 | assert.equal(/sdk;js;.+;5\.0\.0/.test(headerValue), true) 9 | }) 10 | 11 | it('Should properly detect os and browser by user agent', () => { 12 | data.forEach(uaObj => { 13 | const agent = new VirgilAgent('sdk', '5.0.0', uaObj.user_agent); 14 | assert.equal(agent.getOsName(), uaObj.os); 15 | assert.equal(agent.getBrowser(), uaObj.browser); 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/__tests__/unit/base64.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { base64Encode, base64Decode, base64UrlEncode, base64UrlDecode } from '../../Lib/base64'; 3 | describe('base64', () => { 4 | describe('base64Encode', () => { 5 | it('works', () => { 6 | const result = base64Encode('value'); 7 | assert.equal(result, 'dmFsdWU='); 8 | }); 9 | }); 10 | 11 | describe('base64Decode', () => { 12 | it('works', () => { 13 | const result = base64Decode('dmFsdWU='); 14 | assert.equal(result, 'value'); 15 | }); 16 | }); 17 | 18 | describe('base64UrlEncode', () => { 19 | it('works', () => { 20 | const result = base64UrlEncode('value?='); 21 | assert.equal(result, 'dmFsdWU_PQ'); 22 | }); 23 | }); 24 | 25 | describe('base64UrlDecode', () => { 26 | it('works', () => { 27 | const result = base64UrlDecode('dmFsdWU_PQ'); 28 | assert.equal(result, 'value?='); 29 | }); 30 | }); 31 | }) 32 | -------------------------------------------------------------------------------- /src/__tests__/unit/data/userAgents.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "user_agent": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 YaBrowser/18.3.1.1232 Yowser/2.5 Safari/537.36", 4 | "os": "Windows", 5 | "browser": "Chrome" 6 | }, 7 | { 8 | "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", 9 | "os": "macOS", 10 | "browser": "Chrome" 11 | }, 12 | { 13 | "user_agent": "Mozilla/5.0 (X11; U; Linux Core i7-4980HQ; de; rv:32.0; compatible; JobboerseBot; http://www.jobboerse.com/bot.htm) Gecko/20100101 Firefox/38.0", 14 | "os": "Linux", 15 | "browser": "Firefox" 16 | }, 17 | { 18 | "user_agent": "Mozilla/5.0 (Linux; U; Android 2.2) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", 19 | "os": "Android", 20 | "browser": "Android Browser" 21 | }, 22 | { 23 | "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1", 24 | "os": "iOS", 25 | "browser": "Safari" 26 | }, 27 | { 28 | "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299", 29 | "os": "Windows", 30 | "browser": "Microsoft Edge" 31 | }, 32 | { 33 | "user_agent": "Mozilla/5.0 (X11; od-database-crawler) Gecko/20100101 Firefox/52.0", 34 | "os": "other", 35 | "browser": "Firefox" 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module NodeJS { 2 | interface Process { 3 | browser?: boolean; 4 | } 5 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Auth/JwtGenerator'; 2 | export * from './Auth/JwtVerifier'; 3 | export * from './Auth/Jwt'; 4 | export * from './Auth/AccessTokenProviders'; 5 | export * from './Cards/CardManager'; 6 | export * from './Cards/CardVerifier'; 7 | export * from './Cards/RawSignedModel'; 8 | export * from './Cards/ModelSigner'; 9 | export * from './Client/VirgilAgent'; 10 | export * from './Storage/adapters/DefaultStorageAdapter'; 11 | export * from './Storage/adapters/errors'; 12 | export { IStorageAdapter } from './Storage/adapters/IStorageAdapter'; 13 | export * from './Storage/KeyStorage'; 14 | export * from './Storage/KeyEntryStorage/KeyEntryStorage'; 15 | export * from './Storage/KeyEntryStorage/errors'; 16 | export * from './Storage/PrivateKeyStorage'; 17 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Data = import('@virgilsecurity/crypto-types').Data; 2 | export type IPrivateKey = import('@virgilsecurity/crypto-types').IPrivateKey; 3 | export type IPublicKey = import('@virgilsecurity/crypto-types').IPublicKey; 4 | export type IAccessTokenSigner = import('@virgilsecurity/crypto-types').IAccessTokenSigner; 5 | export type ICardCrypto = import('@virgilsecurity/crypto-types').ICardCrypto; 6 | export type IPrivateKeyExporter = import('@virgilsecurity/crypto-types').IPrivateKeyExporter; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es5", "es6", "dom", "dom.iterable"], 4 | "baseUrl": ".", 5 | "target": "es2020", 6 | "outDir": "./dist", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": false, 10 | "strictPropertyInitialization": false, 11 | "noFallthroughCasesInSwitch": true, 12 | "sourceMap": true, 13 | "declaration": true, 14 | "declarationDir": "./dist/types", 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "allowSyntheticDefaultImports": true, 20 | "types": ["node", "jest", "mocha"], 21 | "module": "es2015", 22 | "noImplicitAny": true, 23 | "resolveJsonModule": true, 24 | }, 25 | "include": [ 26 | "src" 27 | ], 28 | "typedocOptions": { 29 | "out": "docs", 30 | "mode": "file", 31 | "target": "ES6", 32 | "module": "commonjs", 33 | "moduleResolution": "node", 34 | "exclude": "**/__tests__/**", 35 | "ignoreCompilerErrors": true, 36 | "excludePrivate": true, 37 | "excludeNotExported": true, 38 | "stripInternal": true, 39 | "suppressExcessPropertyErrors": true, 40 | "suppressImplicitAnyIndexErrors": true, 41 | "theme": "minimal", 42 | "hideGenerator": true, 43 | "readme": "none" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitReturns": false, 8 | "strictPropertyInitialization": false, 9 | "noFallthroughCasesInSwitch": true, 10 | "sourceMap": true, 11 | "declaration": true, 12 | "declarationDir": "./dist/types", 13 | "downlevelIteration": true, 14 | "experimentalDecorators": true, 15 | "noImplicitAny": true, 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "importHelpers": true, 19 | "esModuleInterop": true, 20 | "target": "es2020", 21 | "module": "commonjs", 22 | "lib": [ 23 | "es2018", 24 | "dom" 25 | ] 26 | }, 27 | "include": [ 28 | "src/__tests__/**/*.ts" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------