├── .tool-versions ├── test ├── utils │ ├── index.ts │ ├── mock.ts │ ├── keystore.ts │ └── crypto.ts ├── utils.test.ts ├── errors.test.ts ├── index.test.ts ├── config.test.ts ├── aes.test.ts ├── rsa.keystore.test.ts ├── base.keystore.test.ts ├── ecc.keystore.test.ts ├── rsa.test.ts └── ecc.test.ts ├── tslint.json ├── tsconfig.eslint.json ├── src ├── aes │ ├── index.ts │ ├── keys.ts │ └── operations.ts ├── ecc │ ├── index.ts │ ├── keys.ts │ ├── keystore.ts │ └── operations.ts ├── rsa │ ├── index.ts │ ├── keys.ts │ ├── operations.ts │ └── keystore.ts ├── index.ts ├── keystore │ ├── index.ts │ └── base.ts ├── constants.ts ├── config.ts ├── errors.ts ├── idb.ts ├── types.ts └── utils.ts ├── .eslintrc.cjs ├── tsconfig.json ├── jest.config.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── test.yml ├── PULL_REQUEST_TEMPLATE.md └── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── scripts └── build-minified.js ├── package.json ├── README.md ├── LICENSE └── .gitignore /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 13.7.0 2 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crypto' 2 | export * from './keystore' 3 | export * from './mock' 4 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ] 6 | } -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "exclude": [] 5 | } 6 | -------------------------------------------------------------------------------- /src/aes/index.ts: -------------------------------------------------------------------------------- 1 | import keys from './keys.js' 2 | import operations from './operations.js' 3 | 4 | export * from './keys.js' 5 | export * from './operations.js' 6 | 7 | export default { 8 | ...keys, 9 | ...operations, 10 | } 11 | -------------------------------------------------------------------------------- /src/ecc/index.ts: -------------------------------------------------------------------------------- 1 | import keys from './keys.js' 2 | import operations from './operations.js' 3 | import keystore from './keystore.js' 4 | 5 | export * from './keys.js' 6 | export * from './operations.js' 7 | export * from './keystore.js' 8 | 9 | export default { 10 | ...keys, 11 | ...operations, 12 | ...keystore, 13 | } 14 | -------------------------------------------------------------------------------- /src/rsa/index.ts: -------------------------------------------------------------------------------- 1 | import keys from './keys.js' 2 | import operations from './operations.js' 3 | import keystore from './keystore.js' 4 | 5 | export * from './keys.js' 6 | export * from './operations.js' 7 | export * from './keystore.js' 8 | 9 | export default { 10 | ...keys, 11 | ...operations, 12 | ...keystore, 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | rules: { 13 | '@typescript-eslint/no-use-before-define': ['off', 'nofunc'] 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { init, clear } from './keystore/index.js' 2 | 3 | import * as ecc from './ecc/index.js' 4 | import * as rsa from './rsa/index.js' 5 | import * as config from './config.js' 6 | import * as constants from './constants.js' 7 | import * as utils from './utils.js' 8 | import * as idb from './idb.js' 9 | import * as types from './types.js' 10 | 11 | export default { 12 | init, 13 | clear, 14 | ...types, 15 | ...constants, 16 | ...config, 17 | ...utils, 18 | ecc, 19 | rsa, 20 | idb, 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "ES2020", 5 | "module":"ESNext", 6 | "lib": ["dom", "es2021"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "skipLibCheck": true, 14 | "declarationDir": "lib", 15 | "outDir": "lib", 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "src/**/*.test.ts", 22 | "test/**/*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | resolver: "jest-ts-webcompat-resolver", 3 | transform: { 4 | ".(ts|tsx)": "ts-jest" 5 | }, 6 | testEnvironment: "node", 7 | testRegex: "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 8 | moduleFileExtensions: [ 9 | "ts", 10 | "tsx", 11 | "js" 12 | ], 13 | collectCoverage: true, 14 | coverageDirectory: "coverage", 15 | coverageProvider: "v8", 16 | coveragePathIgnorePatterns: [ 17 | "/node_modules/", 18 | "/test/", 19 | "/src/idb" 20 | ], 21 | coverageThreshold: { 22 | global: { 23 | branches: 90, 24 | functions: 95, 25 | lines: 95, 26 | statements: 95 27 | } 28 | }, 29 | collectCoverageFrom: [ 30 | "src/**/*.{js,ts}" 31 | ], 32 | globals: { 33 | localForage: {} 34 | }, 35 | restoreMocks: true 36 | } 37 | -------------------------------------------------------------------------------- /src/keystore/index.ts: -------------------------------------------------------------------------------- 1 | import ECCKeyStore from '../ecc/keystore.js' 2 | import RSAKeyStore from '../rsa/keystore.js' 3 | import config from '../config.js' 4 | import IDB from '../idb.js' 5 | import { ECCNotEnabled, checkValidCryptoSystem } from '../errors.js' 6 | import { Config, KeyStore } from '../types.js' 7 | 8 | export async function init(maybeCfg?: Partial): Promise{ 9 | const eccEnabled = await config.eccEnabled() 10 | if(!eccEnabled && maybeCfg?.type === 'ecc'){ 11 | throw ECCNotEnabled 12 | } 13 | 14 | const cfg = config.normalize(maybeCfg, eccEnabled) 15 | 16 | checkValidCryptoSystem(cfg.type) 17 | 18 | if(cfg.type === 'ecc'){ 19 | return ECCKeyStore.init(cfg) 20 | }else { 21 | return RSAKeyStore.init(cfg) 22 | } 23 | } 24 | 25 | export async function clear(): Promise { 26 | return IDB.clear() 27 | } 28 | 29 | export default { 30 | init, 31 | clear, 32 | } 33 | -------------------------------------------------------------------------------- /src/aes/keys.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'one-webcrypto' 2 | import utils from '../utils.js' 3 | import { DEFAULT_SYMM_ALG, DEFAULT_SYMM_LEN } from '../constants.js' 4 | import { SymmKey, SymmKeyOpts } from '../types.js' 5 | 6 | export async function makeKey(opts?: Partial): Promise { 7 | return webcrypto.subtle.generateKey( 8 | { 9 | name: opts?.alg || DEFAULT_SYMM_ALG, 10 | length: opts?.length || DEFAULT_SYMM_LEN, 11 | }, 12 | true, 13 | ['encrypt', 'decrypt'] 14 | ) 15 | } 16 | 17 | export async function importKey(base64key: string, opts?: Partial): Promise { 18 | const buf = utils.base64ToArrBuf(base64key) 19 | return webcrypto.subtle.importKey( 20 | 'raw', 21 | buf, 22 | { 23 | name: opts?.alg || DEFAULT_SYMM_ALG, 24 | length: opts?.length || DEFAULT_SYMM_LEN, 25 | }, 26 | true, 27 | ['encrypt', 'decrypt'] 28 | ) 29 | } 30 | 31 | export default { 32 | makeKey, 33 | importKey, 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: "\U0001F497 enhancement" 6 | assignees: '' 7 | 8 | --- 9 | 10 | NB: Feature requests will only be considered if they solve a pain 11 | 12 | # Summary 13 | 14 | ## Problem 15 | 16 | Describe the pain that this feature will solve 17 | 18 | ### Impact 19 | 20 | The impact of not having this feature 21 | 22 | ## Solution 23 | 24 | Describe the solution 25 | 26 | # Detail 27 | 28 | **Is your feature request related to a problem? Please describe.** 29 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 30 | 31 | **Describe the solution you'd like** 32 | A clear and concise description of what you want to happen. 33 | 34 | **Describe alternatives you've considered** 35 | A clear and concise description of any alternative solutions or features you've considered. 36 | 37 | **Additional context** 38 | Add any other context or screenshots about the feature request here. 39 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import errors from '../src/errors' 2 | import utils from '../src/utils' 3 | 4 | describe('utils', () => { 5 | it('uses rejection sampling to generate ', async () => { 6 | let hasAboveMax = false 7 | const max = 10 8 | 9 | for (let i = 0; i < 1000; i++) { 10 | const byte = new Uint8Array(utils.randomBuf(1, { max }))[0] 11 | if (byte > max) { 12 | hasAboveMax = true 13 | break 14 | } 15 | } 16 | 17 | expect(hasAboveMax).toBe(false) 18 | }) 19 | 20 | it('returns ArrayBuffer of specified length', async () => { 21 | const buf1 = new Uint8Array(utils.randomBuf(2)) 22 | const buf2 = new Uint8Array(utils.randomBuf(45, { max: 15 })) 23 | 24 | expect(buf1.length).toBe(2) 25 | expect(buf2.length).toBe(45) 26 | }) 27 | 28 | it('does not support max values above 255', async () => { 29 | const fn = () => utils.randomBuf(1, { max: 256 }) 30 | expect(fn).toThrow(errors.InvalidMaxValue) 31 | }) 32 | 33 | it('does not support max values below 1', async () => { 34 | const fn = () => utils.randomBuf(1, { max: -20 }) 35 | expect(fn).toThrow(errors.InvalidMaxValue) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | 8 | pull_request: 9 | branches: [ main ] 10 | 11 | workflow_dispatch: 12 | 13 | 14 | jobs: 15 | build-and-test: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Check out repository 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup Node Environment 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: '16' 26 | 27 | - name: Get yarn cache directory path 28 | id: yarn-cache-dir-path 29 | run: echo "::set-output name=dir::$(yarn cache dir)" 30 | 31 | - uses: actions/cache@v2 32 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | 39 | # --- 40 | 41 | - name: Install Dependencies 42 | run: yarn install --network-concurrency 1 43 | 44 | - name: Build & Test 45 | run: yarn test:prod 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v0.15.5 4 | 5 | - Support a `max` parameter (based on rejection sampling) in `randomBuf` rng. 6 | 7 | ### v0.15.4 8 | 9 | - Bump one-webcrypto to 1.0.3 for wider bundler support. 10 | 11 | ### v0.15.3 12 | 13 | - Internally use the one-webcrypto library for referring to the webcrypto API 14 | 15 | ### v0.15.2 16 | 17 | - Add `AES-GCM` to the list of valid symmetric algorithms (`SymmAlg`) 18 | - Internally dynamically refer to either the NodeJS or Browser webcrypto API 19 | 20 | ### v0.15.1 21 | 22 | Importing `keystore-idb/lib/*` directly should now work as intended. This allows bundlers to use the "real" import paths (eg. `import "keystore-idb/lib/utils.js"`) in addition to the "proxy" import paths (eg. `import "keystore-idb/utils.js"`). One reason to do this could be that you want your library to support both new and old bundlers, ie. bundlers with or without `exports` support in their `package.json` file. 23 | 24 | 25 | ### v0.15.0 26 | 27 | - Renamed read key to exchange key. 28 | - Switched out `Buffer` usage with `uint8arrays` library. 29 | - Built with esbuild instead of rollup. 30 | 31 | 32 | 33 | ### v0.14.0 34 | 35 | Use the `globalThis` global object instead of `window`. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: "\U0001F41B bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Summary 11 | 12 | ## Problem 13 | 14 | Describe the immediate problem 15 | 16 | ### Impact 17 | 18 | The impact that this bug has 19 | 20 | ## Solution 21 | 22 | Describe the sort of fix that would solve the issue 23 | 24 | # Detail 25 | 26 | **Describe the bug** 27 | A clear and concise description of what the bug is. 28 | 29 | **To Reproduce** 30 | Steps to reproduce the behavior: 31 | 1. Go to '...' 32 | 2. Click on '....' 33 | 3. Scroll down to '....' 34 | 4. See error 35 | 36 | **Expected behavior** 37 | A clear and concise description of what you expected to happen. 38 | 39 | **Screenshots** 40 | If applicable, add screenshots to help explain your problem. 41 | 42 | **Desktop (please complete the following information):** 43 | - OS: [e.g. iOS] 44 | - Browser [e.g. chrome, safari] 45 | - Version [e.g. 22] 46 | 47 | **Smartphone (please complete the following information):** 48 | - Device: [e.g. iPhone6] 49 | - OS: [e.g. iOS8.1] 50 | - Browser [e.g. stock browser, safari] 51 | - Version [e.g. 22] 52 | 53 | **Additional context** 54 | Add any other context about the problem here. 55 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { EccCurve, RsaSize, SymmAlg, SymmKeyLength, HashAlg, CharSize } from './types.js' 2 | 3 | export const ECC_EXCHANGE_ALG = 'ECDH' 4 | export const ECC_WRITE_ALG = 'ECDSA' 5 | export const RSA_EXCHANGE_ALG = 'RSA-OAEP' 6 | export const RSA_WRITE_ALG = 'RSASSA-PKCS1-v1_5' 7 | export const SALT_LENGTH = 128 8 | 9 | export const DEFAULT_CRYPTOSYSTEM = 'ecc' 10 | export const DEFAULT_ECC_CURVE = EccCurve.P_256 11 | export const DEFAULT_RSA_SIZE = RsaSize.B2048 12 | 13 | export const DEFAULT_SYMM_ALG = SymmAlg.AES_CTR 14 | export const DEFAULT_SYMM_LEN = SymmKeyLength.B256 15 | export const DEFAULT_CTR_LEN = 64 16 | 17 | export const DEFAULT_HASH_ALG = HashAlg.SHA_256 18 | export const DEFAULT_CHAR_SIZE = CharSize.B16 19 | 20 | export const DEFAULT_STORE_NAME = 'keystore' 21 | export const DEFAULT_EXCHANGE_KEY_NAME = 'exchange-key' 22 | export const DEFAULT_WRITE_KEY_NAME = 'write-key' 23 | 24 | export default { 25 | ECC_EXCHANGE_ALG, 26 | ECC_WRITE_ALG, 27 | RSA_EXCHANGE_ALG, 28 | RSA_WRITE_ALG, 29 | SALT_LENGTH, 30 | DEFAULT_CRYPTOSYSTEM, 31 | DEFAULT_ECC_CURVE, 32 | DEFAULT_RSA_SIZE, 33 | DEFAULT_SYMM_ALG, 34 | DEFAULT_CTR_LEN, 35 | DEFAULT_HASH_ALG, 36 | DEFAULT_CHAR_SIZE, 37 | DEFAULT_STORE_NAME, 38 | DEFAULT_EXCHANGE_KEY_NAME, 39 | DEFAULT_WRITE_KEY_NAME, 40 | } 41 | -------------------------------------------------------------------------------- /src/ecc/keys.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'one-webcrypto' 2 | import utils from '../utils.js' 3 | import { ECC_EXCHANGE_ALG, ECC_WRITE_ALG } from '../constants.js' 4 | import { EccCurve, KeyUse, PublicKey } from '../types.js' 5 | import { checkValidKeyUse } from '../errors.js' 6 | 7 | export async function makeKeypair( 8 | curve: EccCurve, 9 | use: KeyUse 10 | ): Promise { 11 | checkValidKeyUse(use) 12 | const alg = use === KeyUse.Exchange ? ECC_EXCHANGE_ALG : ECC_WRITE_ALG 13 | const uses: KeyUsage[] = 14 | use === KeyUse.Exchange ? ['deriveKey', 'deriveBits'] : ['sign', 'verify'] 15 | return webcrypto.subtle.generateKey( 16 | { name: alg, namedCurve: curve }, 17 | false, 18 | uses 19 | ) 20 | } 21 | 22 | export async function importPublicKey(base64Key: string, curve: EccCurve, use: KeyUse): Promise { 23 | checkValidKeyUse(use) 24 | const alg = use === KeyUse.Exchange ? ECC_EXCHANGE_ALG : ECC_WRITE_ALG 25 | const uses: KeyUsage[] = 26 | use === KeyUse.Exchange ? [] : ['verify'] 27 | const buf = utils.base64ToArrBuf(base64Key) 28 | return webcrypto.subtle.importKey( 29 | 'raw', 30 | buf, 31 | { name: alg, namedCurve: curve }, 32 | true, 33 | uses 34 | ) 35 | } 36 | 37 | export default { 38 | makeKeypair, 39 | importPublicKey 40 | } 41 | -------------------------------------------------------------------------------- /test/utils/mock.ts: -------------------------------------------------------------------------------- 1 | import utils from '../../src/utils' 2 | 3 | const iv = (new Uint8Array([1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4])).buffer 4 | const msgStr = "test msg bytes" 5 | const msgBytes = utils.strToArrBuf(msgStr, 16) 6 | const sigStr = "dGVzdCBzaWduYXR1cmU=" 7 | const sigBytes = utils.base64ToArrBuf(sigStr) 8 | const cipherStr = "dGVzdCBlbmNyeXB0ZWQgYnl0ZXM=" 9 | const cipherBytes = utils.base64ToArrBuf(cipherStr) 10 | const cipherWithIVBytes = utils.joinBufs(iv, cipherBytes) 11 | const cipherWithIVStr = utils.arrBufToBase64(cipherWithIVBytes) 12 | 13 | /* eslint-disable @typescript-eslint/no-explicit-any */ 14 | export const mock = { 15 | idbStore: { 16 | type: 'fake-store' 17 | } as any, 18 | keys: { 19 | publicKey: { type: 'pub' } as any, 20 | privateKey: { type: 'priv' } as any 21 | } as any, 22 | writeKeys: { 23 | publicKey: { type: 'write-pub' } as any, 24 | privateKey: { type: 'write-priv' } as any 25 | } as any, 26 | encryptForKey: { 27 | publicKey: { type: 'encrypt-pub' } as any, 28 | privateKey: { type: 'encrypt-priv' } as any 29 | } as any, 30 | symmKey: { type: 'symm', algorithm: 'AES-CTR' } as any, 31 | symmKeyName: 'symm-key', 32 | keyBase64: 'q83vEjRWeJA=', 33 | iv, 34 | msgStr, 35 | msgBytes, 36 | sigStr, 37 | sigBytes, 38 | cipherStr, 39 | cipherBytes, 40 | cipherWithIVStr, 41 | cipherWithIVBytes, 42 | } 43 | 44 | export default mock 45 | /* eslint-enable @typescript-eslint/no-explicit-any */ 46 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | A similar PR may already be submitted! 2 | Please search among the [Pull request](../) before creating one. 3 | 4 | Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: 5 | 6 | For more information, see the `CONTRIBUTING` guide. 7 | 8 | 9 | ## Summary 10 | 11 | 12 | This PR fixes/implements the following **bugs/features** 13 | 14 | * [ ] Bug 1 15 | * [ ] Bug 2 16 | * [ ] Feature 1 17 | * [ ] Feature 2 18 | * [ ] Breaking changes 19 | 20 | 21 | 22 | Explain the **motivation** for making this change. What existing problem does the pull request solve? 23 | 24 | 25 | 26 | ## Test plan (required) 27 | 28 | Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. 29 | 30 | 31 | 32 | 33 | ## Closing issues 34 | 35 | 36 | Fixes # 37 | 38 | ## After Merge 39 | * [ ] Does this change invalidate any docs or tutorials? _If so ensure the changes needed are either made or recorded_ 40 | * [ ] Does this change require a release to be made? Is so please create and deploy the release 41 | 42 | -------------------------------------------------------------------------------- /src/rsa/keys.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'one-webcrypto' 2 | import { RSA_EXCHANGE_ALG, RSA_WRITE_ALG } from '../constants.js' 3 | import { RsaSize, HashAlg, KeyUse, PublicKey } from '../types.js' 4 | import utils from '../utils.js' 5 | import { checkValidKeyUse } from '../errors.js' 6 | 7 | export async function makeKeypair( 8 | size: RsaSize, 9 | hashAlg: HashAlg, 10 | use: KeyUse 11 | ): Promise { 12 | checkValidKeyUse(use) 13 | const alg = use === KeyUse.Exchange ? RSA_EXCHANGE_ALG : RSA_WRITE_ALG 14 | const uses: KeyUsage[] = use === KeyUse.Exchange ? ['encrypt', 'decrypt'] : ['sign', 'verify'] 15 | return webcrypto.subtle.generateKey( 16 | { 17 | name: alg, 18 | modulusLength: size, 19 | publicExponent: utils.publicExponent(), 20 | hash: { name: hashAlg } 21 | }, 22 | false, 23 | uses 24 | ) 25 | } 26 | 27 | function stripKeyHeader(base64Key: string): string{ 28 | return base64Key 29 | .replace('-----BEGIN PUBLIC KEY-----\n', '') 30 | .replace('\n-----END PUBLIC KEY-----', '') 31 | } 32 | 33 | export async function importPublicKey(base64Key: string, hashAlg: HashAlg, use: KeyUse): Promise { 34 | checkValidKeyUse(use) 35 | const alg = use === KeyUse.Exchange ? RSA_EXCHANGE_ALG : RSA_WRITE_ALG 36 | const uses: KeyUsage[] = use === KeyUse.Exchange ? ['encrypt'] : ['verify'] 37 | const buf = utils.base64ToArrBuf(stripKeyHeader(base64Key)) 38 | return webcrypto.subtle.importKey( 39 | 'spki', 40 | buf, 41 | { name: alg, hash: {name: hashAlg}}, 42 | true, 43 | uses 44 | ) 45 | } 46 | 47 | export default { 48 | makeKeypair, 49 | importPublicKey 50 | } 51 | -------------------------------------------------------------------------------- /scripts/build-minified.js: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild" 2 | import fs from "fs" 3 | import zlib from "zlib" 4 | 5 | 6 | const globalName = "keystore" 7 | const outfile = "dist/index.umd.min.js" 8 | const outfileGz = `${outfile}.gz` 9 | 10 | // From https://github.com/umdjs/umd/blob/36fd1135ba44e758c7371e7af72295acdebce010/templates/returnExports.js 11 | const umd = { 12 | banner: 13 | `(function (root, factory) { 14 | if (typeof define === 'function' && define.amd) { 15 | // AMD. Register as an anonymous module. 16 | define([], factory); 17 | } else if (typeof module === 'object' && module.exports) { 18 | // Node. Does not work with strict CommonJS, but 19 | // only CommonJS-like environments that support module.exports, 20 | // like Node. 21 | module.exports = factory(); 22 | } else { 23 | // Browser globals (root is window) 24 | root.${globalName} = factory(); 25 | } 26 | }(typeof self !== 'undefined' ? self : this, function () { `, 27 | footer: 28 | `return ${globalName}; 29 | }));` 30 | } 31 | 32 | console.log("📦 bundling & minifying...") 33 | 34 | esbuild.buildSync({ 35 | entryPoints: ["src/index.ts"], 36 | outfile, 37 | bundle: true, 38 | minify: true, 39 | sourcemap: true, 40 | platform: "browser", 41 | format: "iife", 42 | target: "es2020", 43 | globalName, 44 | banner: { 45 | js: umd.banner, 46 | }, 47 | footer: { 48 | js: umd.footer, 49 | }, 50 | }) 51 | 52 | console.log(`📝 Wrote ${outfile} and ${outfile}.map`) 53 | 54 | console.log("💎 compressing into .gz") 55 | 56 | const fileContents = fs.createReadStream(outfile) 57 | const writeStream = fs.createWriteStream(outfileGz) 58 | const gzip = zlib.createGzip() 59 | 60 | fileContents.pipe(gzip).pipe(writeStream) 61 | 62 | console.log(`📝 Wrote ${outfileGz}`) 63 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import ecc from './ecc/keys.js' 2 | import { 3 | DEFAULT_CRYPTOSYSTEM, 4 | DEFAULT_ECC_CURVE, 5 | DEFAULT_RSA_SIZE, 6 | DEFAULT_SYMM_ALG, 7 | DEFAULT_SYMM_LEN, 8 | DEFAULT_HASH_ALG, 9 | DEFAULT_CHAR_SIZE, 10 | DEFAULT_STORE_NAME, 11 | DEFAULT_EXCHANGE_KEY_NAME, 12 | DEFAULT_WRITE_KEY_NAME 13 | } from './constants.js' 14 | import { Config, KeyUse, CryptoSystem, SymmKeyOpts } from './types.js' 15 | import utils from './utils.js' 16 | 17 | export const defaultConfig = { 18 | type: DEFAULT_CRYPTOSYSTEM, 19 | curve: DEFAULT_ECC_CURVE, 20 | rsaSize: DEFAULT_RSA_SIZE, 21 | symmAlg: DEFAULT_SYMM_ALG, 22 | symmLen: DEFAULT_SYMM_LEN, 23 | hashAlg: DEFAULT_HASH_ALG, 24 | charSize: DEFAULT_CHAR_SIZE, 25 | storeName: DEFAULT_STORE_NAME, 26 | exchangeKeyName: DEFAULT_EXCHANGE_KEY_NAME, 27 | writeKeyName: DEFAULT_WRITE_KEY_NAME 28 | } as Config 29 | 30 | export function normalize( 31 | maybeCfg?: Partial, 32 | eccEnabled: boolean = true 33 | ): Config { 34 | let cfg 35 | if (!maybeCfg) { 36 | cfg = defaultConfig 37 | } else { 38 | cfg = { 39 | ...defaultConfig, 40 | ...maybeCfg 41 | } 42 | } 43 | if (!maybeCfg?.type) { 44 | cfg.type = eccEnabled ? CryptoSystem.ECC : CryptoSystem.RSA 45 | } 46 | return cfg 47 | } 48 | 49 | // Attempt a structural clone of an ECC Key (required to store in IndexedDB) 50 | // If it throws an error, use RSA, otherwise use ECC 51 | export async function eccEnabled(): Promise { 52 | const keypair = await ecc.makeKeypair(DEFAULT_ECC_CURVE, KeyUse.Exchange) 53 | try { 54 | await utils.structuralClone(keypair) 55 | } catch (err) { 56 | return false 57 | } 58 | return true 59 | } 60 | 61 | export function merge(cfg: Config, overwrites: Partial = {}): Config { 62 | return { 63 | ...cfg, 64 | ...overwrites 65 | } 66 | } 67 | 68 | export function symmKeyOpts(cfg: Config): Partial { 69 | return { alg: cfg.symmAlg, length: cfg.symmLen } 70 | } 71 | 72 | export default { 73 | defaultConfig, 74 | normalize, 75 | eccEnabled, 76 | merge, 77 | symmKeyOpts 78 | } 79 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import { KeyUse, CryptoSystem } from './types.js' 2 | 3 | export const KeyDoesNotExist = new Error("Key does not exist. Make sure you properly instantiated the keystore.") 4 | export const NotKeyPair = new Error("Retrieved a symmetric key when an asymmetric keypair was expected. Please use a different key name.") 5 | export const NotKey = new Error("Retrieved an asymmetric keypair when an symmetric key was expected. Please use a different key name.") 6 | export const ECCNotEnabled = new Error("ECC is not enabled for this browser. Please use RSA instead.") 7 | export const UnsupportedCrypto = new Error("Cryptosystem not supported. Please use ECC or RSA") 8 | export const InvalidKeyUse = new Error("Invalid key use. Please use 'exchange' or 'write") 9 | export const InvalidMaxValue = new Error("Max must be less than 256 and greater than 0") 10 | 11 | export function checkIsKeyPair(keypair: any): CryptoKeyPair { 12 | if (!keypair || keypair === null) { 13 | throw KeyDoesNotExist 14 | } else if (keypair.privateKey === undefined) { 15 | throw NotKeyPair 16 | } 17 | return keypair as CryptoKeyPair 18 | } 19 | 20 | export function checkIsKey(key: any): CryptoKey { 21 | if (!key || key === null) { 22 | throw KeyDoesNotExist 23 | } else if (key.privateKey !== undefined || key.algorithm === undefined) { 24 | throw NotKey 25 | } 26 | return key 27 | } 28 | 29 | export function checkValidCryptoSystem(type: CryptoSystem): void { 30 | checkValid(type, [CryptoSystem.ECC, CryptoSystem.RSA], UnsupportedCrypto) 31 | } 32 | 33 | export function checkValidKeyUse(use: KeyUse): void { 34 | checkValid(use, [KeyUse.Exchange, KeyUse.Write], InvalidKeyUse) 35 | } 36 | 37 | function checkValid(toCheck: T, opts: T[], error: Error): void { 38 | const match = opts.some(opt => opt === toCheck) 39 | if (!match) { 40 | throw error 41 | } 42 | } 43 | 44 | export default { 45 | KeyDoesNotExist, 46 | NotKeyPair, 47 | NotKey, 48 | ECCNotEnabled, 49 | UnsupportedCrypto, 50 | InvalidKeyUse, 51 | checkIsKeyPair, 52 | checkIsKey, 53 | checkValidCryptoSystem, 54 | checkValidKeyUse, 55 | InvalidMaxValue, 56 | } 57 | -------------------------------------------------------------------------------- /test/utils/keystore.ts: -------------------------------------------------------------------------------- 1 | import { ECCKeyStore } from '../../src/ecc/keystore' 2 | import { RSAKeyStore } from '../../src/rsa/keystore' 3 | import config from '../../src/config' 4 | import idb from '../../src/idb' 5 | import { KeyStore } from '../../src/types' 6 | import { mock } from './mock' 7 | 8 | /* eslint-disable @typescript-eslint/no-explicit-any */ 9 | type Mock = { 10 | mod: any; 11 | meth: string; 12 | resp: any; 13 | params: any; 14 | } 15 | 16 | type KeystoreMethodOpts = { 17 | desc: string; 18 | type: 'ecc' | 'rsa'; 19 | mocks: Mock[]; 20 | reqFn: (ks: KeyStore) => Promise; 21 | expectedResp?: any; 22 | } 23 | /* eslint-enable @typescript-eslint/no-explicit-any */ 24 | 25 | export const keystoreMethod = (opts: KeystoreMethodOpts): void => { 26 | describe(opts.desc, () => { 27 | const fakes = [] as jest.SpyInstance[] 28 | let response: any // eslint-disable-line @typescript-eslint/no-explicit-any 29 | 30 | beforeAll(async () => { 31 | jest.resetAllMocks() 32 | jest.spyOn(idb, 'getKeypair').mockImplementation((keyName) => { 33 | return keyName === 'exchange-key' ? mock.keys : mock.writeKeys 34 | }) 35 | 36 | opts.mocks.forEach(mock => { 37 | const fake = jest.spyOn(mock.mod, mock.meth) 38 | fake.mockResolvedValue(mock.resp) 39 | fakes.push(fake) 40 | }) 41 | 42 | const ks = opts.type === 'ecc' ? 43 | new ECCKeyStore(config.defaultConfig, mock.idbStore) : 44 | new RSAKeyStore(config.defaultConfig, mock.idbStore) 45 | response = await opts.reqFn(ks) 46 | }) 47 | 48 | opts.mocks.forEach((mock, i) => { 49 | it(`should call ${mock.meth} once`, () => { 50 | expect(fakes[i]).toBeCalledTimes(1) 51 | }) 52 | 53 | it(`should call the library function with the expected params`, () => { 54 | expect(fakes[i].mock.calls[0]).toEqual(mock.params) 55 | }) 56 | }) 57 | 58 | if(opts.expectedResp !== undefined) { 59 | it('should return the expectedResp', () => { 60 | expect(response).toEqual(opts.expectedResp) 61 | }) 62 | } 63 | 64 | }) 65 | } 66 | 67 | -------------------------------------------------------------------------------- /src/idb.ts: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage' 2 | import { checkIsKeyPair, checkIsKey } from './errors.js' 3 | 4 | /* istanbul ignore next */ 5 | export function createStore(name: string): LocalForage { 6 | return localforage.createInstance({ name }) 7 | } 8 | 9 | export async function createIfDoesNotExist(id: string, makeFn: () => Promise, store: LocalForage = localforage): Promise { 10 | if(await exists(id, store)) { 11 | return 12 | } 13 | const key = await makeFn() 14 | await put(id, key, store) 15 | } 16 | 17 | /* istanbul ignore next */ 18 | export async function put(id: string, key: CryptoKeyPair | CryptoKey, store: LocalForage = localforage): Promise { 19 | return store.setItem(id, key) 20 | } 21 | 22 | /* istanbul ignore next */ 23 | export async function getKeypair(id: string, store: LocalForage = localforage): Promise { 24 | return get(id, checkIsKeyPair, store) 25 | } 26 | 27 | /* istanbul ignore next */ 28 | export async function getKey(id: string, store: LocalForage = localforage): Promise { 29 | return get(id, checkIsKey, store) 30 | } 31 | 32 | /* istanbul ignore next */ 33 | export async function get(id: string, checkFn: (obj: unknown) => T | null, store: LocalForage = localforage) { 34 | const item = await store.getItem(id) 35 | return item === null ? null : checkFn(item) 36 | } 37 | 38 | /* istanbul ignore next */ 39 | export async function exists(id: string, store: LocalForage = localforage): Promise { 40 | const key = await store.getItem(id) 41 | return key !== null 42 | } 43 | 44 | /* istanbul ignore next */ 45 | export async function rm(id: string, store: LocalForage = localforage): Promise { 46 | return store.removeItem(id) 47 | } 48 | 49 | export async function dropStore(store: LocalForage): Promise { 50 | return store.dropInstance() 51 | } 52 | 53 | /* istanbul ignore next */ 54 | export async function clear(store?: LocalForage): Promise { 55 | if(store){ 56 | return dropStore(store) 57 | }else { 58 | return localforage.clear() 59 | } 60 | } 61 | 62 | export default { 63 | createStore, 64 | createIfDoesNotExist, 65 | put, 66 | getKeypair, 67 | getKey, 68 | exists, 69 | rm, 70 | dropStore, 71 | clear 72 | } 73 | -------------------------------------------------------------------------------- /test/errors.test.ts: -------------------------------------------------------------------------------- 1 | import errors from '../src/errors' 2 | import mock from './utils/mock' 3 | import { CryptoSystem, KeyUse } from '../src/types' 4 | 5 | describe('errors', () => { 6 | 7 | describe('checkIsKeyPair', () => { 8 | it('throws on null', () => { 9 | expect(() => { 10 | errors.checkIsKeyPair(null) 11 | }).toThrow(errors.KeyDoesNotExist) 12 | }) 13 | 14 | it('throws on a symm key', () => { 15 | expect(() => { 16 | errors.checkIsKeyPair(mock.symmKey) 17 | }).toThrow(errors.NotKeyPair) 18 | }) 19 | 20 | it('returns on valid keyapir', () => { 21 | const resp = errors.checkIsKeyPair(mock.keys) 22 | expect(resp).toEqual(mock.keys) 23 | }) 24 | }) 25 | 26 | 27 | describe('checkIsKey', () => { 28 | it('throws on null', () => { 29 | expect(() => { 30 | errors.checkIsKey(null) 31 | }).toThrow(errors.KeyDoesNotExist) 32 | }) 33 | 34 | it('throws on a symm key', () => { 35 | expect(() => { 36 | errors.checkIsKey(mock.keys) 37 | }).toThrow(errors.NotKey) 38 | }) 39 | 40 | it('returns on valid keyapir', () => { 41 | const resp = errors.checkIsKey(mock.symmKey) 42 | expect(resp).toEqual(mock.symmKey) 43 | }) 44 | }) 45 | 46 | 47 | describe('checkValidCryptoSystem', () => { 48 | it('throws on bad input', () => { 49 | expect(() => { 50 | errors.checkValidCryptoSystem("nonsense" as any) 51 | }).toThrow(errors.UnsupportedCrypto) 52 | }) 53 | 54 | describe('passes on valid inputs', () => { 55 | [CryptoSystem.ECC, CryptoSystem.RSA].map((val: CryptoSystem) => { 56 | it(`passes on ${val}`, () => { 57 | errors.checkValidCryptoSystem(val) 58 | expect(true) 59 | }) 60 | }) 61 | }) 62 | }) 63 | 64 | 65 | describe('checkValidKeyUse', () => { 66 | it('throws on bad input', () => { 67 | expect(() => { 68 | errors.checkValidKeyUse("nonsense" as any) 69 | }).toThrow(errors.InvalidKeyUse) 70 | }) 71 | 72 | describe('passes on valid inputs', () => { 73 | [KeyUse.Exchange, KeyUse.Write].map((val: KeyUse) => { 74 | it(`passes on ${val}`, () => { 75 | errors.checkValidKeyUse(val) 76 | expect(true) 77 | }) 78 | }) 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keystore-idb", 3 | "version": "0.15.5", 4 | "description": "In-browser key management with IndexedDB and the Web Crypto API", 5 | "keywords": [], 6 | "type": "module", 7 | "main": "lib/index.js", 8 | "exports": { 9 | ".": "./lib/index.js", 10 | "./lib/*": "./lib/*", 11 | "./*": "./lib/*", 12 | "./package.json": "./package.json" 13 | }, 14 | "types": "lib/index.d.ts", 15 | "typesVersions": { 16 | "*": { 17 | "lib/index.d.ts": [ 18 | "lib/index.d.ts" 19 | ], 20 | "lib/*": [ 21 | "lib/*" 22 | ], 23 | "*": [ 24 | "lib/*" 25 | ] 26 | } 27 | }, 28 | "files": [ 29 | "lib", 30 | "dist", 31 | "README.md", 32 | "CHANGELOG.md", 33 | "LICENSE", 34 | "package.json", 35 | "!*.test.ts", 36 | "docs" 37 | ], 38 | "author": "Daniel Holmgren ", 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/fission-suite/keystore-idb" 42 | }, 43 | "license": "Apache-2.0", 44 | "engines": { 45 | "node": ">=10.21.0" 46 | }, 47 | "scripts": { 48 | "lint": "yarn eslint src/**/*.ts test/**/*.ts", 49 | "prebuild": "rimraf dist", 50 | "build": "tsc && yarn run build:minified", 51 | "build:minified": "node scripts/build-minified.js", 52 | "start": "tsc -w", 53 | "test": "jest --coverage", 54 | "test:watch": "jest --coverage --watch", 55 | "test:prod": "npm run lint && npm run test -- --no-cache", 56 | "prepare": "yarn build", 57 | "publish-dry": "npm publish --dry-run", 58 | "publish-alpha": "npm publish --tag alpha", 59 | "publish-latest": "npm publish --tag latest" 60 | }, 61 | "devDependencies": { 62 | "@types/jest": "^26.0.0", 63 | "@types/node": "^16.9.1", 64 | "@typescript-eslint/eslint-plugin": "^4.31.0", 65 | "@typescript-eslint/parser": "^4.31.0", 66 | "esbuild": "^0.12.27", 67 | "eslint": "^7.32.0", 68 | "jest": "^26.0.0", 69 | "jest-config": "^26.0.0", 70 | "jest-ts-webcompat-resolver": "^1.0.0", 71 | "rimraf": "^3.0.2", 72 | "ts-jest": "^26.0.0", 73 | "ts-node": "^10.2.1", 74 | "typescript": "^4.4.2" 75 | }, 76 | "dependencies": { 77 | "localforage": "^1.10.0", 78 | "one-webcrypto": "^1.0.3", 79 | "uint8arrays": "^3.0.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/rsa/operations.ts: -------------------------------------------------------------------------------- 1 | import keys from './keys.js' 2 | import utils, { normalizeBase64ToBuf, normalizeUnicodeToBuf } from '../utils.js' 3 | import { DEFAULT_CHAR_SIZE, DEFAULT_HASH_ALG, RSA_EXCHANGE_ALG, RSA_WRITE_ALG, SALT_LENGTH } from '../constants.js' 4 | import { CharSize, HashAlg, KeyUse, Msg, PrivateKey, PublicKey } from '../types.js' 5 | import { webcrypto } from 'one-webcrypto' 6 | 7 | 8 | export async function sign( 9 | msg: Msg, 10 | privateKey: PrivateKey, 11 | charSize: CharSize = DEFAULT_CHAR_SIZE 12 | ): Promise { 13 | return webcrypto.subtle.sign( 14 | { name: RSA_WRITE_ALG, saltLength: SALT_LENGTH }, 15 | privateKey, 16 | normalizeUnicodeToBuf(msg, charSize) 17 | ) 18 | } 19 | 20 | export async function verify( 21 | msg: Msg, 22 | sig: Msg, 23 | publicKey: string | PublicKey, 24 | charSize: CharSize = DEFAULT_CHAR_SIZE, 25 | hashAlg: HashAlg = DEFAULT_HASH_ALG 26 | ): Promise { 27 | return webcrypto.subtle.verify( 28 | { name: RSA_WRITE_ALG, saltLength: SALT_LENGTH }, 29 | typeof publicKey === "string" 30 | ? await keys.importPublicKey(publicKey, hashAlg, KeyUse.Write) 31 | : publicKey, 32 | normalizeBase64ToBuf(sig), 33 | normalizeUnicodeToBuf(msg, charSize) 34 | ) 35 | } 36 | 37 | export async function encrypt( 38 | msg: Msg, 39 | publicKey: string | PublicKey, 40 | charSize: CharSize = DEFAULT_CHAR_SIZE, 41 | hashAlg: HashAlg = DEFAULT_HASH_ALG 42 | ): Promise { 43 | return webcrypto.subtle.encrypt( 44 | { name: RSA_EXCHANGE_ALG }, 45 | typeof publicKey === "string" 46 | ? await keys.importPublicKey(publicKey, hashAlg, KeyUse.Exchange) 47 | : publicKey, 48 | normalizeUnicodeToBuf(msg, charSize) 49 | ) 50 | } 51 | 52 | export async function decrypt( 53 | msg: Msg, 54 | privateKey: PrivateKey 55 | ): Promise { 56 | const normalized = normalizeBase64ToBuf(msg) 57 | return webcrypto.subtle.decrypt( 58 | { name: RSA_EXCHANGE_ALG }, 59 | privateKey, 60 | normalized 61 | ) 62 | } 63 | 64 | export async function getPublicKey(keypair: CryptoKeyPair): Promise { 65 | const spki = await webcrypto.subtle.exportKey('spki', keypair.publicKey as PublicKey) 66 | return utils.arrBufToBase64(spki) 67 | } 68 | 69 | export default { 70 | sign, 71 | verify, 72 | encrypt, 73 | decrypt, 74 | getPublicKey, 75 | } 76 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import KeyStore from '../src' 2 | import ECCKeyStore from '../src/ecc/keystore' 3 | import RSAKeyStore from '../src/rsa/keystore' 4 | import config from '../src/config' 5 | import errors from '../src/errors' 6 | import { CryptoSystem } from '../src/types' 7 | import IDB from '../src/idb' 8 | 9 | jest.mock('../src/idb') 10 | 11 | describe('keystore', () => { 12 | describe('init', () => { 13 | describe('ecc enabled', () => { 14 | 15 | beforeEach(async () => { 16 | const mock = jest.spyOn(config, 'eccEnabled') 17 | mock.mockResolvedValue(true) 18 | }) 19 | 20 | it('should instantiate an ecc keystore if not specified', async () => { 21 | const resp = await KeyStore.init() 22 | const eccKeystore = await ECCKeyStore.init() 23 | expect(resp).toStrictEqual(eccKeystore) 24 | }) 25 | 26 | it('should instantiate an rsa keystore if specified', async () => { 27 | const resp = await KeyStore.init({ type: CryptoSystem.RSA }) 28 | const rsaKeystore = await RSAKeyStore.init() 29 | expect(resp).toStrictEqual(rsaKeystore) 30 | }) 31 | }) 32 | 33 | describe('ecc not enabled', () => { 34 | 35 | beforeEach(async () => { 36 | jest.spyOn(config, 'eccEnabled').mockResolvedValue(false) 37 | }) 38 | 39 | it('should instantiate an rsa keystore if not specified', async () => { 40 | const resp = await KeyStore.init() 41 | const rsaKeystore = await RSAKeyStore.init() 42 | expect(resp).toStrictEqual(rsaKeystore) 43 | }) 44 | 45 | it('should throw an error if ecc is specified', async () => { 46 | let error 47 | try{ 48 | await KeyStore.init({ type: CryptoSystem.ECC }) 49 | }catch(err){ 50 | error = err 51 | } 52 | expect(error).toEqual(errors.ECCNotEnabled) 53 | }) 54 | 55 | it('should throw an error if an unsupported type of crypto is specified', async () => { 56 | let error 57 | try{ 58 | await KeyStore.init({ type: 'some-other-crypto' as any }) 59 | }catch(err){ 60 | error = err 61 | } 62 | expect(error).toEqual(errors.UnsupportedCrypto) 63 | }) 64 | 65 | }) 66 | }) 67 | 68 | describe('clear', () => { 69 | let mock: jest.SpyInstance 70 | 71 | beforeAll(async () => { 72 | mock = jest.spyOn(IDB, 'clear') 73 | await KeyStore.clear() 74 | }) 75 | 76 | it('calls IDB.clear once', () => { 77 | expect(mock).toBeCalledTimes(1) 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | type Req = () => Promise 3 | type ParamCheckFn = (params: any) => boolean 4 | 5 | type ParamCheck = { 6 | desc: string; 7 | req: Req; 8 | params: any[] | ParamCheckFn; 9 | } 10 | 11 | type ShouldThrow = { 12 | desc: string; 13 | req: Req; 14 | error: Error; 15 | } 16 | 17 | type WebCryptoReqOpts = { 18 | desc: string; 19 | setMock: (fake: jest.Mock) => void; 20 | mockResp: any; 21 | expectedResp?: any; 22 | simpleReq: Req; 23 | simpleParams?: any[]; 24 | paramChecks: ParamCheck[]; 25 | shouldThrows: ShouldThrow[]; 26 | } 27 | /* eslint-enable @typescript-eslint/no-explicit-any */ 28 | 29 | export const cryptoMethod = (opts: WebCryptoReqOpts): void => { 30 | return describe(opts.desc, () => { 31 | let fake: jest.Mock 32 | 33 | beforeEach(async () => { 34 | fake = jest.fn(() => new Promise(r => r(opts.mockResp))) 35 | opts.setMock(fake) 36 | }) 37 | 38 | it('sends only one request', async () => { 39 | await opts.simpleReq() 40 | expect(fake).toBeCalledTimes(1) 41 | }) 42 | 43 | it('returns expected response', async () => { 44 | const response = await opts.simpleReq() 45 | if(opts.expectedResp){ 46 | expect(response).toEqual(opts.expectedResp) 47 | }else{ 48 | expect(response).toEqual(opts.mockResp) 49 | } 50 | }) 51 | 52 | if(opts.simpleParams !== undefined){ 53 | it('correctly passes params', async () => { 54 | await opts.simpleReq() 55 | expect(fake.mock.calls[0]).toEqual(opts.simpleParams) 56 | }) 57 | } 58 | 59 | opts.paramChecks.forEach(test => { 60 | it(test.desc, async () => { 61 | await test.req() 62 | if(typeof test.params === 'function'){ 63 | expect(test.params(fake.mock.calls[0])).toBeTruthy() 64 | }else { 65 | expect(fake.mock.calls[0]).toEqual(test.params) 66 | } 67 | }) 68 | }) 69 | 70 | opts.shouldThrows.forEach(test => { 71 | it(test.desc, async() => { 72 | let error 73 | try { 74 | await test.req() 75 | }catch(err){ 76 | error = err 77 | } 78 | expect(error).toBe(test.error) 79 | }) 80 | }) 81 | 82 | }) 83 | } 84 | 85 | export function arrBufEq(fstBuf: ArrayBuffer, sndBuf: ArrayBuffer): boolean { 86 | const fst = new Uint8Array(fstBuf) 87 | const snd = new Uint8Array(sndBuf) 88 | if (fst.length !== snd.length) { 89 | return false 90 | } 91 | for(let i=0; i 11 | ): Promise { 12 | const data = utils.normalizeUtf16ToBuf(msg) 13 | const importedKey = typeof key === 'string' ? await keys.importKey(key, opts) : key 14 | const alg = opts?.alg || DEFAULT_SYMM_ALG 15 | const iv = opts?.iv || utils.randomBuf(16) 16 | const cipherBuf = await webcrypto.subtle.encrypt( 17 | { 18 | name: alg, 19 | // AES-CTR uses a counter, AES-GCM/AES-CBC use an initialization vector 20 | iv: alg === SymmAlg.AES_CTR ? undefined : iv, 21 | counter: alg === SymmAlg.AES_CTR ? new Uint8Array(iv) : undefined, 22 | length: alg === SymmAlg.AES_CTR ? DEFAULT_CTR_LEN : undefined, 23 | }, 24 | importedKey, 25 | data 26 | ) 27 | return utils.joinBufs(iv, cipherBuf) 28 | } 29 | 30 | export async function decryptBytes( 31 | msg: Msg, 32 | key: SymmKey | string, 33 | opts?: Partial 34 | ): Promise { 35 | const cipherText = utils.normalizeBase64ToBuf(msg) 36 | const importedKey = typeof key === 'string' ? await keys.importKey(key, opts) : key 37 | const alg = opts?.alg || DEFAULT_SYMM_ALG 38 | const iv = cipherText.slice(0, 16) 39 | const cipherBytes = cipherText.slice(16) 40 | const msgBuff = await webcrypto.subtle.decrypt( 41 | { name: alg, 42 | // AES-CTR uses a counter, AES-GCM/AES-CBC use an initialization vector 43 | iv: alg === SymmAlg.AES_CTR ? undefined : iv, 44 | counter: alg === SymmAlg.AES_CTR ? new Uint8Array(iv) : undefined, 45 | length: alg === SymmAlg.AES_CTR ? DEFAULT_CTR_LEN : undefined, 46 | }, 47 | importedKey, 48 | cipherBytes 49 | ) 50 | return msgBuff 51 | } 52 | 53 | export async function encrypt( 54 | msg: Msg, 55 | key: SymmKey | string, 56 | opts?: Partial 57 | ): Promise { 58 | const cipherText = await encryptBytes(msg, key, opts) 59 | return utils.arrBufToBase64(cipherText) 60 | } 61 | 62 | export async function decrypt( 63 | msg: Msg, 64 | key: SymmKey | string, 65 | opts?: Partial 66 | ): Promise { 67 | const msgBytes = await decryptBytes(msg, key, opts) 68 | return utils.arrBufToStr(msgBytes, 16) 69 | } 70 | 71 | 72 | export async function exportKey(key: SymmKey): Promise { 73 | const raw = await webcrypto.subtle.exportKey('raw', key) 74 | return utils.arrBufToBase64(raw) 75 | } 76 | 77 | export default { 78 | encryptBytes, 79 | decryptBytes, 80 | encrypt, 81 | decrypt, 82 | exportKey 83 | } 84 | -------------------------------------------------------------------------------- /src/keystore/base.ts: -------------------------------------------------------------------------------- 1 | import aes from '../aes/index.js' 2 | import idb from '../idb.js' 3 | import utils from '../utils.js' 4 | import config from '../config.js' 5 | import { Config } from '../types.js' 6 | import { checkIsKeyPair } from '../errors.js' 7 | 8 | export default class KeyStoreBase { 9 | 10 | cfg: Config 11 | protected store: LocalForage 12 | 13 | constructor(cfg: Config, store: LocalForage) { 14 | this.cfg = cfg 15 | this.store = store 16 | } 17 | 18 | async writeKey(): Promise { 19 | const maybeKey = await idb.getKeypair(this.cfg.writeKeyName, this.store) 20 | return checkIsKeyPair(maybeKey) 21 | } 22 | 23 | async exchangeKey(): Promise { 24 | const maybeKey = await idb.getKeypair(this.cfg.exchangeKeyName, this.store) 25 | return checkIsKeyPair(maybeKey) 26 | } 27 | 28 | async getSymmKey(keyName: string, cfg?: Partial): Promise { 29 | const mergedCfg = config.merge(this.cfg, cfg) 30 | const maybeKey = await idb.getKey(keyName, this.store) 31 | if(maybeKey !== null) { 32 | return maybeKey 33 | } 34 | const key = await aes.makeKey(config.symmKeyOpts(mergedCfg)) 35 | await idb.put(keyName, key, this.store) 36 | return key 37 | } 38 | 39 | async keyExists(keyName: string): Promise { 40 | const key = await idb.getKey(keyName, this.store) 41 | return key !== null 42 | } 43 | 44 | async deleteKey(keyName: string): Promise { 45 | return idb.rm(keyName, this.store) 46 | } 47 | 48 | async destroy(): Promise { 49 | return idb.dropStore(this.store) 50 | } 51 | 52 | async importSymmKey(keyStr: string, keyName: string, cfg?: Partial): Promise { 53 | const mergedCfg = config.merge(this.cfg, cfg) 54 | const key = await aes.importKey(keyStr, config.symmKeyOpts(mergedCfg)) 55 | await idb.put(keyName, key, this.store) 56 | } 57 | 58 | async exportSymmKey(keyName: string, cfg?: Partial): Promise { 59 | const key = await this.getSymmKey(keyName, cfg) 60 | return aes.exportKey(key) 61 | } 62 | 63 | async encryptWithSymmKey(msg: string, keyName: string, cfg?: Partial): Promise { 64 | const mergedCfg = config.merge(this.cfg, cfg) 65 | const key = await this.getSymmKey(keyName, cfg) 66 | const cipherText = await aes.encryptBytes( 67 | utils.strToArrBuf(msg, mergedCfg.charSize), 68 | key, 69 | config.symmKeyOpts(mergedCfg) 70 | ) 71 | return utils.arrBufToBase64(cipherText) 72 | } 73 | 74 | async decryptWithSymmKey(cipherText: string, keyName: string, cfg?: Partial): Promise { 75 | const mergedCfg = config.merge(this.cfg, cfg) 76 | const key = await this.getSymmKey(keyName, cfg) 77 | const msgBytes = await aes.decryptBytes( 78 | utils.base64ToArrBuf(cipherText), 79 | key, 80 | config.symmKeyOpts(mergedCfg) 81 | ) 82 | return utils.arrBufToStr(msgBytes, mergedCfg.charSize) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Msg = ArrayBuffer | string | Uint8Array 2 | 3 | export type CipherText = ArrayBuffer 4 | export type SymmKey = CryptoKey 5 | 6 | export type PublicKey = CryptoKey 7 | export type PrivateKey = CryptoKey 8 | 9 | export type Config = { 10 | type: CryptoSystem 11 | curve: EccCurve 12 | rsaSize: RsaSize 13 | symmAlg: SymmAlg 14 | symmLen: SymmKeyLength 15 | hashAlg: HashAlg 16 | charSize: CharSize 17 | storeName: string 18 | exchangeKeyName: string 19 | writeKeyName: string 20 | } 21 | 22 | export type SymmKeyOpts = { 23 | alg: SymmAlg 24 | length: SymmKeyLength 25 | iv: ArrayBuffer 26 | } 27 | 28 | export enum CryptoSystem { 29 | ECC = 'ecc', 30 | RSA = 'rsa', 31 | } 32 | 33 | export enum EccCurve { 34 | P_256 = 'P-256', 35 | P_384 = 'P-384', 36 | P_521 = 'P-521', 37 | } 38 | 39 | export enum RsaSize { 40 | B1024 = 1024, 41 | B2048 = 2048, 42 | B4096 = 4096 43 | } 44 | 45 | export enum SymmAlg { 46 | AES_CTR = 'AES-CTR', 47 | AES_CBC = 'AES-CBC', 48 | AES_GCM = 'AES-GCM', 49 | } 50 | 51 | export enum SymmKeyLength { 52 | B128 = 128, 53 | B192 = 192, 54 | B256 = 256, 55 | } 56 | 57 | export enum HashAlg { 58 | SHA_1 = 'SHA-1', 59 | SHA_256 = 'SHA-256', 60 | SHA_384 = 'SHA-384', 61 | SHA_512 = 'SHA-512', 62 | } 63 | 64 | export enum CharSize { 65 | B8 = 8, 66 | B16 = 16, 67 | } 68 | 69 | export enum KeyUse { 70 | Exchange = 'exchange', 71 | Write = 'write', 72 | } 73 | 74 | export interface KeyStore { 75 | cfg: Config 76 | 77 | exchangeKey: () => Promise 78 | writeKey: () => Promise 79 | getSymmKey: (keyName: string, cfg?: Partial) => Promise 80 | keyExists(keyName: string): Promise 81 | deleteKey(keyName: string): Promise 82 | destroy(): Promise 83 | 84 | // Symmetric 85 | 86 | importSymmKey( 87 | keyStr: string, 88 | keyName: string, 89 | cfg?: Partial 90 | ): Promise 91 | 92 | exportSymmKey( 93 | keyName: string, 94 | cfg?: Partial 95 | ): Promise 96 | 97 | encryptWithSymmKey( 98 | msg: string, 99 | keyName: string, 100 | cfg?: Partial 101 | ): Promise 102 | 103 | decryptWithSymmKey( 104 | cipherBytes: string, 105 | keyName: string, 106 | cfg?: Partial 107 | ): Promise 108 | 109 | // Asymmetric 110 | 111 | sign( 112 | msg: string, 113 | cfg?: Partial 114 | ): Promise 115 | 116 | verify( 117 | msg: string, 118 | sig: string, 119 | publicKey: string, 120 | cfg?: Partial 121 | ): Promise 122 | 123 | encrypt( 124 | msg: string, 125 | publicKey: string, 126 | cfg?: Partial 127 | ): Promise 128 | 129 | decrypt( 130 | cipherText: string, 131 | publicKey: string, 132 | cfg?: Partial 133 | ): Promise 134 | 135 | publicExchangeKey(): Promise 136 | publicWriteKey(): Promise 137 | } 138 | -------------------------------------------------------------------------------- /src/rsa/keystore.ts: -------------------------------------------------------------------------------- 1 | import IDB from '../idb.js' 2 | import keys from './keys.js' 3 | import operations from './operations.js' 4 | import config from '../config.js' 5 | import utils from '../utils.js' 6 | import KeyStoreBase from '../keystore/base.js' 7 | import { KeyStore, Config, KeyUse, CryptoSystem, Msg, PublicKey, PrivateKey } from '../types.js' 8 | 9 | export class RSAKeyStore extends KeyStoreBase implements KeyStore { 10 | 11 | static async init(maybeCfg?: Partial): Promise { 12 | const cfg = config.normalize({ 13 | ...(maybeCfg || {}), 14 | type: CryptoSystem.RSA 15 | }) 16 | 17 | const { rsaSize, hashAlg, storeName, exchangeKeyName, writeKeyName } = cfg 18 | const store = IDB.createStore(storeName) 19 | 20 | await IDB.createIfDoesNotExist(exchangeKeyName, () => ( 21 | keys.makeKeypair(rsaSize, hashAlg, KeyUse.Exchange) 22 | ), store) 23 | await IDB.createIfDoesNotExist(writeKeyName, () => ( 24 | keys.makeKeypair(rsaSize, hashAlg, KeyUse.Write) 25 | ), store) 26 | 27 | return new RSAKeyStore(cfg, store) 28 | } 29 | 30 | 31 | async sign(msg: Msg, cfg?: Partial): Promise { 32 | const mergedCfg = config.merge(this.cfg, cfg) 33 | const writeKey = await this.writeKey() 34 | 35 | return utils.arrBufToBase64(await operations.sign( 36 | msg, 37 | writeKey.privateKey as PrivateKey, 38 | mergedCfg.charSize 39 | )) 40 | } 41 | 42 | async verify( 43 | msg: string, 44 | sig: string, 45 | publicKey: string | PublicKey, 46 | cfg?: Partial 47 | ): Promise { 48 | const mergedCfg = config.merge(this.cfg, cfg) 49 | 50 | return operations.verify( 51 | msg, 52 | sig, 53 | publicKey, 54 | mergedCfg.charSize, 55 | mergedCfg.hashAlg 56 | ) 57 | } 58 | 59 | async encrypt( 60 | msg: Msg, 61 | publicKey: string | PublicKey, 62 | cfg?: Partial 63 | ): Promise { 64 | const mergedCfg = config.merge(this.cfg, cfg) 65 | 66 | return utils.arrBufToBase64(await operations.encrypt( 67 | msg, 68 | publicKey, 69 | mergedCfg.charSize, 70 | mergedCfg.hashAlg 71 | )) 72 | } 73 | 74 | async decrypt( 75 | cipherText: Msg, 76 | publicKey?: string | PublicKey, // unused param so that keystore interfaces match 77 | cfg?: Partial 78 | ): Promise { 79 | const exchangeKey = await this.exchangeKey() 80 | const mergedCfg = config.merge(this.cfg, cfg) 81 | 82 | return utils.arrBufToStr( 83 | await operations.decrypt( 84 | cipherText, 85 | exchangeKey.privateKey as PrivateKey, 86 | ), 87 | mergedCfg.charSize 88 | ) 89 | } 90 | 91 | async publicExchangeKey(): Promise { 92 | const exchangeKey = await this.exchangeKey() 93 | return operations.getPublicKey(exchangeKey) 94 | } 95 | 96 | async publicWriteKey(): Promise { 97 | const writeKey = await this.writeKey() 98 | return operations.getPublicKey(writeKey) 99 | } 100 | } 101 | 102 | export default RSAKeyStore 103 | -------------------------------------------------------------------------------- /src/ecc/keystore.ts: -------------------------------------------------------------------------------- 1 | import IDB from '../idb.js' 2 | import keys from './keys.js' 3 | import operations from './operations.js' 4 | import config from '../config.js' 5 | import utils from '../utils.js' 6 | import KeyStoreBase from '../keystore/base.js' 7 | import { KeyStore, Config, KeyUse, CryptoSystem, PrivateKey } from '../types.js' 8 | 9 | export class ECCKeyStore extends KeyStoreBase implements KeyStore { 10 | 11 | static async init(maybeCfg?: Partial): Promise { 12 | const cfg = config.normalize({ 13 | ...(maybeCfg || {}), 14 | type: CryptoSystem.ECC 15 | }) 16 | const { curve, storeName, exchangeKeyName, writeKeyName } = cfg 17 | 18 | const store = IDB.createStore(storeName) 19 | await IDB.createIfDoesNotExist(exchangeKeyName, () => ( 20 | keys.makeKeypair(curve, KeyUse.Exchange) 21 | ), store) 22 | await IDB.createIfDoesNotExist(writeKeyName, () => ( 23 | keys.makeKeypair(curve, KeyUse.Write) 24 | ), store) 25 | 26 | return new ECCKeyStore(cfg, store) 27 | } 28 | 29 | 30 | async sign(msg: string, cfg?: Partial): Promise { 31 | const mergedCfg = config.merge(this.cfg, cfg) 32 | const writeKey = await this.writeKey() 33 | 34 | return utils.arrBufToBase64(await operations.sign( 35 | msg, 36 | writeKey.privateKey as PrivateKey, 37 | mergedCfg.charSize, 38 | mergedCfg.hashAlg 39 | )) 40 | } 41 | 42 | async verify( 43 | msg: string, 44 | sig: string, 45 | publicKey: string, 46 | cfg?: Partial 47 | ): Promise { 48 | const mergedCfg = config.merge(this.cfg, cfg) 49 | 50 | return operations.verify( 51 | msg, 52 | sig, 53 | publicKey, 54 | mergedCfg.charSize, 55 | mergedCfg.curve, 56 | mergedCfg.hashAlg 57 | ) 58 | } 59 | 60 | async encrypt( 61 | msg: string, 62 | publicKey: string, 63 | cfg?: Partial 64 | ): Promise { 65 | const mergedCfg = config.merge(this.cfg, cfg) 66 | const exchangeKey = await this.exchangeKey() 67 | 68 | return utils.arrBufToBase64(await operations.encrypt( 69 | msg, 70 | exchangeKey.privateKey as PrivateKey, 71 | publicKey, 72 | mergedCfg.charSize, 73 | mergedCfg.curve 74 | )) 75 | } 76 | 77 | async decrypt( 78 | cipherText: string, 79 | publicKey: string, 80 | cfg?: Partial 81 | ): Promise { 82 | const mergedCfg = config.merge(this.cfg, cfg) 83 | const exchangeKey = await this.exchangeKey() 84 | 85 | return utils.arrBufToStr( 86 | await operations.decrypt( 87 | cipherText, 88 | exchangeKey.privateKey as PrivateKey, 89 | publicKey, 90 | mergedCfg.curve 91 | ), 92 | mergedCfg.charSize 93 | ) 94 | } 95 | 96 | async publicExchangeKey(): Promise { 97 | const exchangeKey = await this.exchangeKey() 98 | return operations.getPublicKey(exchangeKey) 99 | } 100 | 101 | async publicWriteKey(): Promise { 102 | const writeKey = await this.writeKey() 103 | return operations.getPublicKey(writeKey) 104 | } 105 | } 106 | 107 | export default ECCKeyStore 108 | -------------------------------------------------------------------------------- /src/ecc/operations.ts: -------------------------------------------------------------------------------- 1 | import aes from '../aes/index.js' 2 | import keys from './keys.js' 3 | import utils, { normalizeBase64ToBuf, normalizeUnicodeToBuf } from '../utils.js' 4 | import { DEFAULT_CHAR_SIZE, DEFAULT_ECC_CURVE, DEFAULT_HASH_ALG, ECC_EXCHANGE_ALG, ECC_WRITE_ALG, DEFAULT_SYMM_ALG, DEFAULT_SYMM_LEN } from '../constants.js' 5 | import { CharSize, EccCurve, Msg, PrivateKey, PublicKey, HashAlg, KeyUse, SymmKey, SymmKeyOpts } from '../types.js' 6 | import { webcrypto } from 'one-webcrypto' 7 | 8 | 9 | export async function sign( 10 | msg: Msg, 11 | privateKey: PrivateKey, 12 | charSize: CharSize = DEFAULT_CHAR_SIZE, 13 | hashAlg: HashAlg = DEFAULT_HASH_ALG, 14 | ): Promise { 15 | return webcrypto.subtle.sign( 16 | { name: ECC_WRITE_ALG, hash: { name: hashAlg }}, 17 | privateKey, 18 | normalizeUnicodeToBuf(msg, charSize) 19 | ) 20 | } 21 | 22 | export async function verify( 23 | msg: Msg, 24 | sig: Msg, 25 | publicKey: string | PublicKey, 26 | charSize: CharSize = DEFAULT_CHAR_SIZE, 27 | curve: EccCurve = DEFAULT_ECC_CURVE, 28 | hashAlg: HashAlg = DEFAULT_HASH_ALG 29 | ): Promise { 30 | return webcrypto.subtle.verify( 31 | { name: ECC_WRITE_ALG, hash: { name: hashAlg }}, 32 | typeof publicKey === "string" 33 | ? await keys.importPublicKey(publicKey, curve, KeyUse.Write) 34 | : publicKey, 35 | normalizeBase64ToBuf(sig), 36 | normalizeUnicodeToBuf(msg, charSize) 37 | ) 38 | } 39 | 40 | export async function encrypt( 41 | msg: Msg, 42 | privateKey: PrivateKey, 43 | publicKey: string | PublicKey, 44 | charSize: CharSize = DEFAULT_CHAR_SIZE, 45 | curve: EccCurve = DEFAULT_ECC_CURVE, 46 | opts?: Partial 47 | ): Promise { 48 | const importedPublicKey = typeof publicKey === "string" 49 | ? await keys.importPublicKey(publicKey, curve, KeyUse.Exchange) 50 | : publicKey 51 | 52 | const cipherKey = await getSharedKey(privateKey, importedPublicKey, opts) 53 | return aes.encryptBytes(normalizeUnicodeToBuf(msg, charSize), cipherKey, opts) 54 | } 55 | 56 | export async function decrypt( 57 | msg: Msg, 58 | privateKey: PrivateKey, 59 | publicKey: string | PublicKey, 60 | curve: EccCurve = DEFAULT_ECC_CURVE, 61 | opts?: Partial 62 | ): Promise { 63 | const importedPublicKey = typeof publicKey === "string" 64 | ? await keys.importPublicKey(publicKey, curve, KeyUse.Exchange) 65 | : publicKey 66 | 67 | const cipherKey = await getSharedKey(privateKey, importedPublicKey, opts) 68 | return aes.decryptBytes(normalizeBase64ToBuf(msg), cipherKey, opts) 69 | } 70 | 71 | export async function getPublicKey(keypair: CryptoKeyPair): Promise { 72 | const raw = await webcrypto.subtle.exportKey('raw', keypair.publicKey as PublicKey) 73 | return utils.arrBufToBase64(raw) 74 | } 75 | 76 | export async function getSharedKey(privateKey: PrivateKey, publicKey: PublicKey, opts?: Partial): Promise { 77 | return webcrypto.subtle.deriveKey( 78 | { name: ECC_EXCHANGE_ALG, public: publicKey }, 79 | privateKey, 80 | { 81 | name: opts?.alg || DEFAULT_SYMM_ALG, 82 | length: opts?.length || DEFAULT_SYMM_LEN 83 | }, 84 | false, 85 | ['encrypt', 'decrypt'] 86 | ) 87 | } 88 | 89 | export default { 90 | sign, 91 | verify, 92 | encrypt, 93 | decrypt, 94 | getPublicKey, 95 | getSharedKey 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IndexedDB KeyStore 2 | 3 | [![NPM](https://img.shields.io/npm/v/keystore-idb)](https://www.npmjs.com/package/keystore-idb) 4 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/fission-suite/blob/master/LICENSE) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/b0fabd7e80c6bd2c0c7b/maintainability)](https://codeclimate.com/github/fission-suite/keystore-idb/maintainability) 6 | [![Built by FISSION](https://img.shields.io/badge/⌘-Built_by_FISSION-purple.svg)](https://fission.codes) 7 | [![Discord](https://img.shields.io/discord/478735028319158273.svg)](https://discord.gg/zAQBDEq) 8 | [![Discourse](https://img.shields.io/discourse/https/talk.fission.codes/topics)](https://talk.fission.codes) 9 | 10 | In-browser key management with IndexedDB and the Web Crypto API. 11 | 12 | Securely store and use keys for encryption, decryption, and signatures. IndexedDB and Web Crypto keep keys safe from malicious javascript. 13 | 14 | Supports both RSA (RSASSA-PKCS1-v1_5 & RSA-OAEP) and Elliptic Curves (P-256, P-381 & P-521). 15 | 16 | ECC (Elliptic Curve Cryptography) is only available on Chrome. Firefox and Safari do not support ECC and must use RSA. 17 | _Specifically, this is an issue with storing ECC keys in IndexedDB_ 18 | 19 | 20 | 21 | ## Config 22 | 23 | Below is the default config and all possible values 24 | _Note: these are given as primitives, but in Typescript you can use the included enums_ 25 | 26 | ```typescript 27 | const defaultConfig = { 28 | type: 'ecc', // 'ecc' | 'rsa' 29 | curve: 'P-256', // 'P-256' | 'P-384' | 'P-521' 30 | rsaSize: 2048, // 1024 | 2048 | 4096 31 | symmAlg: 'AES-CTR', // 'AES-CTR' | 'AES-GCM' | 'AES-CBC' 32 | symmLen: 128, // 128 | 192 | 256 33 | hashAlg: 'SHA-256', // 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512' 34 | charSize: 16, // 8 | 16 35 | storeName: 'keystore', // any string 36 | exchangeKeyName: 'exchange-key', // any string 37 | writeKeyName: 'write-key', // any string 38 | } 39 | ``` 40 | _Note: if you don't include a crypto "type" (`'ecc' | 'rsa'`), the library will check if your browser supports ECC. If so (Chrome), it will use ECC, if not (Firefox, Safari) it will fall back to RSA._ 41 | 42 | 43 | 44 | ## Example Usage 45 | 46 | ```typescript 47 | import keystore from 'keystore-idb' 48 | 49 | async function run() { 50 | await keystore.clear() 51 | 52 | const ks1 = await keystore.init({ storeName: 'keystore' }) 53 | const ks2 = await keystore.init({ storeName: 'keystore2' }) 54 | 55 | const msg = "Incididunt id ullamco et do." 56 | 57 | // exchange keys and write keys are separate because of the Web Crypto API 58 | const exchangeKey1 = await ks1.publicExchangeKey() 59 | const writeKey1 = await ks1.publicWriteKey() 60 | const exchangeKey2 = await ks2.publicExchangeKey() 61 | 62 | // these keys get exported as strings 63 | console.log('exchangeKey1: ', exchangeKey1) 64 | console.log('writeKey1: ', writeKey1) 65 | console.log('exchangeKey2: ', exchangeKey2) 66 | 67 | const sig = await ks1.sign(msg) 68 | const valid = await ks2.verify(msg, sig, writeKey1) 69 | console.log('sig: ', sig) 70 | console.log('valid: ', valid) 71 | 72 | const cipher = await ks1.encrypt(msg, exchangeKey2) 73 | const decipher = await ks2.decrypt(cipher, exchangeKey1) 74 | console.log('cipher: ', cipher) 75 | console.log('decipher: ', decipher) 76 | } 77 | 78 | run() 79 | ``` 80 | 81 | 82 | 83 | ## Development 84 | 85 | ```shell 86 | # install dependencies 87 | yarn 88 | 89 | # run development server 90 | yarn start 91 | 92 | # build 93 | yarn build 94 | 95 | # test 96 | yarn test 97 | 98 | # test w/ reloading 99 | yarn test:watch 100 | 101 | # publish (run this script instead of npm publish!) 102 | ./publish.sh 103 | ``` 104 | -------------------------------------------------------------------------------- /test/config.test.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'one-webcrypto' 2 | import config from '../src/config' 3 | import { CryptoSystem, SymmAlg, SymmKeyLength } from '../src/types' 4 | import utils from '../src/utils' 5 | import { mock } from './utils' 6 | 7 | describe('config', () => { 8 | describe('eccEnabled', () => { 9 | 10 | describe('structural clone works', () => { 11 | let fakeClone: jest.SpyInstance 12 | let fakeMake: jest.Mock 13 | let response: boolean 14 | 15 | beforeAll(async () => { 16 | fakeClone = jest.spyOn(utils, 'structuralClone') 17 | fakeClone.mockResolvedValue(undefined) 18 | 19 | fakeMake = jest.fn(() => new Promise(r => r(mock.keys))) 20 | webcrypto.subtle.generateKey = fakeMake 21 | 22 | response = await config.eccEnabled() 23 | }) 24 | 25 | it('calls structural clone once', () => { 26 | expect(fakeClone).toBeCalledTimes(1) 27 | }) 28 | 29 | it('returns true', () => { 30 | expect(response).toEqual(true) 31 | }) 32 | }) 33 | 34 | describe('structural clone does not works', () => { 35 | let fakeClone: jest.SpyInstance 36 | let fakeMake: jest.Mock 37 | let response: boolean 38 | 39 | beforeAll(async () => { 40 | fakeClone = jest.spyOn(utils, 'structuralClone') 41 | fakeClone.mockReturnValue( 42 | new Promise((_resp, rej) => rej(new Error("cannot structural clone"))) 43 | ) 44 | 45 | fakeMake = jest.fn(() => new Promise(r => r(mock.keys))) 46 | webcrypto.subtle.generateKey = fakeMake 47 | 48 | response = await config.eccEnabled() 49 | }) 50 | 51 | it('calls structural clone once', () => { 52 | expect(fakeClone).toBeCalledTimes(1) 53 | }) 54 | 55 | it('returns false', () => { 56 | expect(response).toEqual(false) 57 | }) 58 | }) 59 | }) 60 | 61 | 62 | describe('normalize', () => { 63 | it('defaults to defaultConfig', () => { 64 | const cfg = config.normalize() 65 | expect(cfg).toEqual(config.defaultConfig) 66 | }) 67 | 68 | it('merges with default config', () => { 69 | const cfg = config.normalize({ 70 | exchangeKeyName: 'test' 71 | }) 72 | const modifiedDef = { 73 | ...config.defaultConfig, 74 | exchangeKeyName: 'test' 75 | } 76 | expect(cfg).toEqual(modifiedDef) 77 | }) 78 | 79 | it('sets ecc if enabled', () => { 80 | const cfg = config.normalize({}, true) 81 | const modifiedDef = { 82 | ...config.defaultConfig, 83 | type: 'ecc' 84 | } 85 | expect(cfg).toEqual(modifiedDef) 86 | }) 87 | 88 | it('sets rsa if ecc not enabled', () => { 89 | const cfg = config.normalize({}, false) 90 | const modifiedDef = { 91 | ...config.defaultConfig, 92 | type: 'rsa' 93 | } 94 | expect(cfg).toEqual(modifiedDef) 95 | }) 96 | 97 | 98 | it('does not overwrite type if user defined', () => { 99 | const cfg = config.normalize({type: CryptoSystem.RSA}, true) 100 | const modifiedDef = { 101 | ...config.defaultConfig, 102 | type: 'rsa' 103 | } 104 | expect(cfg).toEqual(modifiedDef) 105 | }) 106 | }) 107 | 108 | describe('merge', () => { 109 | it('it correctly merges configs', () => { 110 | const merged = config.merge(config.defaultConfig, { symmAlg: SymmAlg.AES_CBC, symmLen: SymmKeyLength.B192 }) 111 | expect(merged).toEqual({ 112 | ...config.defaultConfig, 113 | symmAlg: SymmAlg.AES_CBC, 114 | symmLen: SymmKeyLength.B192 115 | }) 116 | }) 117 | 118 | it('it works when an empty overwrite is passed', () => { 119 | const merged = config.merge(config.defaultConfig) 120 | expect(merged).toEqual(config.defaultConfig) 121 | }) 122 | }) 123 | 124 | }) 125 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'one-webcrypto' 2 | import * as uint8arrays from 'uint8arrays' 3 | import errors from './errors.js' 4 | import { CharSize, Msg } from './types.js' 5 | 6 | 7 | export function arrBufToStr(buf: ArrayBuffer, charSize: CharSize): string { 8 | const arr = charSize === 8 ? new Uint8Array(buf) : new Uint16Array(buf) 9 | return Array.from(arr) 10 | .map(b => String.fromCharCode(b)) 11 | .join('') 12 | } 13 | 14 | export function arrBufToBase64(buf: ArrayBuffer): string { 15 | return uint8arrays.toString(new Uint8Array(buf), "base64pad") 16 | } 17 | 18 | export function strToArrBuf(str: string, charSize: CharSize): ArrayBuffer { 19 | const view = 20 | charSize === 8 ? new Uint8Array(str.length) : new Uint16Array(str.length) 21 | for (let i = 0, strLen = str.length; i < strLen; i++) { 22 | view[i] = str.charCodeAt(i) 23 | } 24 | return view.buffer 25 | } 26 | 27 | export function base64ToArrBuf(string: string): ArrayBuffer { 28 | return uint8arrays.fromString(string, "base64pad").buffer 29 | } 30 | 31 | export function publicExponent(): Uint8Array { 32 | return new Uint8Array([0x01, 0x00, 0x01]) 33 | } 34 | 35 | export function randomBuf(length: number, { max }: { max: number } = { max: 255 }): ArrayBuffer { 36 | if (max < 1 || max > 255) { 37 | throw errors.InvalidMaxValue 38 | } 39 | 40 | const arr = new Uint8Array(length) 41 | 42 | if (max == 255) { 43 | webcrypto.getRandomValues(arr) 44 | return arr.buffer 45 | } 46 | 47 | let index = 0 48 | const interval = max + 1 49 | const divisibleMax = Math.floor(256 / interval) * interval 50 | const tmp = new Uint8Array(1) 51 | 52 | while (index < arr.length) { 53 | webcrypto.getRandomValues(tmp) 54 | if (tmp[0] < divisibleMax) { 55 | arr[index] = tmp[0] % interval 56 | index++ 57 | } 58 | } 59 | 60 | return arr.buffer 61 | } 62 | 63 | export function joinBufs(fst: ArrayBuffer, snd: ArrayBuffer): ArrayBuffer { 64 | const view1 = new Uint8Array(fst) 65 | const view2 = new Uint8Array(snd) 66 | const joined = new Uint8Array(view1.length + view2.length) 67 | joined.set(view1) 68 | joined.set(view2, view1.length) 69 | return joined.buffer 70 | } 71 | 72 | export const normalizeUtf8ToBuf = (msg: Msg): ArrayBuffer => { 73 | return normalizeToBuf(msg, (str) => strToArrBuf(str, CharSize.B8)) 74 | } 75 | 76 | export const normalizeUtf16ToBuf = (msg: Msg): ArrayBuffer => { 77 | return normalizeToBuf(msg, (str) => strToArrBuf(str, CharSize.B16)) 78 | } 79 | 80 | export const normalizeBase64ToBuf = (msg: Msg): ArrayBuffer => { 81 | return normalizeToBuf(msg, base64ToArrBuf) 82 | } 83 | 84 | export const normalizeUnicodeToBuf = (msg: Msg, charSize: CharSize) => { 85 | switch (charSize) { 86 | case 8: return normalizeUtf8ToBuf(msg) 87 | default: return normalizeUtf16ToBuf(msg) 88 | } 89 | } 90 | 91 | export const normalizeToBuf = (msg: Msg, strConv: (str: string) => ArrayBuffer): ArrayBuffer => { 92 | if (typeof msg === 'string') { 93 | return strConv(msg) 94 | } else if (typeof msg === 'object' && msg.byteLength !== undefined) { 95 | // this is the best runtime check I could find for ArrayBuffer/Uint8Array 96 | const temp = new Uint8Array(msg) 97 | return temp.buffer 98 | } else { 99 | throw new Error("Improper value. Must be a string, ArrayBuffer, Uint8Array") 100 | } 101 | } 102 | 103 | /* istanbul ignore next */ 104 | export async function structuralClone(obj: any) { 105 | return new Promise(resolve => { 106 | const { port1, port2 } = new MessageChannel() 107 | port2.onmessage = ev => resolve(ev.data) 108 | port1.postMessage(obj) 109 | }) 110 | } 111 | 112 | export default { 113 | arrBufToStr, 114 | arrBufToBase64, 115 | strToArrBuf, 116 | base64ToArrBuf, 117 | publicExponent, 118 | randomBuf, 119 | joinBufs, 120 | normalizeUtf8ToBuf, 121 | normalizeUtf16ToBuf, 122 | normalizeBase64ToBuf, 123 | normalizeToBuf, 124 | structuralClone 125 | } 126 | -------------------------------------------------------------------------------- /test/aes.test.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'one-webcrypto' 2 | import aes from '../src/aes' 3 | import utils from '../src/utils' 4 | import { SymmAlg, SymmKeyLength } from '../src/types' 5 | import { mock, cryptoMethod, arrBufEq } from './utils' 6 | 7 | describe('aes', () => { 8 | 9 | cryptoMethod({ 10 | desc: 'makeKey', 11 | setMock: fake => webcrypto.subtle.generateKey = fake, 12 | mockResp: mock.symmKey, 13 | simpleReq: () => aes.makeKey(), 14 | simpleParams: [ 15 | { name: 'AES-CTR', length: 256 }, 16 | true, 17 | [ 'encrypt', 'decrypt'] 18 | ], 19 | paramChecks: [ 20 | { 21 | desc: 'handles multiple key algorithms', 22 | req: () => aes.makeKey({ alg: SymmAlg.AES_CBC }), 23 | params: (params: any) => params[0]?.name === 'AES-CBC' 24 | }, 25 | { 26 | desc: 'handles multiple key algorithms', 27 | req: () => aes.makeKey({ alg: SymmAlg.AES_GCM }), 28 | params: (params: any) => params[0]?.name === 'AES-GCM' 29 | }, 30 | { 31 | desc: 'handles multiple key lengths', 32 | req: () => aes.makeKey({ length: SymmKeyLength.B256 }), 33 | params: (params: any) => params[0]?.length === 256 34 | } 35 | ], 36 | shouldThrows: [ ] 37 | }) 38 | 39 | 40 | cryptoMethod({ 41 | desc: 'importKey', 42 | setMock: fake => webcrypto.subtle.importKey = fake, 43 | mockResp: mock.symmKey, 44 | simpleReq: () => aes.importKey(mock.keyBase64), 45 | simpleParams: [ 46 | 'raw', 47 | utils.base64ToArrBuf(mock.keyBase64), 48 | { name: 'AES-CTR', length: 256 }, 49 | true, 50 | [ 'encrypt', 'decrypt'] 51 | ], 52 | paramChecks: [ 53 | { 54 | desc: 'handles multiple key algorithms', 55 | req: () => aes.importKey(mock.keyBase64, { alg: SymmAlg.AES_CBC }), 56 | params: (params: any) => params[2]?.name === 'AES-CBC' 57 | }, 58 | { 59 | desc: 'handles multiple key lengths', 60 | req: () => aes.importKey(mock.keyBase64, { length: SymmKeyLength.B256 }), 61 | params: (params: any) => params[2]?.length === 256 62 | } 63 | ], 64 | shouldThrows: [] 65 | }) 66 | 67 | 68 | cryptoMethod({ 69 | desc: 'encrypt', 70 | setMock: fake => { 71 | webcrypto.subtle.encrypt = fake 72 | webcrypto.subtle.importKey = jest.fn(() => new Promise(r => r(mock.symmKey))) 73 | webcrypto.getRandomValues = jest.fn(() => new Promise(r => r(new Uint8Array(16)))) as any 74 | }, 75 | mockResp: mock.cipherBytes, 76 | expectedResp: mock.cipherWithIVStr, 77 | simpleReq: () => aes.encrypt(mock.msgStr, mock.keyBase64, { iv: mock.iv }), 78 | paramChecks: [ 79 | { 80 | desc: 'correctly passes params with AES-CTR', 81 | req: () => aes.encrypt(mock.msgStr, mock.keyBase64, { iv: mock.iv }), 82 | params: (params: any) => ( 83 | params[0]?.name === 'AES-CTR' 84 | && params[0]?.length === 64 85 | && arrBufEq(params[0]?.counter, mock.iv) 86 | && params[1] === mock.symmKey 87 | && arrBufEq(params[2], mock.msgBytes) 88 | ) 89 | }, 90 | { 91 | desc: 'correctly passes params with AES-CBC', 92 | req: () => aes.encrypt(mock.msgStr, mock.keyBase64, { alg: SymmAlg.AES_CBC, iv: mock.iv }), 93 | params: (params: any) => ( 94 | params[0]?.name === 'AES-CBC' 95 | && arrBufEq(params[0]?.iv, mock.iv) 96 | && params[1] === mock.symmKey 97 | && arrBufEq(params[2], mock.msgBytes) 98 | ) 99 | } 100 | ], 101 | shouldThrows: [] 102 | }) 103 | 104 | 105 | cryptoMethod({ 106 | desc: 'decrypt', 107 | setMock: fake => { 108 | webcrypto.subtle.decrypt = fake 109 | webcrypto.subtle.importKey = jest.fn(() => new Promise(r => r(mock.symmKey))) 110 | }, 111 | mockResp: mock.msgBytes, 112 | expectedResp: mock.msgStr, 113 | simpleReq: () => aes.decrypt(mock.cipherWithIVStr, mock.keyBase64), 114 | paramChecks: [ 115 | { 116 | desc: 'correctly passes params with AES-CTR', 117 | req: () => aes.decrypt(mock.cipherWithIVStr, mock.keyBase64), 118 | params: (params: any) => ( 119 | params[0].name === 'AES-CTR' 120 | && params[0].length === 64 121 | && arrBufEq(params[0].counter.buffer, mock.iv) 122 | && params[1] === mock.symmKey 123 | && arrBufEq(params[2], mock.cipherBytes) 124 | ) 125 | }, 126 | { 127 | desc: 'correctly passes params with AES-CBC', 128 | req: () => aes.decrypt(mock.cipherWithIVStr, mock.keyBase64, { alg: SymmAlg.AES_CBC }), 129 | params: (params: any) => ( 130 | params[0]?.name === 'AES-CBC' 131 | && arrBufEq(params[0].iv, mock.iv) 132 | && arrBufEq(params[2], mock.cipherBytes) 133 | ) 134 | } 135 | ], 136 | shouldThrows: [] 137 | }) 138 | 139 | 140 | cryptoMethod({ 141 | desc: 'exportKey', 142 | setMock: fake => webcrypto.subtle.exportKey = fake, 143 | mockResp: utils.base64ToArrBuf(mock.keyBase64), 144 | expectedResp: mock.keyBase64, 145 | simpleReq: () => aes.exportKey(mock.symmKey), 146 | simpleParams: [ 147 | 'raw', 148 | mock.symmKey 149 | ], 150 | paramChecks: [], 151 | shouldThrows: [] 152 | }) 153 | 154 | 155 | }) 156 | -------------------------------------------------------------------------------- /test/rsa.keystore.test.ts: -------------------------------------------------------------------------------- 1 | import RSAKeyStore from '../src/rsa/keystore' 2 | import keys from '../src/rsa/keys' 3 | import operations from '../src/rsa/operations' 4 | import config, { defaultConfig } from '../src/config' 5 | import idb from '../src/idb' 6 | import { DEFAULT_CHAR_SIZE, DEFAULT_HASH_ALG } from '../src/constants' 7 | import { KeyUse, RsaSize, HashAlg, CryptoSystem } from '../src/types' 8 | import { mock, keystoreMethod } from './utils' 9 | 10 | jest.mock('../src/idb') 11 | 12 | describe("RSAKeyStore", () => { 13 | describe("init", () => { 14 | 15 | let response: any 16 | let fakeStore: jest.SpyInstance 17 | let fakeMake: jest.SpyInstance 18 | let fakeCreateifDNE: jest.SpyInstance 19 | 20 | beforeAll(async () => { 21 | fakeStore = jest.spyOn(idb, 'createStore') 22 | fakeStore.mockReturnValue(mock.idbStore) 23 | 24 | fakeMake = jest.spyOn(keys, 'makeKeypair') 25 | fakeMake.mockResolvedValue(mock.keys) 26 | 27 | fakeCreateifDNE = jest.spyOn(idb, 'createIfDoesNotExist') 28 | fakeCreateifDNE.mockImplementation((_name, makeFn) => { 29 | makeFn() 30 | }) 31 | 32 | response = await RSAKeyStore.init({ exchangeKeyName: 'test-exchange', writeKeyName: 'test-write' }) 33 | }) 34 | 35 | it('should initialize a keystore with expected params', () => { 36 | let cfg = config.normalize({ 37 | type: CryptoSystem.RSA, 38 | exchangeKeyName: 'test-exchange', 39 | writeKeyName: 'test-write' 40 | }) 41 | const keystore = new RSAKeyStore(cfg, mock.idbStore) 42 | expect(response).toStrictEqual(keystore) 43 | }) 44 | 45 | it('should call createIfDoesNotExist with correct params (exchange key)', () => { 46 | expect(fakeCreateifDNE.mock.calls[0][0]).toEqual('test-exchange') 47 | expect(fakeCreateifDNE.mock.calls[0][2]).toEqual(mock.idbStore) 48 | }) 49 | 50 | it('should call createIfDoesNotExist with correct params (write key)', () => { 51 | expect(fakeCreateifDNE.mock.calls[1][0]).toEqual('test-write') 52 | expect(fakeCreateifDNE.mock.calls[1][2]).toEqual(mock.idbStore) 53 | }) 54 | 55 | it('should call makeKeypair with correct params (exchange key)', () => { 56 | expect(fakeMake.mock.calls[0]).toEqual([ 57 | RsaSize.B2048, 58 | HashAlg.SHA_256, 59 | KeyUse.Exchange 60 | ]) 61 | }) 62 | 63 | it('should call makeKeypair with correct params (write key)', () => { 64 | expect(fakeMake.mock.calls[1]).toEqual([ 65 | RsaSize.B2048, 66 | HashAlg.SHA_256, 67 | KeyUse.Write 68 | ]) 69 | }) 70 | 71 | }) 72 | 73 | 74 | keystoreMethod({ 75 | desc: 'sign', 76 | type: 'rsa', 77 | mocks: [ 78 | { 79 | mod: operations, 80 | meth: 'sign', 81 | resp: mock.sigBytes, 82 | params: [ 83 | mock.msgStr, 84 | mock.writeKeys.privateKey, 85 | DEFAULT_CHAR_SIZE 86 | ] 87 | } 88 | ], 89 | reqFn: (ks) => ks.sign(mock.msgStr), 90 | expectedResp: mock.sigStr, 91 | }) 92 | 93 | 94 | keystoreMethod({ 95 | desc: 'verify', 96 | type: 'rsa', 97 | mocks: [ 98 | { 99 | mod: operations, 100 | meth: 'verify', 101 | resp: true, 102 | params: [ 103 | mock.msgStr, 104 | mock.sigStr, 105 | mock.keyBase64, 106 | DEFAULT_CHAR_SIZE, 107 | DEFAULT_HASH_ALG 108 | ] 109 | } 110 | ], 111 | reqFn: (ks) => ks.verify(mock.msgStr, mock.sigStr, mock.keyBase64), 112 | expectedResp: true, 113 | }) 114 | 115 | 116 | keystoreMethod({ 117 | desc: 'encrypt', 118 | type: 'rsa', 119 | mocks: [ 120 | { 121 | mod: operations, 122 | meth: 'encrypt', 123 | resp: mock.cipherBytes, 124 | params: [ 125 | mock.msgStr, 126 | mock.keyBase64, 127 | DEFAULT_CHAR_SIZE, 128 | DEFAULT_HASH_ALG 129 | ] 130 | } 131 | ], 132 | reqFn: (ks) => ks.encrypt(mock.msgStr, mock.keyBase64), 133 | expectedResp: mock.cipherStr, 134 | }) 135 | 136 | 137 | keystoreMethod({ 138 | desc: 'decrypt', 139 | type: 'rsa', 140 | mocks: [ 141 | { 142 | mod: operations, 143 | meth: 'decrypt', 144 | resp: mock.msgBytes, 145 | params: [ 146 | mock.cipherStr, 147 | mock.keys.privateKey 148 | ] 149 | }, 150 | ], 151 | reqFn: (ks) => ks.decrypt(mock.cipherStr, mock.keyBase64), 152 | expectedResp: mock.msgStr, 153 | }) 154 | 155 | 156 | keystoreMethod({ 157 | desc: 'publicExchangeKey', 158 | type: 'rsa', 159 | mocks: [ 160 | { 161 | mod: operations, 162 | meth: 'getPublicKey', 163 | resp: mock.keyBase64, 164 | params: [ 165 | mock.keys 166 | ] 167 | } 168 | ], 169 | reqFn: (ks) => ks.publicExchangeKey(), 170 | expectedResp: mock.keyBase64, 171 | }) 172 | 173 | 174 | keystoreMethod({ 175 | desc: 'publicWriteKey', 176 | type: 'rsa', 177 | mocks: [ 178 | { 179 | mod: operations, 180 | meth: 'getPublicKey', 181 | resp: mock.keyBase64, 182 | params: [ 183 | mock.writeKeys 184 | ] 185 | } 186 | ], 187 | reqFn: (ks) => ks.publicWriteKey(), 188 | expectedResp: mock.keyBase64, 189 | }) 190 | 191 | }) 192 | -------------------------------------------------------------------------------- /test/base.keystore.test.ts: -------------------------------------------------------------------------------- 1 | import aes from '../src/aes' 2 | import idb from '../src/idb' 3 | import config from '../src/config' 4 | import { mock, keystoreMethod } from './utils' 5 | 6 | const defaultOpts = { alg: config.defaultConfig.symmAlg, length: config.defaultConfig.symmLen } 7 | 8 | describe("KeyStoreBase", () => { 9 | 10 | keystoreMethod({ 11 | desc: 'keyExists', 12 | type: 'rsa', 13 | mocks: [ 14 | { 15 | mod: idb, 16 | meth: 'getKey', 17 | resp: null, 18 | params: [ 19 | mock.symmKeyName, 20 | mock.idbStore 21 | ] 22 | }, 23 | ], 24 | reqFn: (ks) => ks.keyExists(mock.symmKeyName), 25 | expectedResp: false, 26 | }) 27 | 28 | 29 | keystoreMethod({ 30 | desc: 'getSymmKey (exists)', 31 | type: 'rsa', 32 | mocks: [ 33 | { 34 | mod: idb, 35 | meth: 'getKey', 36 | resp: mock.symmKey, 37 | params: [ 38 | mock.symmKeyName, 39 | mock.idbStore 40 | ] 41 | }, 42 | ], 43 | reqFn: (ks) => ks.getSymmKey(mock.symmKeyName), 44 | expectedResp: mock.symmKey, 45 | }) 46 | 47 | keystoreMethod({ 48 | desc: 'getSymmKey (does not exist)', 49 | type: 'rsa', 50 | mocks: [ 51 | { 52 | mod: idb, 53 | meth: 'getKey', 54 | resp: null, 55 | params: [ 56 | mock.symmKeyName, 57 | mock.idbStore 58 | ] 59 | }, 60 | { 61 | mod: aes, 62 | meth: 'makeKey', 63 | resp: mock.symmKey, 64 | params: [ 65 | config.symmKeyOpts(config.defaultConfig) 66 | ] 67 | }, 68 | { 69 | mod: idb, 70 | meth: 'put', 71 | resp: null, 72 | params: [ 73 | mock.symmKeyName, 74 | mock.symmKey, 75 | mock.idbStore 76 | ] 77 | }, 78 | 79 | ], 80 | reqFn: (ks) => ks.getSymmKey(mock.symmKeyName), 81 | expectedResp: mock.symmKey, 82 | }) 83 | 84 | 85 | keystoreMethod({ 86 | desc: 'importSymmKey', 87 | type: 'rsa', 88 | mocks: [ 89 | { 90 | mod: aes, 91 | meth: 'importKey', 92 | resp: mock.symmKey, 93 | params: [ 94 | mock.keyBase64, 95 | defaultOpts 96 | ] 97 | }, 98 | { 99 | mod: idb, 100 | meth: 'put', 101 | resp: undefined, 102 | params: [ 103 | mock.symmKeyName, 104 | mock.symmKey, 105 | mock.idbStore 106 | ] 107 | } 108 | ], 109 | reqFn: (ks) => ks.importSymmKey(mock.keyBase64, mock.symmKeyName), 110 | }) 111 | 112 | 113 | keystoreMethod({ 114 | desc: 'exportSymmKey', 115 | type: 'rsa', 116 | mocks: [ 117 | { 118 | mod: idb, 119 | meth: 'getKey', 120 | resp: mock.symmKey, 121 | params: [ 122 | mock.symmKeyName, 123 | mock.idbStore 124 | ] 125 | }, 126 | { 127 | mod: aes, 128 | meth: 'exportKey', 129 | resp: mock.keyBase64, 130 | params: [ 131 | mock.symmKey 132 | ] 133 | } 134 | ], 135 | reqFn: (ks) => ks.exportSymmKey(mock.symmKeyName), 136 | expectedResp: mock.keyBase64 137 | }) 138 | 139 | 140 | keystoreMethod({ 141 | desc: 'encryptWithSymmKey', 142 | type: 'rsa', 143 | mocks: [ 144 | { 145 | mod: idb, 146 | meth: 'getKey', 147 | resp: mock.symmKey, 148 | params: [ 149 | mock.symmKeyName, 150 | mock.idbStore 151 | ] 152 | }, 153 | { 154 | mod: aes, 155 | meth: 'encryptBytes', 156 | resp: mock.cipherBytes, 157 | params: [ 158 | mock.msgBytes, 159 | mock.symmKey, 160 | defaultOpts 161 | ] 162 | } 163 | ], 164 | reqFn: (ks) => ks.encryptWithSymmKey(mock.msgStr, mock.symmKeyName), 165 | expectedResp: mock.cipherStr 166 | }) 167 | 168 | 169 | keystoreMethod({ 170 | desc: 'decryptWithSymmKey', 171 | type: 'rsa', 172 | mocks: [ 173 | { 174 | mod: idb, 175 | meth: 'getKey', 176 | resp: mock.symmKey, 177 | params: [ 178 | mock.symmKeyName, 179 | mock.idbStore 180 | ] 181 | }, 182 | { 183 | mod: aes, 184 | meth: 'decryptBytes', 185 | resp: mock.msgBytes, 186 | params: [ 187 | mock.cipherBytes, 188 | mock.symmKey, 189 | defaultOpts 190 | ] 191 | } 192 | ], 193 | reqFn: (ks) => ks.decryptWithSymmKey(mock.cipherStr, mock.symmKeyName), 194 | expectedResp: mock.msgStr 195 | }) 196 | 197 | 198 | keystoreMethod({ 199 | desc: 'deleteKey', 200 | type: 'rsa', 201 | mocks: [ 202 | { 203 | mod: idb, 204 | meth: 'rm', 205 | resp: undefined, 206 | params: [ 207 | mock.symmKeyName, 208 | mock.idbStore 209 | ] 210 | } 211 | ], 212 | reqFn: (ks) => ks.deleteKey(mock.symmKeyName), 213 | expectedResp: undefined 214 | }) 215 | 216 | keystoreMethod({ 217 | desc: 'destory', 218 | type: 'rsa', 219 | mocks: [ 220 | { 221 | mod: idb, 222 | meth: 'dropStore', 223 | resp: undefined, 224 | params: [ 225 | mock.idbStore 226 | ] 227 | } 228 | ], 229 | reqFn: (ks) => ks.destroy(), 230 | expectedResp: undefined 231 | }) 232 | 233 | }) 234 | -------------------------------------------------------------------------------- /test/ecc.keystore.test.ts: -------------------------------------------------------------------------------- 1 | import ECCKeyStore from '../src/ecc/keystore' 2 | import keys from '../src/ecc/keys' 3 | import operations from '../src/ecc/operations' 4 | import config, { defaultConfig } from '../src/config' 5 | import idb from '../src/idb' 6 | import { DEFAULT_CHAR_SIZE, DEFAULT_ECC_CURVE, DEFAULT_HASH_ALG } from '../src/constants' 7 | import { EccCurve, KeyUse, CryptoSystem } from '../src/types' 8 | import { mock, keystoreMethod } from './utils' 9 | 10 | jest.mock('../src/idb') 11 | 12 | describe("ECCKeyStore", () => { 13 | describe("init", () => { 14 | 15 | let response: any 16 | let fakeStore: jest.SpyInstance 17 | let fakeMake: jest.SpyInstance 18 | let fakeCreateifDNE: jest.SpyInstance 19 | 20 | beforeAll(async () => { 21 | fakeStore = jest.spyOn(idb, 'createStore') 22 | fakeStore.mockReturnValue(mock.idbStore) 23 | 24 | fakeMake = jest.spyOn(keys, 'makeKeypair') 25 | fakeMake.mockResolvedValue(mock.keys) 26 | 27 | fakeCreateifDNE = jest.spyOn(idb, 'createIfDoesNotExist') 28 | fakeCreateifDNE.mockImplementation((_name, makeFn) => { 29 | makeFn() 30 | }) 31 | 32 | response = await ECCKeyStore.init({ exchangeKeyName: 'test-exchange', writeKeyName: 'test-write' }) 33 | }) 34 | 35 | it('should initialize a keystore with expected params', () => { 36 | let cfg = config.normalize({ 37 | type: CryptoSystem.ECC, 38 | exchangeKeyName: 'test-exchange', 39 | writeKeyName: 'test-write' 40 | }) 41 | const keystore = new ECCKeyStore(cfg, mock.idbStore) 42 | expect(response).toStrictEqual(keystore) 43 | }) 44 | 45 | it('should call createIfDoesNotExist with correct params (exchange key)', () => { 46 | expect(fakeCreateifDNE.mock.calls[0][0]).toEqual('test-exchange') 47 | expect(fakeCreateifDNE.mock.calls[0][2]).toEqual(mock.idbStore) 48 | }) 49 | 50 | it('should call createIfDoesNotExist with correct params (write key)', () => { 51 | expect(fakeCreateifDNE.mock.calls[1][0]).toEqual('test-write') 52 | expect(fakeCreateifDNE.mock.calls[1][2]).toEqual(mock.idbStore) 53 | }) 54 | 55 | it('should call makeKeypair with correct params (exchange key)', () => { 56 | expect(fakeMake.mock.calls[0]).toEqual([ 57 | EccCurve.P_256, 58 | KeyUse.Exchange 59 | ]) 60 | }) 61 | 62 | it('should call makeKeypair with correct params (write key)', () => { 63 | expect(fakeMake.mock.calls[1]).toEqual([ 64 | EccCurve.P_256, 65 | KeyUse.Write 66 | ]) 67 | }) 68 | 69 | }) 70 | 71 | 72 | keystoreMethod({ 73 | desc: 'sign', 74 | type: 'ecc', 75 | mocks: [ 76 | { 77 | mod: operations, 78 | meth: 'sign', 79 | resp: mock.sigBytes, 80 | params: [ 81 | mock.msgStr, 82 | mock.writeKeys.privateKey, 83 | DEFAULT_CHAR_SIZE, 84 | DEFAULT_HASH_ALG 85 | ] 86 | } 87 | ], 88 | reqFn: (ks) => ks.sign(mock.msgStr), 89 | expectedResp: mock.sigStr, 90 | }) 91 | 92 | 93 | keystoreMethod({ 94 | desc: 'verify', 95 | type: 'ecc', 96 | mocks: [ 97 | { 98 | mod: operations, 99 | meth: 'verify', 100 | resp: true, 101 | params: [ 102 | mock.msgStr, 103 | mock.sigStr, 104 | mock.keyBase64, 105 | DEFAULT_CHAR_SIZE, 106 | DEFAULT_ECC_CURVE, 107 | DEFAULT_HASH_ALG 108 | ] 109 | } 110 | ], 111 | reqFn: (ks) => ks.verify(mock.msgStr, mock.sigStr, mock.keyBase64), 112 | expectedResp: true, 113 | }) 114 | 115 | 116 | keystoreMethod({ 117 | desc: 'encrypt', 118 | type: 'ecc', 119 | mocks: [ 120 | { 121 | mod: operations, 122 | meth: 'encrypt', 123 | resp: mock.cipherBytes, 124 | params: [ 125 | mock.msgStr, 126 | mock.keys.privateKey, 127 | mock.keyBase64, 128 | DEFAULT_CHAR_SIZE, 129 | DEFAULT_ECC_CURVE 130 | ] 131 | } 132 | ], 133 | reqFn: (ks) => ks.encrypt(mock.msgStr, mock.keyBase64), 134 | expectedResp: mock.cipherStr, 135 | }) 136 | 137 | 138 | keystoreMethod({ 139 | desc: 'decrypt', 140 | type: 'ecc', 141 | mocks: [ 142 | { 143 | mod: operations, 144 | meth: 'decrypt', 145 | resp: mock.msgBytes, 146 | params: [ 147 | mock.cipherStr, 148 | mock.keys.privateKey, 149 | mock.keyBase64, 150 | DEFAULT_ECC_CURVE 151 | ] 152 | } 153 | ], 154 | reqFn: (ks) => ks.decrypt(mock.cipherStr, mock.keyBase64), 155 | expectedResp: mock.msgStr, 156 | }) 157 | 158 | 159 | keystoreMethod({ 160 | desc: 'publicExchangeKey', 161 | type: 'ecc', 162 | mocks: [ 163 | { 164 | mod: operations, 165 | meth: 'getPublicKey', 166 | resp: mock.keyBase64, 167 | params: [ 168 | mock.keys 169 | ] 170 | } 171 | ], 172 | reqFn: (ks) => ks.publicExchangeKey(), 173 | expectedResp: mock.keyBase64, 174 | }) 175 | 176 | 177 | keystoreMethod({ 178 | desc: 'publicWriteKey', 179 | type: 'ecc', 180 | mocks: [ 181 | { 182 | mod: operations, 183 | meth: 'getPublicKey', 184 | resp: mock.keyBase64, 185 | params: [ 186 | mock.writeKeys 187 | ] 188 | } 189 | ], 190 | reqFn: (ks) => ks.publicWriteKey(), 191 | expectedResp: mock.keyBase64, 192 | }) 193 | 194 | }) 195 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of `FISSON` is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in `FISSION` to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open Source Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people’s personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone’s consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Consequences of Unacceptable Behavior 47 | 48 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 49 | 50 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 51 | 52 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 53 | 54 | ## 6. Reporting Guidelines 55 | 56 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. hello@brooklynzelenka.com. 57 | 58 | 59 | 60 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 61 | 62 | ## 7. Addressing Grievances 63 | 64 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify the maintainers with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 65 | 66 | 67 | 68 | ## 8. Scope 69 | 70 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business. 71 | 72 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 73 | 74 | ## 9. Contact info 75 | 76 | hello@fission.codes 77 | 78 | ## 10. License and attribution 79 | 80 | This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 81 | 82 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 83 | 84 | Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/) 85 | 86 | -------------------------------------------------------------------------------- /test/rsa.test.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'one-webcrypto' 2 | import rsa from '../src/rsa' 3 | import errors from '../src/errors' 4 | import utils from '../src/utils' 5 | import { DEFAULT_CHAR_SIZE, DEFAULT_HASH_ALG } from '../src/constants' 6 | import { KeyUse, RsaSize, HashAlg } from '../src/types' 7 | import { mock, cryptoMethod } from './utils' 8 | 9 | describe('rsa', () => { 10 | 11 | cryptoMethod({ 12 | desc: 'makeKeypair', 13 | setMock: fake => webcrypto.subtle.generateKey = fake, 14 | mockResp: mock.keys, 15 | simpleReq: () => rsa.makeKeypair(RsaSize.B2048, HashAlg.SHA_256, KeyUse.Exchange), 16 | simpleParams: [ 17 | { 18 | name: 'RSA-OAEP', 19 | modulusLength: 2048, 20 | publicExponent: utils.publicExponent(), 21 | hash: { name: 'SHA-256' } 22 | }, 23 | false, 24 | ['encrypt', 'decrypt'] 25 | ], 26 | paramChecks: [ 27 | { 28 | desc: 'handles write keys', 29 | req: () => rsa.makeKeypair(RsaSize.B2048, HashAlg.SHA_256, KeyUse.Write), 30 | params: [ 31 | { 32 | name: 'RSASSA-PKCS1-v1_5', 33 | modulusLength: 2048, 34 | publicExponent: utils.publicExponent(), 35 | hash: { name: 'SHA-256' } 36 | }, 37 | false, 38 | ['sign', 'verify'] 39 | ] 40 | }, 41 | { 42 | desc: 'handles multiple key sizes', 43 | req: () => rsa.makeKeypair(RsaSize.B4096, HashAlg.SHA_256, KeyUse.Write), 44 | params: (params: any) => params[0]?.modulusLength === 4096 45 | }, 46 | { 47 | desc: 'handles multiple hash algorithms', 48 | req: () => rsa.makeKeypair(RsaSize.B2048, HashAlg.SHA_512, KeyUse.Write), 49 | params: (params: any) => params[0]?.hash?.name === 'SHA-512' 50 | } 51 | ], 52 | shouldThrows: [ 53 | { 54 | desc: 'throws an error when passing in an invalid use', 55 | req: () => rsa.makeKeypair(RsaSize.B2048, HashAlg.SHA_256, 'sigBytes' as any), 56 | error: errors.InvalidKeyUse 57 | } 58 | ] 59 | }) 60 | 61 | 62 | cryptoMethod({ 63 | desc: 'importPublicExchangeKey', 64 | setMock: fake => webcrypto.subtle.importKey = fake, 65 | mockResp: mock.keys.publicKey, 66 | expectedResp: mock.keys.publicKey, 67 | simpleReq: () => rsa.importPublicKey(mock.keyBase64, HashAlg.SHA_256, KeyUse.Exchange), 68 | simpleParams: [ 69 | 'spki', 70 | utils.base64ToArrBuf(mock.keyBase64), 71 | { name: 'RSA-OAEP', hash: {name: 'SHA-256'}}, 72 | true, 73 | ['encrypt'] 74 | ], 75 | paramChecks: [ 76 | { 77 | desc: 'handles multiple hash algs', 78 | req: () => rsa.importPublicKey(mock.keyBase64, HashAlg.SHA_512, KeyUse.Exchange), 79 | params: (params: any) => params[2]?.hash?.name === 'SHA-512' 80 | }, 81 | { 82 | desc: 'handles write keys', 83 | req: () => rsa.importPublicKey(mock.keyBase64, HashAlg.SHA_256, KeyUse.Write), 84 | params: [ 85 | 'spki', 86 | utils.base64ToArrBuf(mock.keyBase64), 87 | { name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'}}, 88 | true, 89 | ['verify'] 90 | ] 91 | } 92 | ], 93 | shouldThrows: [] 94 | }) 95 | 96 | 97 | cryptoMethod({ 98 | desc: 'sign', 99 | setMock: fake => webcrypto.subtle.sign = fake, 100 | mockResp: mock.sigBytes, 101 | simpleReq: () => rsa.sign( 102 | mock.msgBytes, 103 | mock.keys.privateKey 104 | ), 105 | simpleParams: [ 106 | { name: 'RSASSA-PKCS1-v1_5', saltLength: 128 }, 107 | mock.keys.privateKey, 108 | mock.msgBytes 109 | ], 110 | paramChecks: [], 111 | shouldThrows: [] 112 | }) 113 | 114 | 115 | cryptoMethod({ 116 | desc: 'sign', 117 | setMock: fake => webcrypto.subtle.sign = fake, 118 | mockResp: mock.sigBytes, 119 | simpleReq: () => rsa.sign( 120 | mock.msgStr, 121 | mock.keys.privateKey, 122 | DEFAULT_CHAR_SIZE 123 | ), 124 | simpleParams: [ 125 | { name: 'RSASSA-PKCS1-v1_5', saltLength: 128 }, 126 | mock.keys.privateKey, 127 | mock.msgBytes 128 | ], 129 | paramChecks: [], 130 | shouldThrows: [] 131 | }) 132 | 133 | 134 | cryptoMethod({ 135 | desc: 'verify', 136 | setMock: fake => webcrypto.subtle.verify = fake, 137 | mockResp: true, 138 | simpleReq: () => rsa.verify( 139 | mock.msgBytes, 140 | mock.sigBytes, 141 | mock.keys.publicKey 142 | ), 143 | simpleParams: [ 144 | { name: 'RSASSA-PKCS1-v1_5', saltLength: 128 }, 145 | mock.keys.publicKey, 146 | mock.sigBytes, 147 | mock.msgBytes 148 | ], 149 | paramChecks: [], 150 | shouldThrows: [] 151 | }) 152 | 153 | 154 | cryptoMethod({ 155 | desc: 'verify', 156 | setMock: fake => webcrypto.subtle.verify = fake, 157 | mockResp: true, 158 | simpleReq: () => rsa.verify( 159 | mock.msgStr, 160 | mock.sigStr, 161 | mock.keyBase64, 162 | DEFAULT_CHAR_SIZE, 163 | DEFAULT_HASH_ALG 164 | ), 165 | simpleParams: [ 166 | { name: 'RSASSA-PKCS1-v1_5', saltLength: 128 }, 167 | mock.keys.publicKey, 168 | mock.sigBytes, 169 | mock.msgBytes 170 | ], 171 | paramChecks: [], 172 | shouldThrows: [] 173 | }) 174 | 175 | 176 | cryptoMethod({ 177 | desc: 'encrypt', 178 | setMock: fake => webcrypto.subtle.encrypt = fake, 179 | mockResp: mock.cipherBytes, 180 | simpleReq: () => rsa.encrypt( 181 | mock.msgBytes, 182 | mock.keys.publicKey 183 | ), 184 | simpleParams: [ 185 | { name: 'RSA-OAEP' }, 186 | mock.keys.publicKey, 187 | mock.msgBytes 188 | ], 189 | paramChecks: [], 190 | shouldThrows: [] 191 | }) 192 | 193 | 194 | cryptoMethod({ 195 | desc: 'encrypt', 196 | setMock: fake => webcrypto.subtle.encrypt = fake, 197 | mockResp: mock.cipherBytes, 198 | simpleReq: () => rsa.encrypt( 199 | mock.msgStr, 200 | mock.keyBase64, 201 | DEFAULT_CHAR_SIZE, 202 | DEFAULT_HASH_ALG 203 | ), 204 | simpleParams: [ 205 | { name: 'RSA-OAEP' }, 206 | mock.keys.publicKey, 207 | mock.msgBytes 208 | ], 209 | paramChecks: [], 210 | shouldThrows: [] 211 | }) 212 | 213 | 214 | cryptoMethod({ 215 | desc: 'decrypt', 216 | setMock: fake => webcrypto.subtle.decrypt = fake, 217 | mockResp: mock.msgBytes, 218 | simpleReq: () => rsa.decrypt( 219 | mock.cipherBytes, 220 | mock.keys.privateKey 221 | ), 222 | simpleParams: [ 223 | { name: 'RSA-OAEP' }, 224 | mock.keys.privateKey, 225 | mock.cipherBytes 226 | ], 227 | paramChecks: [], 228 | shouldThrows: [] 229 | }) 230 | 231 | 232 | cryptoMethod({ 233 | desc: 'decrypt', 234 | setMock: fake => webcrypto.subtle.decrypt = fake, 235 | mockResp: mock.msgBytes, 236 | simpleReq: () => rsa.decrypt( 237 | mock.cipherStr, 238 | mock.keys.privateKey, 239 | ), 240 | simpleParams: [ 241 | { name: 'RSA-OAEP' }, 242 | mock.keys.privateKey, 243 | mock.cipherBytes 244 | ], 245 | paramChecks: [], 246 | shouldThrows: [] 247 | }) 248 | 249 | 250 | cryptoMethod({ 251 | desc: 'getPublicKey', 252 | setMock: fake => webcrypto.subtle.exportKey = fake, 253 | mockResp: utils.base64ToArrBuf(mock.keyBase64), 254 | expectedResp: mock.keyBase64, 255 | simpleReq: () => rsa.getPublicKey(mock.keys), 256 | simpleParams: [ 257 | 'spki', 258 | mock.keys.publicKey 259 | ], 260 | paramChecks: [], 261 | shouldThrows: [] 262 | }) 263 | 264 | }) 265 | -------------------------------------------------------------------------------- /test/ecc.test.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'one-webcrypto' 2 | import ecc from '../src/ecc' 3 | import errors from '../src/errors' 4 | import utils from '../src/utils' 5 | import { DEFAULT_CHAR_SIZE, DEFAULT_ECC_CURVE } from '../src/constants' 6 | import { KeyUse, EccCurve, HashAlg, SymmAlg, SymmKeyLength } from '../src/types' 7 | import { mock, cryptoMethod, arrBufEq } from './utils' 8 | 9 | describe('ecc', () => { 10 | 11 | cryptoMethod({ 12 | desc: 'makeKeypair', 13 | setMock: fake => webcrypto.subtle.generateKey = fake, 14 | mockResp: mock.keys, 15 | simpleReq: () => ecc.makeKeypair(EccCurve.P_256, KeyUse.Exchange), 16 | simpleParams: [ 17 | { name: 'ECDH', namedCurve: 'P-256' }, 18 | false, 19 | [ 'deriveKey', 'deriveBits'] 20 | ], 21 | paramChecks: [ 22 | { 23 | desc: 'handles multiple key algorithms', 24 | req: () => ecc.makeKeypair(EccCurve.P_521, KeyUse.Exchange), 25 | params: (params: any) => params[0]?.namedCurve === 'P-521' 26 | }, 27 | { 28 | desc: 'handles write keys', 29 | req: () => ecc.makeKeypair(EccCurve.P_256, KeyUse.Write), 30 | params: [ 31 | { name: 'ECDSA', namedCurve: 'P-256' }, 32 | false, 33 | ['sign', 'verify'] 34 | ] 35 | } 36 | ], 37 | shouldThrows: [ 38 | { 39 | desc: 'throws an error when passing in an invalid use', 40 | req: () => ecc.makeKeypair(EccCurve.P_256, 'sigBytes' as any), 41 | error: errors.InvalidKeyUse 42 | } 43 | ] 44 | }) 45 | 46 | 47 | cryptoMethod({ 48 | desc: 'importPublicExchangeKey', 49 | setMock: fake => webcrypto.subtle.importKey = fake, 50 | mockResp: mock.keys.publicKey, 51 | expectedResp: mock.keys.publicKey, 52 | simpleReq: () => ecc.importPublicKey(mock.keyBase64, EccCurve.P_256, KeyUse.Exchange), 53 | simpleParams: [ 54 | 'raw', 55 | utils.base64ToArrBuf(mock.keyBase64), 56 | { name: 'ECDH', namedCurve: 'P-256' }, 57 | true, 58 | [] 59 | ], 60 | paramChecks: [ 61 | { 62 | desc: 'handles multiple curves', 63 | req: () => ecc.importPublicKey(mock.keyBase64, EccCurve.P_521, KeyUse.Exchange), 64 | params: (params: any) => params[2]?.namedCurve === 'P-521' 65 | }, 66 | { 67 | desc: 'handles write keys', 68 | req: () => ecc.importPublicKey(mock.keyBase64, EccCurve.P_256, KeyUse.Write), 69 | params: [ 70 | 'raw', 71 | utils.base64ToArrBuf(mock.keyBase64), 72 | { name: 'ECDSA', namedCurve: 'P-256' }, 73 | true, 74 | ['verify'] 75 | ] 76 | } 77 | ], 78 | shouldThrows: [] 79 | }) 80 | 81 | 82 | cryptoMethod({ 83 | desc: 'sign', 84 | setMock: fake => webcrypto.subtle.sign = fake, 85 | mockResp: mock.sigBytes, 86 | simpleReq: () => ecc.sign( 87 | mock.msgBytes, 88 | mock.keys.privateKey 89 | ), 90 | simpleParams: [ 91 | { name: 'ECDSA', hash: { name: 'SHA-256' }}, 92 | mock.keys.privateKey, 93 | mock.msgBytes 94 | ], 95 | paramChecks: [ 96 | { 97 | desc: 'handles multiple hash algorithms', 98 | req: () => ecc.sign( 99 | mock.msgBytes, 100 | mock.keys.privateKey, 101 | DEFAULT_CHAR_SIZE, 102 | HashAlg.SHA_512 103 | ), 104 | params: (params: any) => params[0]?.hash?.name === 'SHA-512' 105 | }, 106 | ], 107 | shouldThrows: [] 108 | }) 109 | 110 | 111 | cryptoMethod({ 112 | desc: 'sign', 113 | setMock: fake => webcrypto.subtle.sign = fake, 114 | mockResp: mock.sigBytes, 115 | simpleReq: () => ecc.sign( 116 | mock.msgStr, 117 | mock.keys.privateKey, 118 | DEFAULT_CHAR_SIZE, 119 | HashAlg.SHA_256 120 | ), 121 | simpleParams: [ 122 | { name: 'ECDSA', hash: { name: 'SHA-256' }}, 123 | mock.keys.privateKey, 124 | mock.msgBytes 125 | ], 126 | paramChecks: [], 127 | shouldThrows: [] 128 | }) 129 | 130 | 131 | cryptoMethod({ 132 | desc: 'verify', 133 | setMock: fake => webcrypto.subtle.verify = fake, 134 | mockResp: true, 135 | simpleReq: () => ecc.verify( 136 | mock.msgBytes, 137 | mock.sigBytes, 138 | mock.keys.publicKey 139 | ), 140 | simpleParams: [ 141 | { name: 'ECDSA', hash: { name: 'SHA-256' }}, 142 | mock.keys.publicKey, 143 | mock.sigBytes, 144 | mock.msgBytes 145 | ], 146 | paramChecks: [ 147 | { 148 | desc: 'handles multiple hash algorithms', 149 | req: () => ecc.verify( 150 | mock.msgBytes, 151 | mock.sigBytes, 152 | mock.keys.publicKey, 153 | DEFAULT_CHAR_SIZE, 154 | DEFAULT_ECC_CURVE, 155 | HashAlg.SHA_512 156 | ), 157 | params: (params: any) => params[0]?.hash?.name === 'SHA-512' 158 | } 159 | ], 160 | shouldThrows: [] 161 | }) 162 | 163 | 164 | cryptoMethod({ 165 | desc: 'verify', 166 | setMock: fake => webcrypto.subtle.verify = fake, 167 | mockResp: true, 168 | simpleReq: () => ecc.verify( 169 | mock.msgStr, 170 | mock.sigStr, 171 | mock.keyBase64, 172 | DEFAULT_CHAR_SIZE, 173 | DEFAULT_ECC_CURVE, 174 | HashAlg.SHA_256 175 | ), 176 | simpleParams: [ 177 | { name: 'ECDSA', hash: { name: 'SHA-256' }}, 178 | mock.keys.publicKey, 179 | mock.sigBytes, 180 | mock.msgBytes 181 | ], 182 | paramChecks: [], 183 | shouldThrows: [] 184 | }) 185 | 186 | 187 | cryptoMethod({ 188 | desc: 'encrypt', 189 | setMock: fake => { 190 | webcrypto.subtle.encrypt = fake 191 | webcrypto.subtle.deriveKey = jest.fn(() => new Promise(r => r(mock.symmKey))) 192 | webcrypto.getRandomValues = jest.fn(() => new Promise(r => r(new Uint8Array(16)))) as any 193 | }, 194 | mockResp: mock.cipherBytes, 195 | simpleReq: () => ecc.encrypt( 196 | mock.msgBytes, 197 | mock.keys.privateKey, 198 | mock.keys.publicKey 199 | ), 200 | simpleParams: [ 201 | { name: 'AES-CTR', 202 | counter: new Uint8Array(16), 203 | length: 64 204 | }, 205 | mock.symmKey, 206 | mock.msgBytes 207 | ], 208 | paramChecks: [ 209 | { 210 | desc: 'handles multiple symm key algorithms', 211 | req: () => ecc.encrypt( 212 | mock.msgBytes, 213 | mock.keys.privateKey, 214 | mock.keys.publicKey, 215 | DEFAULT_CHAR_SIZE, 216 | DEFAULT_ECC_CURVE, 217 | { alg: SymmAlg.AES_CBC } 218 | ), 219 | params: (params: any) => params[0]?.name === 'AES-CBC' 220 | }, 221 | { 222 | desc: 'handles an IV with AES-CTR', 223 | req: () => ecc.encrypt( 224 | mock.msgBytes, 225 | mock.keys.privateKey, 226 | mock.keys.publicKey, 227 | DEFAULT_CHAR_SIZE, 228 | DEFAULT_ECC_CURVE, 229 | { iv: mock.iv } 230 | ), 231 | params: (params: any) => arrBufEq(params[0]?.counter, mock.iv) 232 | }, 233 | { 234 | desc: 'handles an IV with AES-CBC', 235 | req: () => ecc.encrypt( 236 | mock.msgBytes, 237 | mock.keys.privateKey, 238 | mock.keys.publicKey, 239 | DEFAULT_CHAR_SIZE, 240 | DEFAULT_ECC_CURVE, 241 | { alg: SymmAlg.AES_CBC, iv: mock.iv } 242 | ), 243 | params: (params: any) => params[0]?.iv === mock.iv 244 | } 245 | ], 246 | shouldThrows: [] 247 | }) 248 | 249 | 250 | cryptoMethod({ 251 | desc: 'encrypt', 252 | setMock: fake => { 253 | webcrypto.subtle.encrypt = fake 254 | webcrypto.subtle.deriveKey = jest.fn(() => new Promise(r => r(mock.symmKey))) 255 | webcrypto.getRandomValues = jest.fn(() => new Promise(r => r(new Uint8Array(16)))) as any 256 | }, 257 | mockResp: mock.cipherBytes, 258 | simpleReq: () => ecc.encrypt( 259 | mock.msgStr, 260 | mock.keys.privateKey, 261 | mock.keyBase64, 262 | DEFAULT_CHAR_SIZE, 263 | DEFAULT_ECC_CURVE 264 | ), 265 | simpleParams: [ 266 | { name: 'AES-CTR', 267 | counter: new Uint8Array(16), 268 | length: 64 269 | }, 270 | mock.symmKey, 271 | mock.msgBytes 272 | ], 273 | paramChecks: [], 274 | shouldThrows: [] 275 | }) 276 | 277 | 278 | cryptoMethod({ 279 | desc: 'decrypt', 280 | setMock: fake => { 281 | webcrypto.subtle.decrypt = fake 282 | webcrypto.subtle.deriveKey = jest.fn(() => new Promise(r => r(mock.symmKey))) 283 | }, 284 | mockResp: mock.msgBytes, 285 | simpleReq: () => ecc.decrypt( 286 | mock.cipherWithIVBytes, 287 | mock.keys.privateKey, 288 | mock.keys.publicKey 289 | ), 290 | paramChecks: [ 291 | { 292 | desc: 'correctly passes params with AES-CTR', 293 | req: () => ecc.decrypt( 294 | mock.cipherWithIVBytes, 295 | mock.keys.privateKey, 296 | mock.keys.publicKey 297 | ), 298 | params: (params: any) => ( 299 | params[0].name === 'AES-CTR' 300 | && params[0].length === 64 301 | && arrBufEq(params[0].counter.buffer, mock.iv) 302 | && params[1] === mock.symmKey 303 | && arrBufEq(params[2], mock.cipherBytes) 304 | ) 305 | }, 306 | { 307 | desc: 'correctly passes params with AES-CBC', 308 | req: () => ecc.decrypt( 309 | mock.cipherWithIVBytes, 310 | mock.keys.privateKey, 311 | mock.keys.publicKey, 312 | DEFAULT_ECC_CURVE, 313 | { alg: SymmAlg.AES_CBC } 314 | ), 315 | params: (params: any) => ( 316 | params[0]?.name === 'AES-CBC' 317 | && arrBufEq(params[0].iv, mock.iv) 318 | && arrBufEq(params[2], mock.cipherBytes) 319 | ) 320 | } 321 | ], 322 | shouldThrows: [] 323 | }) 324 | 325 | 326 | cryptoMethod({ 327 | desc: 'decrypt', 328 | setMock: fake => { 329 | webcrypto.subtle.decrypt = fake 330 | webcrypto.subtle.deriveKey = jest.fn(() => new Promise(r => r(mock.symmKey))) 331 | }, 332 | mockResp: mock.msgBytes, 333 | simpleReq: () => ecc.decrypt( 334 | mock.cipherWithIVStr, 335 | mock.keys.privateKey, 336 | mock.keyBase64, 337 | DEFAULT_ECC_CURVE 338 | ), 339 | paramChecks: [], 340 | shouldThrows: [] 341 | }) 342 | 343 | 344 | cryptoMethod({ 345 | desc: 'getPublicKey', 346 | setMock: fake => webcrypto.subtle.exportKey = fake, 347 | mockResp: utils.base64ToArrBuf(mock.keyBase64), 348 | expectedResp: mock.keyBase64, 349 | simpleReq: () => ecc.getPublicKey(mock.keys), 350 | simpleParams: [ 351 | 'raw', 352 | mock.keys.publicKey 353 | ], 354 | paramChecks: [], 355 | shouldThrows: [] 356 | }) 357 | 358 | 359 | cryptoMethod({ 360 | desc: 'getSharedKey', 361 | setMock: fake => webcrypto.subtle.deriveKey = fake, 362 | mockResp: mock.symmKey, 363 | simpleReq: () => ecc.getSharedKey(mock.keys.privateKey, mock.keys.publicKey), 364 | simpleParams: [ 365 | { name: 'ECDH', public: mock.keys.publicKey }, 366 | mock.keys.privateKey, 367 | { name: 'AES-CTR', length: 256 }, 368 | false, 369 | ['encrypt', 'decrypt'] 370 | ], 371 | paramChecks: [ 372 | { 373 | desc: 'handles multiple symm key algorithms', 374 | req: () => ecc.getSharedKey(mock.keys.privateKey, mock.keys.publicKey, { alg: SymmAlg.AES_CBC }), 375 | params: (params: any) => params[2]?.name === 'AES-CBC' 376 | }, 377 | { 378 | desc: 'handles multiple symm key lengths', 379 | req: () => ecc.getSharedKey(mock.keys.privateKey, mock.keys.publicKey, { length: SymmKeyLength.B256 }), 380 | params: (params: any) => params[2]?.length === 256 381 | } 382 | ], 383 | shouldThrows: [] 384 | }) 385 | 386 | }) 387 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/elm,vim,node,xcode,ocaml,linux,macos,emacs,elixir,phoenix,windows,haskell,reasonml,reactnative,sublimetext,visualstudio,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=elm,vim,node,xcode,ocaml,linux,macos,emacs,elixir,phoenix,windows,haskell,reasonml,reactnative,sublimetext,visualstudio,visualstudiocode 4 | 5 | ### Elixir ### 6 | /_build 7 | /cover 8 | /deps 9 | /doc 10 | /.fetch 11 | erl_crash.dump 12 | *.ez 13 | *.beam 14 | /config/*.secret.exs 15 | .elixir_ls/ 16 | 17 | ### Elixir Patch ### 18 | 19 | ### Elm ### 20 | # elm-package generated files 21 | elm-stuff 22 | # elm-repl generated files 23 | repl-temp-* 24 | 25 | ### Emacs ### 26 | # -*- mode: gitignore; -*- 27 | *~ 28 | \#*\# 29 | /.emacs.desktop 30 | /.emacs.desktop.lock 31 | *.elc 32 | auto-save-list 33 | tramp 34 | .\#* 35 | 36 | # Org-mode 37 | .org-id-locations 38 | *_archive 39 | 40 | # flymake-mode 41 | *_flymake.* 42 | 43 | # eshell files 44 | /eshell/history 45 | /eshell/lastdir 46 | 47 | # elpa packages 48 | /elpa/ 49 | 50 | # reftex files 51 | *.rel 52 | 53 | # AUCTeX auto folder 54 | /auto/ 55 | 56 | # cask packages 57 | .cask/ 58 | dist/ 59 | 60 | # Flycheck 61 | flycheck_*.el 62 | 63 | # server auth directory 64 | /server/ 65 | 66 | # projectiles files 67 | .projectile 68 | 69 | # directory configuration 70 | .dir-locals.el 71 | 72 | # network security 73 | /network-security.data 74 | 75 | 76 | ### Haskell ### 77 | dist 78 | dist-* 79 | cabal-dev 80 | *.o 81 | *.hi 82 | *.chi 83 | *.chs.h 84 | *.dyn_o 85 | *.dyn_hi 86 | .hpc 87 | .hsenv 88 | .cabal-sandbox/ 89 | cabal.sandbox.config 90 | *.prof 91 | *.aux 92 | *.hp 93 | *.eventlog 94 | .stack-work/ 95 | cabal.project.local 96 | cabal.project.local~ 97 | .HTF/ 98 | .ghc.environment.* 99 | 100 | ### Linux ### 101 | 102 | # temporary files which can be created if a process still has a handle open of a deleted file 103 | .fuse_hidden* 104 | 105 | # KDE directory preferences 106 | .directory 107 | 108 | # Linux trash folder which might appear on any partition or disk 109 | .Trash-* 110 | 111 | # .nfs files are created when an open file is removed but is still being accessed 112 | .nfs* 113 | 114 | ### macOS ### 115 | # General 116 | .DS_Store 117 | .AppleDouble 118 | .LSOverride 119 | 120 | # Icon must end with two \r 121 | Icon 122 | 123 | # Thumbnails 124 | ._* 125 | 126 | # Files that might appear in the root of a volume 127 | .DocumentRevisions-V100 128 | .fseventsd 129 | .Spotlight-V100 130 | .TemporaryItems 131 | .Trashes 132 | .VolumeIcon.icns 133 | .com.apple.timemachine.donotpresent 134 | 135 | # Directories potentially created on remote AFP share 136 | .AppleDB 137 | .AppleDesktop 138 | Network Trash Folder 139 | Temporary Items 140 | .apdisk 141 | 142 | ### Node ### 143 | # Logs 144 | logs 145 | *.log 146 | npm-debug.log* 147 | yarn-debug.log* 148 | yarn-error.log* 149 | lerna-debug.log* 150 | 151 | # Diagnostic reports (https://nodejs.org/api/report.html) 152 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 153 | 154 | # Runtime data 155 | pids 156 | *.pid 157 | *.seed 158 | *.pid.lock 159 | 160 | # Directory for instrumented libs generated by jscoverage/JSCover 161 | lib-cov 162 | 163 | # Coverage directory used by tools like istanbul 164 | coverage 165 | *.lcov 166 | 167 | # nyc test coverage 168 | .nyc_output 169 | 170 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 171 | .grunt 172 | 173 | # Bower dependency directory (https://bower.io/) 174 | bower_components 175 | 176 | # node-waf configuration 177 | .lock-wscript 178 | 179 | # Compiled binary addons (https://nodejs.org/api/addons.html) 180 | build/Release 181 | 182 | # Dependency directories 183 | node_modules/ 184 | jspm_packages/ 185 | 186 | # TypeScript v1 declaration files 187 | typings/ 188 | 189 | # TypeScript cache 190 | *.tsbuildinfo 191 | 192 | # Optional npm cache directory 193 | .npm 194 | 195 | # Optional eslint cache 196 | .eslintcache 197 | 198 | # Optional REPL history 199 | .node_repl_history 200 | 201 | # Output of 'npm pack' 202 | *.tgz 203 | 204 | # Yarn Integrity file 205 | .yarn-integrity 206 | 207 | # dotenv environment variables file 208 | .env 209 | .env.test 210 | 211 | # parcel-bundler cache (https://parceljs.org/) 212 | .cache 213 | 214 | # next.js build output 215 | .next 216 | 217 | # nuxt.js build output 218 | .nuxt 219 | 220 | # vuepress build output 221 | .vuepress/dist 222 | 223 | # Serverless directories 224 | .serverless/ 225 | 226 | # FuseBox cache 227 | .fusebox/ 228 | 229 | # DynamoDB Local files 230 | .dynamodb/ 231 | 232 | ### OCaml ### 233 | *.annot 234 | *.cmo 235 | *.cma 236 | *.cmi 237 | *.a 238 | *.cmx 239 | *.cmxs 240 | *.cmxa 241 | 242 | # ocamlbuild working directory 243 | _build/ 244 | 245 | # ocamlbuild targets 246 | *.byte 247 | *.native 248 | 249 | # oasis generated files 250 | setup.data 251 | setup.log 252 | 253 | # Merlin configuring file for Vim and Emacs 254 | .merlin 255 | 256 | # Dune generated files 257 | *.install 258 | 259 | # Local OPAM switch 260 | _opam/ 261 | 262 | ### Phoenix ### 263 | # gitignore template for Phoenix projects 264 | # website: http://www.phoenixframework.org/ 265 | # 266 | # Recommended template: Elixir.gitignore 267 | 268 | # Temporary files 269 | /tmp 270 | 271 | # Static artifacts 272 | /node_modules 273 | /assets/node_modules 274 | 275 | # Since we are building assets from web/static, 276 | # we ignore priv/static. You may want to comment 277 | # this depending on your deployment strategy. 278 | /priv/static/ 279 | 280 | # Installer-related files 281 | /installer/_build 282 | /installer/tmp 283 | /installer/doc 284 | /installer/deps 285 | 286 | ### ReactNative ### 287 | # React Native Stack Base 288 | 289 | .expo 290 | __generated__ 291 | 292 | ### ReactNative.Xcode Stack ### 293 | # Xcode 294 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 295 | 296 | ## User settings 297 | xcuserdata/ 298 | 299 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 300 | *.xcscmblueprint 301 | *.xccheckout 302 | 303 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 304 | build/ 305 | DerivedData/ 306 | *.moved-aside 307 | *.pbxuser 308 | !default.pbxuser 309 | *.mode1v3 310 | !default.mode1v3 311 | *.mode2v3 312 | !default.mode2v3 313 | *.perspectivev3 314 | !default.perspectivev3 315 | 316 | ## Xcode Patch 317 | *.xcodeproj/* 318 | !*.xcodeproj/project.pbxproj 319 | !*.xcodeproj/xcshareddata/ 320 | !*.xcworkspace/contents.xcworkspacedata 321 | /*.gcno 322 | 323 | ### ReactNative.macOS Stack ### 324 | # General 325 | 326 | # Icon must end with two \r 327 | Icon 328 | 329 | 330 | # Thumbnails 331 | 332 | # Files that might appear in the root of a volume 333 | 334 | # Directories potentially created on remote AFP share 335 | 336 | ### ReactNative.Node Stack ### 337 | # Logs 338 | 339 | # Diagnostic reports (https://nodejs.org/api/report.html) 340 | 341 | # Runtime data 342 | 343 | # Directory for instrumented libs generated by jscoverage/JSCover 344 | 345 | # Coverage directory used by tools like istanbul 346 | 347 | # nyc test coverage 348 | 349 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 350 | 351 | # Bower dependency directory (https://bower.io/) 352 | 353 | # node-waf configuration 354 | 355 | # Compiled binary addons (https://nodejs.org/api/addons.html) 356 | 357 | # Dependency directories 358 | 359 | # TypeScript v1 declaration files 360 | 361 | # TypeScript cache 362 | 363 | # Optional npm cache directory 364 | 365 | # Optional eslint cache 366 | 367 | # Optional REPL history 368 | 369 | # Output of 'npm pack' 370 | 371 | # Yarn Integrity file 372 | 373 | # dotenv environment variables file 374 | 375 | # parcel-bundler cache (https://parceljs.org/) 376 | 377 | # next.js build output 378 | 379 | # nuxt.js build output 380 | 381 | # vuepress build output 382 | 383 | # Serverless directories 384 | 385 | # FuseBox cache 386 | 387 | # DynamoDB Local files 388 | 389 | ### ReactNative.Gradle Stack ### 390 | .gradle 391 | 392 | # Ignore Gradle GUI config 393 | gradle-app.setting 394 | 395 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 396 | !gradle-wrapper.jar 397 | 398 | # Cache of project 399 | .gradletasknamecache 400 | 401 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 402 | # gradle/wrapper/gradle-wrapper.properties 403 | 404 | ### ReactNative.Linux Stack ### 405 | 406 | # temporary files which can be created if a process still has a handle open of a deleted file 407 | 408 | # KDE directory preferences 409 | 410 | # Linux trash folder which might appear on any partition or disk 411 | 412 | # .nfs files are created when an open file is removed but is still being accessed 413 | 414 | ### ReactNative.Buck Stack ### 415 | buck-out/ 416 | .buckconfig.local 417 | .buckd/ 418 | .buckversion 419 | .fakebuckversion 420 | 421 | ### ReactNative.Android Stack ### 422 | # Built application files 423 | *.apk 424 | *.ap_ 425 | *.aab 426 | 427 | # Files for the ART/Dalvik VM 428 | *.dex 429 | 430 | # Java class files 431 | *.class 432 | 433 | # Generated files 434 | bin/ 435 | gen/ 436 | out/ 437 | release/ 438 | 439 | # Gradle files 440 | .gradle/ 441 | 442 | # Local configuration file (sdk path, etc) 443 | local.properties 444 | 445 | # Proguard folder generated by Eclipse 446 | proguard/ 447 | 448 | # Log Files 449 | 450 | # Android Studio Navigation editor temp files 451 | .navigation/ 452 | 453 | # Android Studio captures folder 454 | captures/ 455 | 456 | # IntelliJ 457 | *.iml 458 | .idea/workspace.xml 459 | .idea/tasks.xml 460 | .idea/gradle.xml 461 | .idea/assetWizardSettings.xml 462 | .idea/dictionaries 463 | .idea/libraries 464 | # Android Studio 3 in .gitignore file. 465 | .idea/caches 466 | .idea/modules.xml 467 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 468 | .idea/navEditor.xml 469 | 470 | # Keystore files 471 | # Uncomment the following lines if you do not want to check your keystore files in. 472 | #*.jks 473 | #*.keystore 474 | 475 | # External native build folder generated in Android Studio 2.2 and later 476 | .externalNativeBuild 477 | 478 | # Google Services (e.g. APIs or Firebase) 479 | # google-services.json 480 | 481 | # Freeline 482 | freeline.py 483 | freeline/ 484 | freeline_project_description.json 485 | 486 | # fastlane 487 | fastlane/report.xml 488 | fastlane/Preview.html 489 | fastlane/screenshots 490 | fastlane/test_output 491 | fastlane/readme.md 492 | 493 | # Version control 494 | vcs.xml 495 | 496 | # lint 497 | lint/intermediates/ 498 | lint/generated/ 499 | lint/outputs/ 500 | lint/tmp/ 501 | # lint/reports/ 502 | 503 | ### Reasonml ### 504 | /lib 505 | 506 | ### SublimeText ### 507 | # Cache files for Sublime Text 508 | *.tmlanguage.cache 509 | *.tmPreferences.cache 510 | *.stTheme.cache 511 | 512 | # Workspace files are user-specific 513 | *.sublime-workspace 514 | 515 | # Project files should be checked into the repository, unless a significant 516 | # proportion of contributors will probably not be using Sublime Text 517 | # *.sublime-project 518 | 519 | # SFTP configuration file 520 | sftp-config.json 521 | 522 | # Package control specific files 523 | Package Control.last-run 524 | Package Control.ca-list 525 | Package Control.ca-bundle 526 | Package Control.system-ca-bundle 527 | Package Control.cache/ 528 | Package Control.ca-certs/ 529 | Package Control.merged-ca-bundle 530 | Package Control.user-ca-bundle 531 | oscrypto-ca-bundle.crt 532 | bh_unicode_properties.cache 533 | 534 | # Sublime-github package stores a github token in this file 535 | # https://packagecontrol.io/packages/sublime-github 536 | GitHub.sublime-settings 537 | 538 | ### Vim ### 539 | # Swap 540 | [._]*.s[a-v][a-z] 541 | [._]*.sw[a-p] 542 | [._]s[a-rt-v][a-z] 543 | [._]ss[a-gi-z] 544 | [._]sw[a-p] 545 | 546 | # Session 547 | Session.vim 548 | Sessionx.vim 549 | 550 | # Temporary 551 | .netrwhist 552 | # Auto-generated tag files 553 | tags 554 | # Persistent undo 555 | [._]*.un~ 556 | 557 | ### VisualStudioCode ### 558 | .vscode/* 559 | !.vscode/settings.json 560 | !.vscode/tasks.json 561 | !.vscode/launch.json 562 | !.vscode/extensions.json 563 | 564 | ### VisualStudioCode Patch ### 565 | # Ignore all local history of files 566 | .history 567 | 568 | ### Windows ### 569 | # Windows thumbnail cache files 570 | Thumbs.db 571 | Thumbs.db:encryptable 572 | ehthumbs.db 573 | ehthumbs_vista.db 574 | 575 | # Dump file 576 | *.stackdump 577 | 578 | # Folder config file 579 | [Dd]esktop.ini 580 | 581 | # Recycle Bin used on file shares 582 | $RECYCLE.BIN/ 583 | 584 | # Windows Installer files 585 | *.cab 586 | *.msi 587 | *.msix 588 | *.msm 589 | *.msp 590 | 591 | # Windows shortcuts 592 | *.lnk 593 | 594 | ### Xcode ### 595 | # Xcode 596 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 597 | 598 | 599 | 600 | 601 | 602 | ### Xcode Patch ### 603 | **/xcshareddata/WorkspaceSettings.xcsettings 604 | 605 | ### VisualStudio ### 606 | ## Ignore Visual Studio temporary files, build results, and 607 | ## files generated by popular Visual Studio add-ons. 608 | ## 609 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 610 | 611 | # User-specific files 612 | *.rsuser 613 | *.suo 614 | *.user 615 | *.userosscache 616 | *.sln.docstates 617 | 618 | # User-specific files (MonoDevelop/Xamarin Studio) 619 | *.userprefs 620 | 621 | # Mono auto generated files 622 | mono_crash.* 623 | 624 | # Build results 625 | [Dd]ebug/ 626 | [Dd]ebugPublic/ 627 | [Rr]elease/ 628 | [Rr]eleases/ 629 | x64/ 630 | x86/ 631 | [Aa][Rr][Mm]/ 632 | [Aa][Rr][Mm]64/ 633 | bld/ 634 | [Bb]in/ 635 | [Oo]bj/ 636 | [Ll]og/ 637 | 638 | # Visual Studio 2015/2017 cache/options directory 639 | .vs/ 640 | # Uncomment if you have tasks that create the project's static files in wwwroot 641 | #wwwroot/ 642 | 643 | # Visual Studio 2017 auto generated files 644 | Generated\ Files/ 645 | 646 | # MSTest test Results 647 | [Tt]est[Rr]esult*/ 648 | [Bb]uild[Ll]og.* 649 | 650 | # NUnit 651 | *.VisualState.xml 652 | TestResult.xml 653 | nunit-*.xml 654 | 655 | # Build Results of an ATL Project 656 | [Dd]ebugPS/ 657 | [Rr]eleasePS/ 658 | dlldata.c 659 | 660 | # Benchmark Results 661 | BenchmarkDotNet.Artifacts/ 662 | 663 | # .NET Core 664 | project.lock.json 665 | project.fragment.lock.json 666 | artifacts/ 667 | 668 | # StyleCop 669 | StyleCopReport.xml 670 | 671 | # Files built by Visual Studio 672 | *_i.c 673 | *_p.c 674 | *_h.h 675 | *.ilk 676 | *.meta 677 | *.obj 678 | *.iobj 679 | *.pch 680 | *.pdb 681 | *.ipdb 682 | *.pgc 683 | *.pgd 684 | *.rsp 685 | *.sbr 686 | *.tlb 687 | *.tli 688 | *.tlh 689 | *.tmp 690 | *.tmp_proj 691 | *_wpftmp.csproj 692 | *.vspscc 693 | *.vssscc 694 | .builds 695 | *.pidb 696 | *.svclog 697 | *.scc 698 | 699 | # Chutzpah Test files 700 | _Chutzpah* 701 | 702 | # Visual C++ cache files 703 | ipch/ 704 | *.aps 705 | *.ncb 706 | *.opendb 707 | *.opensdf 708 | *.sdf 709 | *.cachefile 710 | *.VC.db 711 | *.VC.VC.opendb 712 | 713 | # Visual Studio profiler 714 | *.psess 715 | *.vsp 716 | *.vspx 717 | *.sap 718 | 719 | # Visual Studio Trace Files 720 | *.e2e 721 | 722 | # TFS 2012 Local Workspace 723 | $tf/ 724 | 725 | # Guidance Automation Toolkit 726 | *.gpState 727 | 728 | # ReSharper is a .NET coding add-in 729 | _ReSharper*/ 730 | *.[Rr]e[Ss]harper 731 | *.DotSettings.user 732 | 733 | # JustCode is a .NET coding add-in 734 | .JustCode 735 | 736 | # TeamCity is a build add-in 737 | _TeamCity* 738 | 739 | # DotCover is a Code Coverage Tool 740 | *.dotCover 741 | 742 | # AxoCover is a Code Coverage Tool 743 | .axoCover/* 744 | !.axoCover/settings.json 745 | 746 | # Visual Studio code coverage results 747 | *.coverage 748 | *.coveragexml 749 | 750 | # NCrunch 751 | _NCrunch_* 752 | .*crunch*.local.xml 753 | nCrunchTemp_* 754 | 755 | # MightyMoose 756 | *.mm.* 757 | AutoTest.Net/ 758 | 759 | # Web workbench (sass) 760 | .sass-cache/ 761 | 762 | # Installshield output folder 763 | [Ee]xpress/ 764 | 765 | # DocProject is a documentation generator add-in 766 | DocProject/buildhelp/ 767 | DocProject/Help/*.HxT 768 | DocProject/Help/*.HxC 769 | DocProject/Help/*.hhc 770 | DocProject/Help/*.hhk 771 | DocProject/Help/*.hhp 772 | DocProject/Help/Html2 773 | DocProject/Help/html 774 | 775 | # Click-Once directory 776 | publish/ 777 | 778 | # Publish Web Output 779 | *.[Pp]ublish.xml 780 | *.azurePubxml 781 | # Note: Comment the next line if you want to checkin your web deploy settings, 782 | # but database connection strings (with potential passwords) will be unencrypted 783 | *.pubxml 784 | *.publishproj 785 | 786 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 787 | # checkin your Azure Web App publish settings, but sensitive information contained 788 | # in these scripts will be unencrypted 789 | PublishScripts/ 790 | 791 | # NuGet Packages 792 | *.nupkg 793 | # NuGet Symbol Packages 794 | *.snupkg 795 | # The packages folder can be ignored because of Package Restore 796 | **/[Pp]ackages/* 797 | # except build/, which is used as an MSBuild target. 798 | !**/[Pp]ackages/build/ 799 | # Uncomment if necessary however generally it will be regenerated when needed 800 | #!**/[Pp]ackages/repositories.config 801 | # NuGet v3's project.json files produces more ignorable files 802 | *.nuget.props 803 | *.nuget.targets 804 | 805 | # Microsoft Azure Build Output 806 | csx/ 807 | *.build.csdef 808 | 809 | # Microsoft Azure Emulator 810 | ecf/ 811 | rcf/ 812 | 813 | # Windows Store app package directories and files 814 | AppPackages/ 815 | BundleArtifacts/ 816 | Package.StoreAssociation.xml 817 | _pkginfo.txt 818 | *.appx 819 | *.appxbundle 820 | *.appxupload 821 | 822 | # Visual Studio cache files 823 | # files ending in .cache can be ignored 824 | *.[Cc]ache 825 | # but keep track of directories ending in .cache 826 | !?*.[Cc]ache/ 827 | 828 | # Others 829 | ClientBin/ 830 | ~$* 831 | *.dbmdl 832 | *.dbproj.schemaview 833 | *.jfm 834 | *.pfx 835 | *.publishsettings 836 | orleans.codegen.cs 837 | 838 | # Including strong name files can present a security risk 839 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 840 | #*.snk 841 | 842 | # Since there are multiple workflows, uncomment next line to ignore bower_components 843 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 844 | #bower_components/ 845 | 846 | # RIA/Silverlight projects 847 | Generated_Code/ 848 | 849 | # Backup & report files from converting an old project file 850 | # to a newer Visual Studio version. Backup files are not needed, 851 | # because we have git ;-) 852 | _UpgradeReport_Files/ 853 | Backup*/ 854 | UpgradeLog*.XML 855 | UpgradeLog*.htm 856 | ServiceFabricBackup/ 857 | *.rptproj.bak 858 | 859 | # SQL Server files 860 | *.mdf 861 | *.ldf 862 | *.ndf 863 | 864 | # Business Intelligence projects 865 | *.rdl.data 866 | *.bim.layout 867 | *.bim_*.settings 868 | *.rptproj.rsuser 869 | *- [Bb]ackup.rdl 870 | *- [Bb]ackup ([0-9]).rdl 871 | *- [Bb]ackup ([0-9][0-9]).rdl 872 | 873 | # Microsoft Fakes 874 | FakesAssemblies/ 875 | 876 | # GhostDoc plugin setting file 877 | *.GhostDoc.xml 878 | 879 | # Node.js Tools for Visual Studio 880 | .ntvs_analysis.dat 881 | 882 | # Visual Studio 6 build log 883 | *.plg 884 | 885 | # Visual Studio 6 workspace options file 886 | *.opt 887 | 888 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 889 | *.vbw 890 | 891 | # Visual Studio LightSwitch build output 892 | **/*.HTMLClient/GeneratedArtifacts 893 | **/*.DesktopClient/GeneratedArtifacts 894 | **/*.DesktopClient/ModelManifest.xml 895 | **/*.Server/GeneratedArtifacts 896 | **/*.Server/ModelManifest.xml 897 | _Pvt_Extensions 898 | 899 | # Paket dependency manager 900 | .paket/paket.exe 901 | paket-files/ 902 | 903 | # FAKE - F# Make 904 | .fake/ 905 | 906 | # CodeRush personal settings 907 | .cr/personal 908 | 909 | # Python Tools for Visual Studio (PTVS) 910 | __pycache__/ 911 | *.pyc 912 | 913 | # Cake - Uncomment if you are using it 914 | # tools/** 915 | # !tools/packages.config 916 | 917 | # Tabs Studio 918 | *.tss 919 | 920 | # Telerik's JustMock configuration file 921 | *.jmconfig 922 | 923 | # BizTalk build output 924 | *.btp.cs 925 | *.btm.cs 926 | *.odx.cs 927 | *.xsd.cs 928 | 929 | # OpenCover UI analysis results 930 | OpenCover/ 931 | 932 | # Azure Stream Analytics local run output 933 | ASALocalRun/ 934 | 935 | # MSBuild Binary and Structured Log 936 | *.binlog 937 | 938 | # NVidia Nsight GPU debugger configuration file 939 | *.nvuser 940 | 941 | # MFractors (Xamarin productivity tool) working folder 942 | .mfractor/ 943 | 944 | # Local History for Visual Studio 945 | .localhistory/ 946 | 947 | # BeatPulse healthcheck temp database 948 | healthchecksdb 949 | 950 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 951 | MigrationBackup/ 952 | 953 | # End of https://www.gitignore.io/api/elm,vim,node,xcode,ocaml,linux,macos,emacs,elixir,phoenix,windows,haskell,reasonml,reactnative,sublimetext,visualstudio,visualstudiocode 954 | 955 | 956 | # Created by https://www.gitignore.io/api/latex 957 | # Edit at https://www.gitignore.io/?templates=latex 958 | 959 | ### LaTeX ### 960 | ## Core latex/pdflatex auxiliary files: 961 | *.aux 962 | *.lof 963 | *.log 964 | *.lot 965 | *.fls 966 | *.out 967 | *.toc 968 | *.fmt 969 | *.fot 970 | *.cb 971 | *.cb2 972 | .*.lb 973 | 974 | ## Intermediate documents: 975 | *.dvi 976 | *.xdv 977 | *-converted-to.* 978 | # these rules might exclude image files for figures etc. 979 | # *.ps 980 | # *.eps 981 | # *.pdf 982 | 983 | ## Generated if empty string is given at "Please type another file name for output:" 984 | .pdf 985 | 986 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 987 | *.bbl 988 | *.bcf 989 | *.blg 990 | *-blx.aux 991 | *-blx.bib 992 | *.run.xml 993 | 994 | ## Build tool auxiliary files: 995 | *.fdb_latexmk 996 | *.synctex 997 | *.synctex(busy) 998 | *.synctex.gz 999 | *.synctex.gz(busy) 1000 | *.pdfsync 1001 | 1002 | ## Build tool directories for auxiliary files 1003 | # latexrun 1004 | latex.out/ 1005 | 1006 | ## Auxiliary and intermediate files from other packages: 1007 | # algorithms 1008 | *.alg 1009 | *.loa 1010 | 1011 | # achemso 1012 | acs-*.bib 1013 | 1014 | # amsthm 1015 | *.thm 1016 | 1017 | # beamer 1018 | *.nav 1019 | *.pre 1020 | *.snm 1021 | *.vrb 1022 | 1023 | # changes 1024 | *.soc 1025 | 1026 | # comment 1027 | *.cut 1028 | 1029 | # cprotect 1030 | *.cpt 1031 | 1032 | # elsarticle (documentclass of Elsevier journals) 1033 | *.spl 1034 | 1035 | # endnotes 1036 | *.ent 1037 | 1038 | # fixme 1039 | *.lox 1040 | 1041 | # feynmf/feynmp 1042 | *.mf 1043 | *.mp 1044 | *.t[1-9] 1045 | *.t[1-9][0-9] 1046 | *.tfm 1047 | 1048 | #(r)(e)ledmac/(r)(e)ledpar 1049 | *.end 1050 | *.?end 1051 | *.[1-9] 1052 | *.[1-9][0-9] 1053 | *.[1-9][0-9][0-9] 1054 | *.[1-9]R 1055 | *.[1-9][0-9]R 1056 | *.[1-9][0-9][0-9]R 1057 | *.eledsec[1-9] 1058 | *.eledsec[1-9]R 1059 | *.eledsec[1-9][0-9] 1060 | *.eledsec[1-9][0-9]R 1061 | *.eledsec[1-9][0-9][0-9] 1062 | *.eledsec[1-9][0-9][0-9]R 1063 | 1064 | # glossaries 1065 | *.acn 1066 | *.acr 1067 | *.glg 1068 | *.glo 1069 | *.gls 1070 | *.glsdefs 1071 | 1072 | # uncomment this for glossaries-extra (will ignore makeindex's style files!) 1073 | # *.ist 1074 | 1075 | # gnuplottex 1076 | *-gnuplottex-* 1077 | 1078 | # gregoriotex 1079 | *.gaux 1080 | *.gtex 1081 | 1082 | # htlatex 1083 | *.4ct 1084 | *.4tc 1085 | *.idv 1086 | *.lg 1087 | *.trc 1088 | *.xref 1089 | 1090 | # hyperref 1091 | *.brf 1092 | 1093 | # knitr 1094 | *-concordance.tex 1095 | # TODO Comment the next line if you want to keep your tikz graphics files 1096 | *.tikz 1097 | *-tikzDictionary 1098 | 1099 | # listings 1100 | *.lol 1101 | 1102 | # luatexja-ruby 1103 | *.ltjruby 1104 | 1105 | # makeidx 1106 | *.idx 1107 | *.ilg 1108 | *.ind 1109 | 1110 | # minitoc 1111 | *.maf 1112 | *.mlf 1113 | *.mlt 1114 | *.mtc[0-9]* 1115 | *.slf[0-9]* 1116 | *.slt[0-9]* 1117 | *.stc[0-9]* 1118 | 1119 | # minted 1120 | _minted* 1121 | *.pyg 1122 | 1123 | # morewrites 1124 | *.mw 1125 | 1126 | # nomencl 1127 | *.nlg 1128 | *.nlo 1129 | *.nls 1130 | 1131 | # pax 1132 | *.pax 1133 | 1134 | # pdfpcnotes 1135 | *.pdfpc 1136 | 1137 | # sagetex 1138 | *.sagetex.sage 1139 | *.sagetex.py 1140 | *.sagetex.scmd 1141 | 1142 | # scrwfile 1143 | *.wrt 1144 | 1145 | # sympy 1146 | *.sout 1147 | *.sympy 1148 | sympy-plots-for-*.tex/ 1149 | 1150 | # pdfcomment 1151 | *.upa 1152 | *.upb 1153 | 1154 | # pythontex 1155 | *.pytxcode 1156 | pythontex-files-*/ 1157 | 1158 | # tcolorbox 1159 | *.listing 1160 | 1161 | # thmtools 1162 | *.loe 1163 | 1164 | # TikZ & PGF 1165 | *.dpth 1166 | *.md5 1167 | *.auxlock 1168 | 1169 | # todonotes 1170 | *.tdo 1171 | 1172 | # vhistory 1173 | *.hst 1174 | *.ver 1175 | 1176 | # easy-todo 1177 | *.lod 1178 | 1179 | # xcolor 1180 | *.xcp 1181 | 1182 | # xmpincl 1183 | *.xmpi 1184 | 1185 | # xindy 1186 | *.xdy 1187 | 1188 | # xypic precompiled matrices 1189 | *.xyc 1190 | 1191 | # endfloat 1192 | *.ttt 1193 | *.fff 1194 | 1195 | # Latexian 1196 | TSWLatexianTemp* 1197 | 1198 | ## Editors: 1199 | # WinEdt 1200 | *.bak 1201 | *.sav 1202 | 1203 | # Texpad 1204 | .texpadtmp 1205 | 1206 | # LyX 1207 | *.lyx~ 1208 | 1209 | # Kile 1210 | *.backup 1211 | 1212 | # KBibTeX 1213 | *~[0-9]* 1214 | 1215 | # auto folder when using emacs and auctex 1216 | ./auto/* 1217 | *.el 1218 | 1219 | # expex forward references with \gathertags 1220 | *-tags.tex 1221 | 1222 | # standalone packages 1223 | *.sta 1224 | 1225 | ### LaTeX Patch ### 1226 | # glossaries 1227 | *.glstex 1228 | 1229 | # End of https://www.gitignore.io/api/latex 1230 | 1231 | # Custom 1232 | .rpt2_cache 1233 | docs 1234 | --------------------------------------------------------------------------------