├── .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 |
--------------------------------------------------------------------------------