├── .eslintrc ├── .github ├── CODEOWNERS └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── karma.conf.js ├── package.json ├── scripts ├── schema-validation-codegen.ts └── schema.json ├── src ├── checksum │ ├── index.ts │ └── sha256.ts ├── cipher │ ├── aes128Ctr.ts │ └── index.ts ├── class.ts ├── env.ts ├── functional.ts ├── index.ts ├── kdf │ ├── index.ts │ ├── pbkdf2.ts │ └── scrypt.ts ├── password.ts ├── schema-validation-generated.ts ├── schema-validation.ts └── types.ts ├── test ├── index.test.ts └── vectors │ ├── pbkdf2-0.json │ ├── pbkdf2-1.json │ ├── scrypt-0.json │ └── scrypt-1.json ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "env": { 7 | "jest": true 8 | }, 9 | "plugins": ["@typescript-eslint"], 10 | "extends": ["plugin:@typescript-eslint/recommended"], 11 | "rules": { 12 | "new-parens": "error", 13 | "no-caller": "error", 14 | "no-bitwise": "off", 15 | "@typescript-eslint/indent": ["error", 2], 16 | "@typescript-eslint/no-use-before-define": "off", 17 | "@typescript-eslint/no-explicit-any": "off", 18 | "@typescript-eslint/interface-name-prefix": "off", 19 | "no-console": "warn" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # They will be requested for 4 | # review when someone opens a pull request. 5 | * @ChainSafe/lodestar 6 | 7 | # Order is important; the last matching pattern takes the most 8 | # precedence. When someone opens a pull request that only 9 | # modifies md files, only md owners and not the global 10 | # owner(s) will be requested for a review. 11 | *.md @ChainSafe/lodestar 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | tests: 7 | name: Tests 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node: [16, 18, 20] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: ${{matrix.node}} 18 | - run: yarn install 19 | - run: yarn build 20 | - name: Lint 21 | run: yarn lint 22 | - name: Check Types 23 | run: yarn run check-types 24 | - name: Unit tests 25 | run: yarn test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea 3 | .env 4 | .nyc_output 5 | lib 6 | dist 7 | .idea 8 | coverage 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | 18 | .vscode 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.1.0 (2024-07-15) 2 | 3 | - feat: use native crypto functions ([#61](https://github.com/ChainSafe/bls-keystore/pull/61)) 4 | 5 | ## 3.0.1 (2024-01-30) 6 | 7 | - fix: update dependencies ([#57](https://github.com/ChainSafe/bls-keystore/pull/57)) 8 | 9 | ## 3.0.0 (2022-02-22) 10 | 11 | ### BREAKING CHANGES 12 | 13 | - Replace `Buffer` with `Uint8Array` in all public APIs ([#28](https://github.com/ChainSafe/bls-keystore/pull/28)) 14 | 15 | ### Chores 16 | 17 | - Use ajv codegen ([#32](https://github.com/ChainSafe/bls-keystore/pull/32)) 18 | - remove stale files ([#35](https://github.com/ChainSafe/bls-keystore/pull/35)) 19 | - lock uuid version ([#41](https://github.com/ChainSafe/bls-keystore/pull/41)) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ChainSafe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ChainSafe/bls-keystore 2 | 3 | ![npm](https://img.shields.io/npm/v/@chainsafe/bls-keystore) 4 | ![Discord](https://img.shields.io/discord/593655374469660673?color=blue&label=Discord&logo=discord) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | ![es-version](https://img.shields.io/badge/ES-2015-yellow) 7 | ![node-version](https://img.shields.io/badge/node-10.x-green) 8 | 9 | > Typescript implementation of [EIP 2335](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2335.md) for node and browser. 10 | 11 | ### How to use? 12 | 13 | Functional interface 14 | ```typescript 15 | import { 16 | IKeystore, 17 | create, 18 | decrypt, 19 | verifyPassword, 20 | isValidKeystore, 21 | validateKeystore, 22 | } from "@chainsafe/bls-keystore"; 23 | 24 | // encrypt private key 25 | const password: string | Uint8Array = "SomePassword123"; 26 | const privateKey: Uint8Array = ...; 27 | const publicKey: Uint8Array = ...; 28 | const path: string = "m/12381/60/0/0"; 29 | 30 | // keystore is an `object` that follows the EIP-2335 schema 31 | const keystore: IKeystore = await create(password, privateKey, publicKey, path); 32 | 33 | // verify password 34 | await verifyPassword(keystore, password); //true | false 35 | 36 | // decrypt 37 | const decryptedPrivateKey: Uint8Array = await decrypt(keystore, password); 38 | 39 | // convert to string 40 | JSON.stringify(keystore); //string 41 | 42 | // determine if unsanitized data fits the EIP-2335 schema 43 | const data: unknown = ...; 44 | isValidKeystore(data); // true | false 45 | 46 | validateKeystore(data); // throws if invalid 47 | ``` 48 | 49 | Class-based interface 50 | ```typescript 51 | import { 52 | Keystore, 53 | } from "@chainsafe/bls-keystore"; 54 | 55 | // encrypt private key 56 | const password: string | Uint8Array = "SomePassword123"; 57 | const privateKey: Uint8Array = ...; 58 | const publicKey: Uint8Array = ...; 59 | const path: string = "m/12381/60/0/0"; 60 | 61 | // keystore is a `Keystore` instance that follows the EIP-2335 schema with additional convenience methods 62 | const keystore: Keystore = await Keystore.create(password, privateKey, publicKey, path); 63 | 64 | // verify password 65 | await keystore.verifyPassword(password); //true | false 66 | 67 | // decrypt 68 | const decryptedPrivateKey: Uint8Array = await keystore.decrypt(password); 69 | 70 | // convert to string 71 | keystore.stringify(); //string 72 | 73 | // determine if unsanitized data fits the EIP-2335 schema 74 | const data: unknown = ...; 75 | Keystore.fromObject(data); // returns a Keystore or throws if data is invalid 76 | ``` 77 | 78 | For key derivation checkout [@chainsafe/bls-keygen](https://github.com/ChainSafe/bls-keygen) 79 | 80 | ### Contribute 81 | 82 | - get yarn 83 | - yarn install 84 | - yarn test 85 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | const webpackConfig = require("./webpack.config"); 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | basePath: "", 8 | frameworks: ["mocha", "chai"], 9 | files: ["test/*.ts"], 10 | exclude: [], 11 | preprocessors: { 12 | "test/**/*.ts": ["webpack"] 13 | }, 14 | webpack: { 15 | mode: "production", 16 | module: webpackConfig.module, 17 | resolve: webpackConfig.resolve 18 | }, 19 | reporters: ["spec"], 20 | 21 | browsers: ["ChromeHeadless"], 22 | 23 | singleRun: true 24 | }); 25 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chainsafe/bls-keystore", 3 | "version": "3.1.0", 4 | "main": "lib/index.js", 5 | "files": [ 6 | "lib" 7 | ], 8 | "browser": { 9 | "lib/node": "lib/browser" 10 | }, 11 | "types": "lib/index.d.ts", 12 | "repository": "git@github.com:ChainSafe/bls-keystore.git", 13 | "author": "ChainSafe ", 14 | "license": "MIT", 15 | "keywords": [ 16 | "ethereum", 17 | "eth2", 18 | "bls", 19 | "eip-2335" 20 | ], 21 | "scripts": { 22 | "prebuild": "rm -rf lib && yarn build:codegen", 23 | "build:codegen": "ts-node -P tsconfig.json scripts/schema-validation-codegen.ts", 24 | "build": "tsc --declaration --outDir lib", 25 | "check-types": "tsc --noEmit", 26 | "test": "yarn test:node && yarn test:browser", 27 | "test:node": "mocha --verbose --colors --coverage -r ts-node/register test/*.test.ts", 28 | "test:browser": "karma start", 29 | "lint": "eslint --ext .ts src/" 30 | }, 31 | "devDependencies": { 32 | "@types/chai": "^4.2.11", 33 | "@types/mocha": "^7.0.2", 34 | "@types/node": "^20.11.11", 35 | "@types/uuid": "^9.0.0", 36 | "@typescript-eslint/eslint-plugin": "^6.20.0", 37 | "@typescript-eslint/parser": "^6.20.0", 38 | "ajv": "^8.10.0", 39 | "ajv-formats": "^2.1.1", 40 | "chai": "^4.2.0", 41 | "eslint": "^8.56.0", 42 | "karma": "^6.3.16", 43 | "karma-chai": "^0.1.0", 44 | "karma-chrome-launcher": "^3.1.0", 45 | "karma-cli": "^2.0.0", 46 | "karma-mocha": "^2.0.1", 47 | "karma-spec-reporter": "^0.0.32", 48 | "karma-webpack": "^5.0.0", 49 | "mocha": "^7.1.2", 50 | "ts-loader": "^9.5.0", 51 | "ts-node": "^10.9.0", 52 | "typescript": "^5.3.0", 53 | "webpack": "^5.0.0", 54 | "webpack-cli": "^5.0.0" 55 | }, 56 | "dependencies": { 57 | "ethereum-cryptography": "^2.0.0", 58 | "uuid": "^9.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scripts/schema-validation-codegen.ts: -------------------------------------------------------------------------------- 1 | import {writeFileSync} from "fs"; 2 | import {join} from "path"; 3 | 4 | import Ajv from "ajv"; 5 | import addFormats from "ajv-formats"; 6 | import standaloneCode from "ajv/dist/standalone"; 7 | 8 | import schema = require("./schema.json"); 9 | 10 | // Generate a function that validates the Keystore schema. 11 | // Write the function to a file in the source. 12 | 13 | const OUTPUT_DIR = join(__dirname, "../src"); 14 | const OUTPUT_SOURCE_FILE = join(OUTPUT_DIR, "schema-validation-generated.ts"); 15 | 16 | // Initialize ajv instance 17 | const ajv = new Ajv({ 18 | schemas: [schema], 19 | code: { 20 | source: true, 21 | esm: true, 22 | } 23 | }); 24 | addFormats(ajv); 25 | 26 | // The output code is minified vanilla javascript and uses the cute pattern of attaching errors to the exported function as a property. 27 | // The easiest way to treat this is to just ts-ignore the whole output and wrap the function with a handcrafted type. 28 | const moduleCode = ` 29 | // This file was generated by /scripts/schema-validation-codegen.ts 30 | // Do not modify this file by hand. 31 | 32 | /* eslint-disable */ 33 | // @ts-ignore 34 | ` + standaloneCode(ajv, { 35 | Keystore: "#/definitions/Keystore", 36 | }); 37 | 38 | writeFileSync(OUTPUT_SOURCE_FILE, moduleCode) 39 | -------------------------------------------------------------------------------- /scripts/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$ref": "#/definitions/Keystore", 3 | "definitions": { 4 | "Keystore": { 5 | "type": "object", 6 | "properties": { 7 | "crypto": { 8 | "type": "object", 9 | "properties": { 10 | "kdf": { 11 | "$ref": "#/definitions/KdfModule" 12 | }, 13 | "checksum": { 14 | "$ref": "#/definitions/ChecksumModule" 15 | }, 16 | "cipher": { 17 | "$ref": "#/definitions/CipherModule" 18 | } 19 | }, 20 | "required": [ 21 | "kdf", 22 | "checksum", 23 | "cipher" 24 | ] 25 | }, 26 | "pubkey": { 27 | "type": "string", 28 | "pattern": "^([A-Fa-f0-9]{2}){48}$" 29 | }, 30 | "path": { 31 | "type": "string" 32 | }, 33 | "description": { 34 | "type": "string" 35 | }, 36 | "uuid": { 37 | "type": "string", 38 | "format": "uuid" 39 | }, 40 | "version": { 41 | "type": "integer", 42 | "minimum": 4, 43 | "maximum": 4 44 | } 45 | }, 46 | "required": [ 47 | "crypto", 48 | "path", 49 | "uuid", 50 | "version" 51 | ], 52 | "title": "Keystore" 53 | }, 54 | "Module": { 55 | "type": "object", 56 | "properties": { 57 | "function": { 58 | "type": "string" 59 | }, 60 | "params": { 61 | "type": "object" 62 | }, 63 | "message": { 64 | "type": "string" 65 | } 66 | }, 67 | "required": [ 68 | "function", 69 | "message", 70 | "params" 71 | ] 72 | }, 73 | "KdfModule": { 74 | "allOf": [ 75 | { 76 | "$ref": "#/definitions/Module" 77 | }, 78 | { 79 | "oneOf": [ 80 | { 81 | "$ref": "#/definitions/Pbkdf2Module" 82 | }, 83 | { 84 | "$ref": "#/definitions/ScryptModule" 85 | } 86 | ] 87 | } 88 | ] 89 | }, 90 | "ChecksumModule": { 91 | "allOf": [ 92 | { 93 | "$ref": "#/definitions/Module" 94 | }, 95 | { 96 | "oneOf": [ 97 | { 98 | "$ref": "#/definitions/Sha2Module" 99 | } 100 | ] 101 | } 102 | ] 103 | }, 104 | "CipherModule": { 105 | "allOf": [ 106 | { 107 | "$ref": "#/definitions/Module" 108 | }, 109 | { 110 | "oneOf": [ 111 | { 112 | "$ref": "#/definitions/Aes128CtrModule" 113 | } 114 | ] 115 | } 116 | ] 117 | }, 118 | "Pbkdf2Module": { 119 | "type": "object", 120 | "properties": { 121 | "function": { 122 | "type": "string", 123 | "pattern": "^pbkdf2$" 124 | }, 125 | "params": { 126 | "type": "object", 127 | "properties": { 128 | "dklen": { 129 | "type": "integer", 130 | "minimum": 0 131 | }, 132 | "c": { 133 | "type": "integer", 134 | "minimum": 0 135 | }, 136 | "prf": { 137 | "type": "string", 138 | "pattern": "^hmac-sha256$" 139 | }, 140 | "salt": { 141 | "type": "string" 142 | } 143 | }, 144 | "required": [ 145 | "dklen", 146 | "c", 147 | "prf", 148 | "salt" 149 | ] 150 | }, 151 | "message": { 152 | "type": "string", 153 | "pattern": "^$" 154 | } 155 | } 156 | }, 157 | "ScryptModule": { 158 | "type": "object", 159 | "properties": { 160 | "function": { 161 | "type": "string", 162 | "pattern": "^scrypt$" 163 | }, 164 | "params": { 165 | "type": "object", 166 | "properties": { 167 | "dklen": { 168 | "type": "integer", 169 | "minimum": 0 170 | }, 171 | "n": { 172 | "type": "integer", 173 | "minimum": 0 174 | }, 175 | "p": { 176 | "type": "integer", 177 | "minimum": 0 178 | }, 179 | "r": { 180 | "type": "integer", 181 | "minimum": 0 182 | }, 183 | "salt": { 184 | "type": "string", 185 | "pattern": "^([A-Fa-f0-9]{2}){32}$" 186 | } 187 | }, 188 | "required": [ 189 | "dklen", 190 | "n", 191 | "p", 192 | "r", 193 | "salt" 194 | ] 195 | }, 196 | "message": { 197 | "type": "string", 198 | "pattern": "^$" 199 | } 200 | } 201 | }, 202 | "Sha2Module": { 203 | "type": "object", 204 | "properties": { 205 | "function": { 206 | "type": "string", 207 | "pattern": "^sha256$" 208 | }, 209 | "params": { 210 | "type": "object", 211 | "additionalProperties": false 212 | }, 213 | "message": { 214 | "type": "string", 215 | "pattern": "^([A-Fa-f0-9]{2}){32}$" 216 | } 217 | } 218 | }, 219 | "Aes128CtrModule": { 220 | "type": "object", 221 | "properties": { 222 | "function": { 223 | "type": "string", 224 | "pattern": "^aes-128-ctr$" 225 | }, 226 | "params": { 227 | "type": "object", 228 | "properties": { 229 | "iv": { 230 | "type": "string", 231 | "pattern": "^([A-Fa-f0-9]{2}){16}$" 232 | } 233 | }, 234 | "required": [ 235 | "iv" 236 | ] 237 | }, 238 | "message": { 239 | "type": "string", 240 | "pattern": "^([A-Fa-f0-9]{2}){32}$" 241 | } 242 | } 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/checksum/index.ts: -------------------------------------------------------------------------------- 1 | import { concatBytes, equalsBytes, hexToBytes } from "ethereum-cryptography/utils"; 2 | 3 | import { IChecksumModule } from "../types"; 4 | import { sha256 } from "./sha256"; 5 | 6 | // default checksum configuration 7 | 8 | export function defaultSha256Module(): Pick { 9 | return { 10 | function: "sha256", 11 | }; 12 | } 13 | 14 | // checksum operations 15 | 16 | function checksumData(key: Uint8Array, ciphertext: Uint8Array): Uint8Array { 17 | return concatBytes(key.slice(16), ciphertext); 18 | } 19 | 20 | export async function checksum(mod: IChecksumModule, key: Uint8Array, ciphertext: Uint8Array): Promise { 21 | if (mod.function === "sha256") { 22 | return await sha256(checksumData(key, ciphertext)); 23 | } else { 24 | throw new Error("Invalid checksum type"); 25 | } 26 | } 27 | 28 | export async function verifyChecksum(mod: IChecksumModule, key: Uint8Array, ciphertext: Uint8Array): Promise { 29 | if (mod.function === "sha256") { 30 | return equalsBytes(hexToBytes(mod.message), await sha256(checksumData(key, ciphertext))); 31 | } else { 32 | throw new Error("Invalid checksum type"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/checksum/sha256.ts: -------------------------------------------------------------------------------- 1 | import { sha256 as _sha256 } from "ethereum-cryptography/sha256"; 2 | 3 | import { hasWebCrypto } from "../env"; 4 | 5 | export const sha256 = hasWebCrypto ? sha256WebCrypto : sha256Js; 6 | 7 | async function sha256WebCrypto(data: Uint8Array): Promise { 8 | return new Uint8Array(await crypto.subtle.digest("SHA-256", data)); 9 | } 10 | 11 | async function sha256Js(data: Uint8Array): Promise { 12 | return _sha256(data); 13 | } 14 | -------------------------------------------------------------------------------- /src/cipher/aes128Ctr.ts: -------------------------------------------------------------------------------- 1 | import { encrypt as aesEncrypt, decrypt as aesDecrypt } from "ethereum-cryptography/aes"; 2 | 3 | import { hasWebCrypto } from "../env"; 4 | 5 | export const aes128CtrEncrypt = hasWebCrypto ? aes128CtrEncryptWebCrypto : aes128CtrEncryptJs; 6 | export const aes128CtrDecrypt = hasWebCrypto ? aes128CtrDecryptWebCrypto : aes128CtrDecryptJs; 7 | 8 | export async function aes128CtrEncryptJs(key: Uint8Array, iv: Uint8Array, data: Uint8Array): Promise { 9 | return await aesEncrypt( 10 | data, 11 | key, 12 | iv, 13 | "aes-128-ctr", 14 | false, 15 | ); 16 | } 17 | 18 | async function aes128CtrEncryptWebCrypto( 19 | key: Uint8Array, 20 | iv: Uint8Array, 21 | data: Uint8Array 22 | ): Promise { 23 | const cryptoKey = await crypto.subtle.importKey( 24 | "raw", 25 | key, 26 | {name: "AES-CTR"}, 27 | false, 28 | ["encrypt"] 29 | ); 30 | return new Uint8Array(await crypto.subtle.encrypt( 31 | { name: "AES-CTR", counter: iv, length: 128 }, 32 | cryptoKey, 33 | data 34 | )); 35 | } 36 | 37 | export async function aes128CtrDecryptJs(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Promise { 38 | return await aesDecrypt( 39 | ciphertext, 40 | key, 41 | iv, 42 | "aes-128-ctr", 43 | false, 44 | ); 45 | } 46 | 47 | async function aes128CtrDecryptWebCrypto(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Promise { 48 | const cryptoKey = await crypto.subtle.importKey( 49 | "raw", 50 | key, 51 | {name: "AES-CTR"}, 52 | false, 53 | ["decrypt"] 54 | ); 55 | return new Uint8Array(await crypto.subtle.decrypt( 56 | { name: "AES-CTR", counter: iv, length: 128 }, 57 | cryptoKey, 58 | ciphertext 59 | )); 60 | } 61 | -------------------------------------------------------------------------------- /src/cipher/index.ts: -------------------------------------------------------------------------------- 1 | import { getRandomBytesSync } from "ethereum-cryptography/random"; 2 | import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils"; 3 | 4 | import { ICipherModule } from "../types"; 5 | import { aes128CtrDecrypt, aes128CtrEncrypt } from "./aes128Ctr"; 6 | 7 | export function defaultAes128CtrModule(): Pick { 8 | return { 9 | function: "aes-128-ctr", 10 | params: { 11 | iv: bytesToHex(getRandomBytesSync(16)), 12 | }, 13 | }; 14 | } 15 | 16 | export async function cipherEncrypt(mod: ICipherModule, key: Uint8Array, data: Uint8Array): Promise { 17 | if (mod.function === "aes-128-ctr") { 18 | try { 19 | return await aes128CtrEncrypt(key, hexToBytes(mod.params.iv), data); 20 | } catch (e) { 21 | throw new Error("Unable to encrypt"); 22 | } 23 | } else { 24 | throw new Error("Invalid cipher type"); 25 | } 26 | } 27 | 28 | export async function cipherDecrypt(mod: ICipherModule, key: Uint8Array): Promise { 29 | if (mod.function === "aes-128-ctr") { 30 | try { 31 | return await aes128CtrDecrypt(key, hexToBytes(mod.params.iv), hexToBytes(mod.message)); 32 | } catch (e) { 33 | throw new Error("Unable to decrypt") 34 | } 35 | } else { 36 | throw new Error("Invalid cipher type"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/class.ts: -------------------------------------------------------------------------------- 1 | import { defaultPbkdfModule } from "./kdf"; 2 | import { defaultSha256Module } from "./checksum"; 3 | import { defaultAes128CtrModule } from "./cipher"; 4 | import { IKeystore, IKdfModule, IChecksumModule, ICipherModule } from "./types"; 5 | import { create, decrypt, verifyPassword } from "./functional"; 6 | import { validateKeystore } from "./schema-validation"; 7 | 8 | /** 9 | * Class-based BLS Keystore 10 | */ 11 | export class Keystore implements IKeystore { 12 | version: number; 13 | uuid: string; 14 | description?: string; 15 | path: string; 16 | pubkey: string; 17 | crypto: { 18 | kdf: IKdfModule; 19 | checksum: IChecksumModule; 20 | cipher: ICipherModule; 21 | }; 22 | 23 | constructor(obj: IKeystore) { 24 | this.version = obj.version; 25 | this.uuid = obj.uuid; 26 | this.description = obj.description; 27 | this.path = obj.path; 28 | this.pubkey = obj.pubkey; 29 | this.crypto = { 30 | kdf: {...obj.crypto.kdf}, 31 | checksum: {...obj.crypto.checksum}, 32 | cipher: {...obj.crypto.cipher}, 33 | } 34 | } 35 | 36 | /** 37 | * Create a new Keystore object 38 | */ 39 | static async create( 40 | password: string | Uint8Array, 41 | secret: Uint8Array, 42 | pubkey: Uint8Array, 43 | path: string, 44 | description: string | null = null, 45 | kdfMod: Pick = defaultPbkdfModule(), 46 | checksumMod: Pick = defaultSha256Module(), 47 | cipherMod: Pick = defaultAes128CtrModule(), 48 | ): Promise { 49 | const obj = await create( 50 | password, secret, pubkey, path, description, kdfMod, checksumMod, cipherMod, 51 | ); 52 | return new Keystore(obj) 53 | } 54 | 55 | /** 56 | * Create a keystore from an unknown object 57 | */ 58 | static fromObject(obj: unknown): Keystore { 59 | validateKeystore(obj); 60 | return new Keystore(obj); 61 | } 62 | 63 | /** 64 | * Parse a keystore from a JSON string 65 | */ 66 | static parse(str: string): Keystore { 67 | return Keystore.fromObject(JSON.parse(str)) 68 | } 69 | 70 | /** 71 | * Decrypt a keystore, returns the secret key or throws on invalid password 72 | */ 73 | async decrypt(password: string | Uint8Array): Promise { 74 | return decrypt(this, password); 75 | } 76 | 77 | /** 78 | * Verify the password as correct or not 79 | */ 80 | async verifyPassword(password: string | Uint8Array): Promise { 81 | return verifyPassword(this, password); 82 | } 83 | 84 | /** 85 | * Return the keystore as a plain object 86 | */ 87 | toObject(): IKeystore { 88 | return { 89 | ...this 90 | } 91 | } 92 | 93 | /** 94 | * Return the keystore as stringified JSON 95 | */ 96 | stringify(): string { 97 | return JSON.stringify(this.toObject(), null, 2); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | /** Is in nodejs environment */ 2 | export const isNode = 3 | typeof process !== "undefined" && 4 | process.versions != null && 5 | process.versions.node != null; 6 | 7 | /** Is in environment with web crypto */ 8 | export const hasWebCrypto = globalThis?.crypto?.subtle != null; 9 | -------------------------------------------------------------------------------- /src/functional.ts: -------------------------------------------------------------------------------- 1 | import {v4 as uuidV4} from "uuid"; 2 | 3 | import { IKeystore, IKdfModule, ICipherModule, IChecksumModule } from "./types"; 4 | import { kdf, defaultPbkdfModule, defaultScryptModule } from "./kdf"; 5 | import { checksum, verifyChecksum, defaultSha256Module } from "./checksum"; 6 | import { cipherEncrypt, cipherDecrypt, defaultAes128CtrModule } from "./cipher"; 7 | import { normalizePassword } from "./password"; 8 | import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils"; 9 | 10 | export { 11 | defaultPbkdfModule, 12 | defaultScryptModule, 13 | defaultSha256Module, 14 | defaultAes128CtrModule, 15 | } 16 | 17 | /** 18 | * Create a new keystore object 19 | * 20 | * @param password password used to encrypt the keystore 21 | * @param secret secret key material to be encrypted 22 | * @param pubkey public key, not checked for validity 23 | * @param path HD path used to generate the secret 24 | * @param kdfMod key derivation function (kdf) configuration module 25 | * @param checksumMod checksum configuration module 26 | * @param cipherMod cipher configuration module 27 | */ 28 | export async function create( 29 | password: string | Uint8Array, 30 | secret: Uint8Array, 31 | pubkey: Uint8Array, 32 | path: string, 33 | description: string | null = null, 34 | kdfMod: Pick = defaultPbkdfModule(), 35 | checksumMod: Pick = defaultSha256Module(), 36 | cipherMod: Pick = defaultAes128CtrModule(), 37 | ): Promise { 38 | const encryptionKey = await kdf(kdfMod as IKdfModule, normalizePassword(password)); 39 | const ciphertext = await cipherEncrypt(cipherMod as ICipherModule, encryptionKey.slice(0, 16), secret); 40 | return { 41 | version: 4, 42 | uuid: uuidV4(), 43 | description: description || undefined, 44 | path: path, 45 | pubkey: bytesToHex(pubkey), 46 | crypto: { 47 | kdf: { 48 | function: kdfMod.function, 49 | params: { 50 | ...kdfMod.params, 51 | }, 52 | message: "", 53 | } as IKdfModule, 54 | checksum: { 55 | function: checksumMod.function, 56 | params: {}, 57 | message: bytesToHex(await checksum(checksumMod as IChecksumModule, encryptionKey, ciphertext)), 58 | }, 59 | cipher: { 60 | function: cipherMod.function, 61 | params: { 62 | ...cipherMod.params, 63 | }, 64 | message: bytesToHex(ciphertext), 65 | }, 66 | }, 67 | }; 68 | } 69 | 70 | /** 71 | * Verify the password of a keystore object 72 | */ 73 | export async function verifyPassword(keystore: IKeystore, password: string | Uint8Array): Promise { 74 | const decryptionKey = await kdf(keystore.crypto.kdf, normalizePassword(password)); 75 | const ciphertext = hexToBytes(keystore.crypto.cipher.message); 76 | return verifyChecksum(keystore.crypto.checksum, decryptionKey, ciphertext); 77 | } 78 | 79 | /** 80 | * Decrypt a keystore, returns the secret key or throws on invalid password 81 | */ 82 | export async function decrypt(keystore: IKeystore, password: string | Uint8Array): Promise { 83 | const decryptionKey = await kdf(keystore.crypto.kdf, normalizePassword(password)); 84 | const ciphertext = hexToBytes(keystore.crypto.cipher.message); 85 | if (!(await verifyChecksum(keystore.crypto.checksum, decryptionKey, ciphertext))) { 86 | throw new Error("Invalid password"); 87 | } 88 | return cipherDecrypt(keystore.crypto.cipher, decryptionKey.slice(0, 16)); 89 | } 90 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./functional"; 3 | export * from "./class"; 4 | export * from "./schema-validation"; 5 | -------------------------------------------------------------------------------- /src/kdf/index.ts: -------------------------------------------------------------------------------- 1 | import { getRandomBytesSync } from "ethereum-cryptography/random"; 2 | import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils" 3 | 4 | import { IKdfModule, IPbkdf2KdfModule, IScryptKdfModule } from "../types"; 5 | import { doPbkdf2 } from "./pbkdf2"; 6 | import { doScrypt } from "./scrypt"; 7 | 8 | // default kdf configurations 9 | 10 | export function defaultPbkdfModule(): Pick { 11 | return { 12 | function: "pbkdf2", 13 | params: { 14 | dklen: 32, 15 | c: 262144, 16 | prf: "hmac-sha256", 17 | salt: bytesToHex(getRandomBytesSync(32)), 18 | }, 19 | }; 20 | } 21 | 22 | export function defaultScryptModule(): Pick { 23 | return { 24 | function: "scrypt", 25 | params: { 26 | dklen: 32, 27 | n: 262144, 28 | r: 8, 29 | p: 1, 30 | salt: bytesToHex(getRandomBytesSync(32)), 31 | }, 32 | }; 33 | } 34 | 35 | // kdf operations 36 | 37 | export async function kdf(mod: IKdfModule, password: Uint8Array): Promise { 38 | if (mod.function === "pbkdf2") { 39 | const { salt, c, dklen } = mod.params; 40 | return await doPbkdf2(hexToBytes(salt), c, dklen, password); 41 | } else if (mod.function === "scrypt") { 42 | const { salt, n, p, r, dklen } = mod.params; 43 | return await doScrypt(hexToBytes(salt), n, p, r, dklen, password); 44 | } else { 45 | throw new Error("Invalid kdf type"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/kdf/pbkdf2.ts: -------------------------------------------------------------------------------- 1 | import { pbkdf2 } from "ethereum-cryptography/pbkdf2"; 2 | 3 | import { hasWebCrypto, isNode } from "../env"; 4 | 5 | export const doPbkdf2 = isNode ? doPbkdf2Node : hasWebCrypto ? doPbkdf2WebCrypto : doPbkdf2Js; 6 | 7 | async function doPbkdf2Js(salt: Uint8Array, c: number, dklen: number, password: Uint8Array): Promise { 8 | return pbkdf2( 9 | password, 10 | salt, 11 | c, 12 | dklen, 13 | "sha256", 14 | ); 15 | } 16 | 17 | async function doPbkdf2Node(salt: Uint8Array, c: number, dklen: number, password: Uint8Array): Promise { 18 | const crypto = await import("crypto"); 19 | return crypto.pbkdf2Sync(password, salt, c, dklen, "sha256"); 20 | } 21 | 22 | async function doPbkdf2WebCrypto(salt: Uint8Array, c: number, dklen: number, password: Uint8Array): Promise { 23 | const passwordKey = await crypto.subtle.importKey( 24 | "raw", 25 | password, 26 | {name: "PBKDF2"}, 27 | false, 28 | ["deriveBits"], 29 | ); 30 | return new Uint8Array(await crypto.subtle.deriveBits( 31 | { 32 | name: "PBKDF2", 33 | salt, 34 | iterations: c, 35 | hash: "SHA-256", 36 | }, 37 | passwordKey, 38 | dklen * 8, 39 | )); 40 | } 41 | -------------------------------------------------------------------------------- /src/kdf/scrypt.ts: -------------------------------------------------------------------------------- 1 | import { scrypt } from "ethereum-cryptography/scrypt"; 2 | 3 | import { isNode } from "../env"; 4 | 5 | 6 | export const doScrypt = isNode ? doScryptNode : doScryptJs; 7 | 8 | async function doScryptJs(salt: Uint8Array, n: number, p: number, r: number, dklen: number, password: Uint8Array): Promise { 9 | return scrypt( 10 | password, 11 | salt, 12 | n, 13 | p, 14 | r, 15 | dklen, 16 | ); 17 | } 18 | 19 | async function doScryptNode(salt: Uint8Array, n: number, p: number, r: number, dklen: number, password: Uint8Array): Promise { 20 | const crypto = await import("crypto"); 21 | return crypto.scryptSync( 22 | password, 23 | salt, 24 | dklen, { 25 | N: n, 26 | r, 27 | p, 28 | maxmem: n * r * 256, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/password.ts: -------------------------------------------------------------------------------- 1 | import { utf8ToBytes } from "ethereum-cryptography/utils"; 2 | 3 | /** 4 | * Normalizes password to NFKD representation and strips the C0, C1, and Delete control codes. 5 | * C0 are the control codes between 0x00 - 0x1F (inclusive) 6 | * C1 codes lie between 0x80 and 0x9F (inclusive) 7 | * 8 | * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2335.md#password-requirements 9 | */ 10 | export function normalizePassword(password: string | Uint8Array): Uint8Array { 11 | if (typeof password === "string") { 12 | return utf8ToBytes( 13 | password 14 | .normalize("NFKD") 15 | .split("") 16 | .filter(char => controlCodeFilter(char.charCodeAt(0))).join("")); 17 | } else { 18 | return password.filter(controlCodeFilter); 19 | } 20 | } 21 | 22 | 23 | function controlCodeFilter(charCode: number): boolean { 24 | return (charCode > 0x1F) && !(charCode >= 0x7f && charCode <= 0x9F) 25 | } -------------------------------------------------------------------------------- /src/schema-validation-generated.ts: -------------------------------------------------------------------------------- 1 | 2 | // This file was generated by /scripts/schema-validation-codegen.ts 3 | // Do not modify this file by hand. 4 | 5 | /* eslint-disable */ 6 | // @ts-ignore 7 | "use strict";export const Keystore = validate19;const schema12 = {"type":"object","properties":{"crypto":{"type":"object","properties":{"kdf":{"$ref":"#/definitions/KdfModule"},"checksum":{"$ref":"#/definitions/ChecksumModule"},"cipher":{"$ref":"#/definitions/CipherModule"}},"required":["kdf","checksum","cipher"]},"pubkey":{"type":"string","pattern":"^([A-Fa-f0-9]{2}){48}$"},"path":{"type":"string"},"description":{"type":"string"},"uuid":{"type":"string","format":"uuid"},"version":{"type":"integer","minimum":4,"maximum":4}},"required":["crypto","path","uuid","version"],"title":"Keystore"};const schema13 = {"allOf":[{"$ref":"#/definitions/Module"},{"oneOf":[{"$ref":"#/definitions/Pbkdf2Module"},{"$ref":"#/definitions/ScryptModule"}]}]};const schema14 = {"type":"object","properties":{"function":{"type":"string"},"params":{"type":"object"},"message":{"type":"string"}},"required":["function","message","params"]};const schema15 = {"type":"object","properties":{"function":{"type":"string","pattern":"^pbkdf2$"},"params":{"type":"object","properties":{"dklen":{"type":"integer","minimum":0},"c":{"type":"integer","minimum":0},"prf":{"type":"string","pattern":"^hmac-sha256$"},"salt":{"type":"string"}},"required":["dklen","c","prf","salt"]},"message":{"type":"string","pattern":"^$"}}};const schema16 = {"type":"object","properties":{"function":{"type":"string","pattern":"^scrypt$"},"params":{"type":"object","properties":{"dklen":{"type":"integer","minimum":0},"n":{"type":"integer","minimum":0},"p":{"type":"integer","minimum":0},"r":{"type":"integer","minimum":0},"salt":{"type":"string","pattern":"^([A-Fa-f0-9]{2}){32}$"}},"required":["dklen","n","p","r","salt"]},"message":{"type":"string","pattern":"^$"}}};const pattern0 = new RegExp("^pbkdf2$", "u");const pattern1 = new RegExp("^hmac-sha256$", "u");const pattern2 = new RegExp("^$", "u");const pattern3 = new RegExp("^scrypt$", "u");const pattern4 = new RegExp("^([A-Fa-f0-9]{2}){32}$", "u");function validate12(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;const _errs0 = errors;const _errs1 = errors;if(errors === _errs1){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((data.function === undefined) && (missing0 = "function")) || ((data.message === undefined) && (missing0 = "message"))) || ((data.params === undefined) && (missing0 = "params"))){validate12.errors = [{instancePath,schemaPath:"#/definitions/Module/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.function !== undefined){const _errs3 = errors;if(typeof data.function !== "string"){validate12.errors = [{instancePath:instancePath+"/function",schemaPath:"#/definitions/Module/properties/function/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid2 = _errs3 === errors;}else {var valid2 = true;}if(valid2){if(data.params !== undefined){let data1 = data.params;const _errs5 = errors;if(!(data1 && typeof data1 == "object" && !Array.isArray(data1))){validate12.errors = [{instancePath:instancePath+"/params",schemaPath:"#/definitions/Module/properties/params/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid2 = _errs5 === errors;}else {var valid2 = true;}if(valid2){if(data.message !== undefined){const _errs7 = errors;if(typeof data.message !== "string"){validate12.errors = [{instancePath:instancePath+"/message",schemaPath:"#/definitions/Module/properties/message/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid2 = _errs7 === errors;}else {var valid2 = true;}}}}}else {validate12.errors = [{instancePath,schemaPath:"#/definitions/Module/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs0 === errors;if(valid0){const _errs9 = errors;const _errs10 = errors;let valid3 = false;let passing0 = null;const _errs11 = errors;const _errs12 = errors;if(errors === _errs12){if(data && typeof data == "object" && !Array.isArray(data)){if(data.function !== undefined){let data3 = data.function;const _errs14 = errors;if(errors === _errs14){if(typeof data3 === "string"){if(!pattern0.test(data3)){const err0 = {instancePath:instancePath+"/function",schemaPath:"#/definitions/Pbkdf2Module/properties/function/pattern",keyword:"pattern",params:{pattern: "^pbkdf2$"},message:"must match pattern \""+"^pbkdf2$"+"\""};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}else {const err1 = {instancePath:instancePath+"/function",schemaPath:"#/definitions/Pbkdf2Module/properties/function/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}var valid5 = _errs14 === errors;}else {var valid5 = true;}if(valid5){if(data.params !== undefined){let data4 = data.params;const _errs16 = errors;if(errors === _errs16){if(data4 && typeof data4 == "object" && !Array.isArray(data4)){let missing1;if(((((data4.dklen === undefined) && (missing1 = "dklen")) || ((data4.c === undefined) && (missing1 = "c"))) || ((data4.prf === undefined) && (missing1 = "prf"))) || ((data4.salt === undefined) && (missing1 = "salt"))){const err2 = {instancePath:instancePath+"/params",schemaPath:"#/definitions/Pbkdf2Module/properties/params/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}else {if(data4.dklen !== undefined){let data5 = data4.dklen;const _errs18 = errors;if(!(((typeof data5 == "number") && (!(data5 % 1) && !isNaN(data5))) && (isFinite(data5)))){const err3 = {instancePath:instancePath+"/params/dklen",schemaPath:"#/definitions/Pbkdf2Module/properties/params/properties/dklen/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}if(errors === _errs18){if((typeof data5 == "number") && (isFinite(data5))){if(data5 < 0 || isNaN(data5)){const err4 = {instancePath:instancePath+"/params/dklen",schemaPath:"#/definitions/Pbkdf2Module/properties/params/properties/dklen/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}}var valid6 = _errs18 === errors;}else {var valid6 = true;}if(valid6){if(data4.c !== undefined){let data6 = data4.c;const _errs20 = errors;if(!(((typeof data6 == "number") && (!(data6 % 1) && !isNaN(data6))) && (isFinite(data6)))){const err5 = {instancePath:instancePath+"/params/c",schemaPath:"#/definitions/Pbkdf2Module/properties/params/properties/c/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}if(errors === _errs20){if((typeof data6 == "number") && (isFinite(data6))){if(data6 < 0 || isNaN(data6)){const err6 = {instancePath:instancePath+"/params/c",schemaPath:"#/definitions/Pbkdf2Module/properties/params/properties/c/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}}var valid6 = _errs20 === errors;}else {var valid6 = true;}if(valid6){if(data4.prf !== undefined){let data7 = data4.prf;const _errs22 = errors;if(errors === _errs22){if(typeof data7 === "string"){if(!pattern1.test(data7)){const err7 = {instancePath:instancePath+"/params/prf",schemaPath:"#/definitions/Pbkdf2Module/properties/params/properties/prf/pattern",keyword:"pattern",params:{pattern: "^hmac-sha256$"},message:"must match pattern \""+"^hmac-sha256$"+"\""};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}else {const err8 = {instancePath:instancePath+"/params/prf",schemaPath:"#/definitions/Pbkdf2Module/properties/params/properties/prf/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}var valid6 = _errs22 === errors;}else {var valid6 = true;}if(valid6){if(data4.salt !== undefined){const _errs24 = errors;if(typeof data4.salt !== "string"){const err9 = {instancePath:instancePath+"/params/salt",schemaPath:"#/definitions/Pbkdf2Module/properties/params/properties/salt/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}var valid6 = _errs24 === errors;}else {var valid6 = true;}}}}}}else {const err10 = {instancePath:instancePath+"/params",schemaPath:"#/definitions/Pbkdf2Module/properties/params/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}}var valid5 = _errs16 === errors;}else {var valid5 = true;}if(valid5){if(data.message !== undefined){let data9 = data.message;const _errs26 = errors;if(errors === _errs26){if(typeof data9 === "string"){if(!pattern2.test(data9)){const err11 = {instancePath:instancePath+"/message",schemaPath:"#/definitions/Pbkdf2Module/properties/message/pattern",keyword:"pattern",params:{pattern: "^$"},message:"must match pattern \""+"^$"+"\""};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}}else {const err12 = {instancePath:instancePath+"/message",schemaPath:"#/definitions/Pbkdf2Module/properties/message/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}}var valid5 = _errs26 === errors;}else {var valid5 = true;}}}}else {const err13 = {instancePath,schemaPath:"#/definitions/Pbkdf2Module/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}}var _valid0 = _errs11 === errors;if(_valid0){valid3 = true;passing0 = 0;}const _errs28 = errors;const _errs29 = errors;if(errors === _errs29){if(data && typeof data == "object" && !Array.isArray(data)){if(data.function !== undefined){let data10 = data.function;const _errs31 = errors;if(errors === _errs31){if(typeof data10 === "string"){if(!pattern3.test(data10)){const err14 = {instancePath:instancePath+"/function",schemaPath:"#/definitions/ScryptModule/properties/function/pattern",keyword:"pattern",params:{pattern: "^scrypt$"},message:"must match pattern \""+"^scrypt$"+"\""};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}else {const err15 = {instancePath:instancePath+"/function",schemaPath:"#/definitions/ScryptModule/properties/function/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}var valid8 = _errs31 === errors;}else {var valid8 = true;}if(valid8){if(data.params !== undefined){let data11 = data.params;const _errs33 = errors;if(errors === _errs33){if(data11 && typeof data11 == "object" && !Array.isArray(data11)){let missing2;if((((((data11.dklen === undefined) && (missing2 = "dklen")) || ((data11.n === undefined) && (missing2 = "n"))) || ((data11.p === undefined) && (missing2 = "p"))) || ((data11.r === undefined) && (missing2 = "r"))) || ((data11.salt === undefined) && (missing2 = "salt"))){const err16 = {instancePath:instancePath+"/params",schemaPath:"#/definitions/ScryptModule/properties/params/required",keyword:"required",params:{missingProperty: missing2},message:"must have required property '"+missing2+"'"};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}else {if(data11.dklen !== undefined){let data12 = data11.dklen;const _errs35 = errors;if(!(((typeof data12 == "number") && (!(data12 % 1) && !isNaN(data12))) && (isFinite(data12)))){const err17 = {instancePath:instancePath+"/params/dklen",schemaPath:"#/definitions/ScryptModule/properties/params/properties/dklen/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}if(errors === _errs35){if((typeof data12 == "number") && (isFinite(data12))){if(data12 < 0 || isNaN(data12)){const err18 = {instancePath:instancePath+"/params/dklen",schemaPath:"#/definitions/ScryptModule/properties/params/properties/dklen/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}}}var valid9 = _errs35 === errors;}else {var valid9 = true;}if(valid9){if(data11.n !== undefined){let data13 = data11.n;const _errs37 = errors;if(!(((typeof data13 == "number") && (!(data13 % 1) && !isNaN(data13))) && (isFinite(data13)))){const err19 = {instancePath:instancePath+"/params/n",schemaPath:"#/definitions/ScryptModule/properties/params/properties/n/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}if(errors === _errs37){if((typeof data13 == "number") && (isFinite(data13))){if(data13 < 0 || isNaN(data13)){const err20 = {instancePath:instancePath+"/params/n",schemaPath:"#/definitions/ScryptModule/properties/params/properties/n/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}}var valid9 = _errs37 === errors;}else {var valid9 = true;}if(valid9){if(data11.p !== undefined){let data14 = data11.p;const _errs39 = errors;if(!(((typeof data14 == "number") && (!(data14 % 1) && !isNaN(data14))) && (isFinite(data14)))){const err21 = {instancePath:instancePath+"/params/p",schemaPath:"#/definitions/ScryptModule/properties/params/properties/p/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}if(errors === _errs39){if((typeof data14 == "number") && (isFinite(data14))){if(data14 < 0 || isNaN(data14)){const err22 = {instancePath:instancePath+"/params/p",schemaPath:"#/definitions/ScryptModule/properties/params/properties/p/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}}}var valid9 = _errs39 === errors;}else {var valid9 = true;}if(valid9){if(data11.r !== undefined){let data15 = data11.r;const _errs41 = errors;if(!(((typeof data15 == "number") && (!(data15 % 1) && !isNaN(data15))) && (isFinite(data15)))){const err23 = {instancePath:instancePath+"/params/r",schemaPath:"#/definitions/ScryptModule/properties/params/properties/r/type",keyword:"type",params:{type: "integer"},message:"must be integer"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}if(errors === _errs41){if((typeof data15 == "number") && (isFinite(data15))){if(data15 < 0 || isNaN(data15)){const err24 = {instancePath:instancePath+"/params/r",schemaPath:"#/definitions/ScryptModule/properties/params/properties/r/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}}var valid9 = _errs41 === errors;}else {var valid9 = true;}if(valid9){if(data11.salt !== undefined){let data16 = data11.salt;const _errs43 = errors;if(errors === _errs43){if(typeof data16 === "string"){if(!pattern4.test(data16)){const err25 = {instancePath:instancePath+"/params/salt",schemaPath:"#/definitions/ScryptModule/properties/params/properties/salt/pattern",keyword:"pattern",params:{pattern: "^([A-Fa-f0-9]{2}){32}$"},message:"must match pattern \""+"^([A-Fa-f0-9]{2}){32}$"+"\""};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}}else {const err26 = {instancePath:instancePath+"/params/salt",schemaPath:"#/definitions/ScryptModule/properties/params/properties/salt/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}var valid9 = _errs43 === errors;}else {var valid9 = true;}}}}}}}else {const err27 = {instancePath:instancePath+"/params",schemaPath:"#/definitions/ScryptModule/properties/params/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}var valid8 = _errs33 === errors;}else {var valid8 = true;}if(valid8){if(data.message !== undefined){let data17 = data.message;const _errs45 = errors;if(errors === _errs45){if(typeof data17 === "string"){if(!pattern2.test(data17)){const err28 = {instancePath:instancePath+"/message",schemaPath:"#/definitions/ScryptModule/properties/message/pattern",keyword:"pattern",params:{pattern: "^$"},message:"must match pattern \""+"^$"+"\""};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}}else {const err29 = {instancePath:instancePath+"/message",schemaPath:"#/definitions/ScryptModule/properties/message/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}}var valid8 = _errs45 === errors;}else {var valid8 = true;}}}}else {const err30 = {instancePath,schemaPath:"#/definitions/ScryptModule/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}}var _valid0 = _errs28 === errors;if(_valid0 && valid3){valid3 = false;passing0 = [passing0, 1];}else {if(_valid0){valid3 = true;passing0 = 1;}}if(!valid3){const err31 = {instancePath,schemaPath:"#/allOf/1/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;validate12.errors = vErrors;return false;}else {errors = _errs10;if(vErrors !== null){if(_errs10){vErrors.length = _errs10;}else {vErrors = null;}}}var valid0 = _errs9 === errors;}validate12.errors = vErrors;return errors === 0;}const schema17 = {"allOf":[{"$ref":"#/definitions/Module"},{"oneOf":[{"$ref":"#/definitions/Sha2Module"}]}]};const schema19 = {"type":"object","properties":{"function":{"type":"string","pattern":"^sha256$"},"params":{"type":"object","additionalProperties":false},"message":{"type":"string","pattern":"^([A-Fa-f0-9]{2}){32}$"}}};const pattern6 = new RegExp("^sha256$", "u");function validate14(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;const _errs0 = errors;const _errs1 = errors;if(errors === _errs1){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((data.function === undefined) && (missing0 = "function")) || ((data.message === undefined) && (missing0 = "message"))) || ((data.params === undefined) && (missing0 = "params"))){validate14.errors = [{instancePath,schemaPath:"#/definitions/Module/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.function !== undefined){const _errs3 = errors;if(typeof data.function !== "string"){validate14.errors = [{instancePath:instancePath+"/function",schemaPath:"#/definitions/Module/properties/function/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid2 = _errs3 === errors;}else {var valid2 = true;}if(valid2){if(data.params !== undefined){let data1 = data.params;const _errs5 = errors;if(!(data1 && typeof data1 == "object" && !Array.isArray(data1))){validate14.errors = [{instancePath:instancePath+"/params",schemaPath:"#/definitions/Module/properties/params/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid2 = _errs5 === errors;}else {var valid2 = true;}if(valid2){if(data.message !== undefined){const _errs7 = errors;if(typeof data.message !== "string"){validate14.errors = [{instancePath:instancePath+"/message",schemaPath:"#/definitions/Module/properties/message/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid2 = _errs7 === errors;}else {var valid2 = true;}}}}}else {validate14.errors = [{instancePath,schemaPath:"#/definitions/Module/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs0 === errors;if(valid0){const _errs9 = errors;const _errs10 = errors;let valid3 = false;let passing0 = null;const _errs11 = errors;const _errs12 = errors;if(errors === _errs12){if(data && typeof data == "object" && !Array.isArray(data)){if(data.function !== undefined){let data3 = data.function;const _errs14 = errors;if(errors === _errs14){if(typeof data3 === "string"){if(!pattern6.test(data3)){const err0 = {instancePath:instancePath+"/function",schemaPath:"#/definitions/Sha2Module/properties/function/pattern",keyword:"pattern",params:{pattern: "^sha256$"},message:"must match pattern \""+"^sha256$"+"\""};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}else {const err1 = {instancePath:instancePath+"/function",schemaPath:"#/definitions/Sha2Module/properties/function/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}var valid5 = _errs14 === errors;}else {var valid5 = true;}if(valid5){if(data.params !== undefined){let data4 = data.params;const _errs16 = errors;if(errors === _errs16){if(data4 && typeof data4 == "object" && !Array.isArray(data4)){for(const key0 in data4){const err2 = {instancePath:instancePath+"/params",schemaPath:"#/definitions/Sha2Module/properties/params/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;break;}}else {const err3 = {instancePath:instancePath+"/params",schemaPath:"#/definitions/Sha2Module/properties/params/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}var valid5 = _errs16 === errors;}else {var valid5 = true;}if(valid5){if(data.message !== undefined){let data5 = data.message;const _errs19 = errors;if(errors === _errs19){if(typeof data5 === "string"){if(!pattern4.test(data5)){const err4 = {instancePath:instancePath+"/message",schemaPath:"#/definitions/Sha2Module/properties/message/pattern",keyword:"pattern",params:{pattern: "^([A-Fa-f0-9]{2}){32}$"},message:"must match pattern \""+"^([A-Fa-f0-9]{2}){32}$"+"\""};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}else {const err5 = {instancePath:instancePath+"/message",schemaPath:"#/definitions/Sha2Module/properties/message/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}}var valid5 = _errs19 === errors;}else {var valid5 = true;}}}}else {const err6 = {instancePath,schemaPath:"#/definitions/Sha2Module/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}var _valid0 = _errs11 === errors;if(_valid0){valid3 = true;passing0 = 0;}if(!valid3){const err7 = {instancePath,schemaPath:"#/allOf/1/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;validate14.errors = vErrors;return false;}else {errors = _errs10;if(vErrors !== null){if(_errs10){vErrors.length = _errs10;}else {vErrors = null;}}}var valid0 = _errs9 === errors;}validate14.errors = vErrors;return errors === 0;}const schema20 = {"allOf":[{"$ref":"#/definitions/Module"},{"oneOf":[{"$ref":"#/definitions/Aes128CtrModule"}]}]};const schema22 = {"type":"object","properties":{"function":{"type":"string","pattern":"^aes-128-ctr$"},"params":{"type":"object","properties":{"iv":{"type":"string","pattern":"^([A-Fa-f0-9]{2}){16}$"}},"required":["iv"]},"message":{"type":"string","pattern":"^([A-Fa-f0-9]{2}){32}$"}}};const pattern8 = new RegExp("^aes-128-ctr$", "u");const pattern9 = new RegExp("^([A-Fa-f0-9]{2}){16}$", "u");function validate16(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;const _errs0 = errors;const _errs1 = errors;if(errors === _errs1){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((((data.function === undefined) && (missing0 = "function")) || ((data.message === undefined) && (missing0 = "message"))) || ((data.params === undefined) && (missing0 = "params"))){validate16.errors = [{instancePath,schemaPath:"#/definitions/Module/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.function !== undefined){const _errs3 = errors;if(typeof data.function !== "string"){validate16.errors = [{instancePath:instancePath+"/function",schemaPath:"#/definitions/Module/properties/function/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid2 = _errs3 === errors;}else {var valid2 = true;}if(valid2){if(data.params !== undefined){let data1 = data.params;const _errs5 = errors;if(!(data1 && typeof data1 == "object" && !Array.isArray(data1))){validate16.errors = [{instancePath:instancePath+"/params",schemaPath:"#/definitions/Module/properties/params/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid2 = _errs5 === errors;}else {var valid2 = true;}if(valid2){if(data.message !== undefined){const _errs7 = errors;if(typeof data.message !== "string"){validate16.errors = [{instancePath:instancePath+"/message",schemaPath:"#/definitions/Module/properties/message/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid2 = _errs7 === errors;}else {var valid2 = true;}}}}}else {validate16.errors = [{instancePath,schemaPath:"#/definitions/Module/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs0 === errors;if(valid0){const _errs9 = errors;const _errs10 = errors;let valid3 = false;let passing0 = null;const _errs11 = errors;const _errs12 = errors;if(errors === _errs12){if(data && typeof data == "object" && !Array.isArray(data)){if(data.function !== undefined){let data3 = data.function;const _errs14 = errors;if(errors === _errs14){if(typeof data3 === "string"){if(!pattern8.test(data3)){const err0 = {instancePath:instancePath+"/function",schemaPath:"#/definitions/Aes128CtrModule/properties/function/pattern",keyword:"pattern",params:{pattern: "^aes-128-ctr$"},message:"must match pattern \""+"^aes-128-ctr$"+"\""};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}else {const err1 = {instancePath:instancePath+"/function",schemaPath:"#/definitions/Aes128CtrModule/properties/function/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}var valid5 = _errs14 === errors;}else {var valid5 = true;}if(valid5){if(data.params !== undefined){let data4 = data.params;const _errs16 = errors;if(errors === _errs16){if(data4 && typeof data4 == "object" && !Array.isArray(data4)){let missing1;if((data4.iv === undefined) && (missing1 = "iv")){const err2 = {instancePath:instancePath+"/params",schemaPath:"#/definitions/Aes128CtrModule/properties/params/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}else {if(data4.iv !== undefined){let data5 = data4.iv;const _errs18 = errors;if(errors === _errs18){if(typeof data5 === "string"){if(!pattern9.test(data5)){const err3 = {instancePath:instancePath+"/params/iv",schemaPath:"#/definitions/Aes128CtrModule/properties/params/properties/iv/pattern",keyword:"pattern",params:{pattern: "^([A-Fa-f0-9]{2}){16}$"},message:"must match pattern \""+"^([A-Fa-f0-9]{2}){16}$"+"\""};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}else {const err4 = {instancePath:instancePath+"/params/iv",schemaPath:"#/definitions/Aes128CtrModule/properties/params/properties/iv/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}}}}else {const err5 = {instancePath:instancePath+"/params",schemaPath:"#/definitions/Aes128CtrModule/properties/params/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}}var valid5 = _errs16 === errors;}else {var valid5 = true;}if(valid5){if(data.message !== undefined){let data6 = data.message;const _errs20 = errors;if(errors === _errs20){if(typeof data6 === "string"){if(!pattern4.test(data6)){const err6 = {instancePath:instancePath+"/message",schemaPath:"#/definitions/Aes128CtrModule/properties/message/pattern",keyword:"pattern",params:{pattern: "^([A-Fa-f0-9]{2}){32}$"},message:"must match pattern \""+"^([A-Fa-f0-9]{2}){32}$"+"\""};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}else {const err7 = {instancePath:instancePath+"/message",schemaPath:"#/definitions/Aes128CtrModule/properties/message/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}var valid5 = _errs20 === errors;}else {var valid5 = true;}}}}else {const err8 = {instancePath,schemaPath:"#/definitions/Aes128CtrModule/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}var _valid0 = _errs11 === errors;if(_valid0){valid3 = true;passing0 = 0;}if(!valid3){const err9 = {instancePath,schemaPath:"#/allOf/1/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;validate16.errors = vErrors;return false;}else {errors = _errs10;if(vErrors !== null){if(_errs10){vErrors.length = _errs10;}else {vErrors = null;}}}var valid0 = _errs9 === errors;}validate16.errors = vErrors;return errors === 0;}const pattern11 = new RegExp("^([A-Fa-f0-9]{2}){48}$", "u");const formats0 = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;function validate19(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((((data.crypto === undefined) && (missing0 = "crypto")) || ((data.path === undefined) && (missing0 = "path"))) || ((data.uuid === undefined) && (missing0 = "uuid"))) || ((data.version === undefined) && (missing0 = "version"))){validate19.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.crypto !== undefined){let data0 = data.crypto;const _errs1 = errors;if(errors === _errs1){if(data0 && typeof data0 == "object" && !Array.isArray(data0)){let missing1;if((((data0.kdf === undefined) && (missing1 = "kdf")) || ((data0.checksum === undefined) && (missing1 = "checksum"))) || ((data0.cipher === undefined) && (missing1 = "cipher"))){validate19.errors = [{instancePath:instancePath+"/crypto",schemaPath:"#/properties/crypto/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}];return false;}else {if(data0.kdf !== undefined){const _errs3 = errors;if(!(validate12(data0.kdf, {instancePath:instancePath+"/crypto/kdf",parentData:data0,parentDataProperty:"kdf",rootData}))){vErrors = vErrors === null ? validate12.errors : vErrors.concat(validate12.errors);errors = vErrors.length;}var valid1 = _errs3 === errors;}else {var valid1 = true;}if(valid1){if(data0.checksum !== undefined){const _errs4 = errors;if(!(validate14(data0.checksum, {instancePath:instancePath+"/crypto/checksum",parentData:data0,parentDataProperty:"checksum",rootData}))){vErrors = vErrors === null ? validate14.errors : vErrors.concat(validate14.errors);errors = vErrors.length;}var valid1 = _errs4 === errors;}else {var valid1 = true;}if(valid1){if(data0.cipher !== undefined){const _errs5 = errors;if(!(validate16(data0.cipher, {instancePath:instancePath+"/crypto/cipher",parentData:data0,parentDataProperty:"cipher",rootData}))){vErrors = vErrors === null ? validate16.errors : vErrors.concat(validate16.errors);errors = vErrors.length;}var valid1 = _errs5 === errors;}else {var valid1 = true;}}}}}else {validate19.errors = [{instancePath:instancePath+"/crypto",schemaPath:"#/properties/crypto/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}var valid0 = _errs1 === errors;}else {var valid0 = true;}if(valid0){if(data.pubkey !== undefined){let data4 = data.pubkey;const _errs6 = errors;if(errors === _errs6){if(typeof data4 === "string"){if(!pattern11.test(data4)){validate19.errors = [{instancePath:instancePath+"/pubkey",schemaPath:"#/properties/pubkey/pattern",keyword:"pattern",params:{pattern: "^([A-Fa-f0-9]{2}){48}$"},message:"must match pattern \""+"^([A-Fa-f0-9]{2}){48}$"+"\""}];return false;}}else {validate19.errors = [{instancePath:instancePath+"/pubkey",schemaPath:"#/properties/pubkey/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}var valid0 = _errs6 === errors;}else {var valid0 = true;}if(valid0){if(data.path !== undefined){const _errs8 = errors;if(typeof data.path !== "string"){validate19.errors = [{instancePath:instancePath+"/path",schemaPath:"#/properties/path/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs8 === errors;}else {var valid0 = true;}if(valid0){if(data.description !== undefined){const _errs10 = errors;if(typeof data.description !== "string"){validate19.errors = [{instancePath:instancePath+"/description",schemaPath:"#/properties/description/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs10 === errors;}else {var valid0 = true;}if(valid0){if(data.uuid !== undefined){let data7 = data.uuid;const _errs12 = errors;if(errors === _errs12){if(errors === _errs12){if(typeof data7 === "string"){if(!(formats0.test(data7))){validate19.errors = [{instancePath:instancePath+"/uuid",schemaPath:"#/properties/uuid/format",keyword:"format",params:{format: "uuid"},message:"must match format \""+"uuid"+"\""}];return false;}}else {validate19.errors = [{instancePath:instancePath+"/uuid",schemaPath:"#/properties/uuid/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}}}var valid0 = _errs12 === errors;}else {var valid0 = true;}if(valid0){if(data.version !== undefined){let data8 = data.version;const _errs14 = errors;if(!(((typeof data8 == "number") && (!(data8 % 1) && !isNaN(data8))) && (isFinite(data8)))){validate19.errors = [{instancePath:instancePath+"/version",schemaPath:"#/properties/version/type",keyword:"type",params:{type: "integer"},message:"must be integer"}];return false;}if(errors === _errs14){if((typeof data8 == "number") && (isFinite(data8))){if(data8 > 4 || isNaN(data8)){validate19.errors = [{instancePath:instancePath+"/version",schemaPath:"#/properties/version/maximum",keyword:"maximum",params:{comparison: "<=", limit: 4},message:"must be <= 4"}];return false;}else {if(data8 < 4 || isNaN(data8)){validate19.errors = [{instancePath:instancePath+"/version",schemaPath:"#/properties/version/minimum",keyword:"minimum",params:{comparison: ">=", limit: 4},message:"must be >= 4"}];return false;}}}}var valid0 = _errs14 === errors;}else {var valid0 = true;}}}}}}}}else {validate19.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate19.errors = vErrors;return errors === 0;} -------------------------------------------------------------------------------- /src/schema-validation.ts: -------------------------------------------------------------------------------- 1 | import {Keystore as _validateKeystoreGenerated} from "./schema-validation-generated"; 2 | 3 | import { IKeystore } from "./types"; 4 | 5 | type ErrorObject = { 6 | instancePath: string; 7 | schemaPath: string; 8 | keyword: string; 9 | params: object; 10 | message: string; 11 | }; 12 | 13 | // Redeclare generated function with the proper type 14 | const _validateKeystore = _validateKeystoreGenerated as (((data: unknown) => boolean) & {errors: ErrorObject[]}); 15 | 16 | /** 17 | * Return schema validation errors for a potential keystore object 18 | */ 19 | // This function wraps the generated code weirdness 20 | export function schemaValidationErrors(data: unknown): ErrorObject[] | null { 21 | const validated = _validateKeystore(data) 22 | if (validated) { 23 | return null; 24 | } 25 | return _validateKeystore.errors; 26 | } 27 | 28 | /** 29 | * Validate an unknown object as a valid keystore, throws on invalid keystore 30 | */ 31 | export function validateKeystore(keystore: unknown): asserts keystore is IKeystore { 32 | const errors = schemaValidationErrors(keystore); 33 | if (errors) { 34 | throw new Error( 35 | errors.map((error) => `${error.instancePath}: ${error.message}`).join('\n') 36 | ); 37 | } 38 | } 39 | 40 | /** 41 | * Predicate for validating an unknown object as a valid keystore 42 | */ 43 | export function isValidKeystore(keystore: unknown): keystore is IKeystore { 44 | return !schemaValidationErrors(keystore); 45 | } 46 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Keystore object is broken into several parts: 3 | * metadata, kdf, checksum, and cipher 4 | */ 5 | export interface IKeystore { 6 | version: number; 7 | uuid: string; 8 | description?: string; 9 | path: string; 10 | pubkey: string; 11 | crypto: { 12 | kdf: IKdfModule; 13 | checksum: IChecksumModule; 14 | cipher: ICipherModule; 15 | }; 16 | } 17 | 18 | // kdf 19 | 20 | export type IKdfModule = IPbkdf2KdfModule | IScryptKdfModule; 21 | 22 | export interface IPbkdf2KdfModule { 23 | function: "pbkdf2"; 24 | params: { 25 | dklen: number; 26 | c: number; 27 | prf: "hmac-sha256"; 28 | salt: string; 29 | }; 30 | message: ""; 31 | } 32 | 33 | export interface IScryptKdfModule { 34 | function: "scrypt"; 35 | params: { 36 | dklen: number; 37 | n: number; 38 | p: number; 39 | r: number; 40 | salt: string; 41 | }; 42 | message: ""; 43 | } 44 | 45 | // checksum 46 | 47 | export type IChecksumModule = ISha2ChecksumModule; 48 | 49 | export interface ISha2ChecksumModule { 50 | function: "sha256"; 51 | params: object; 52 | message: string; 53 | } 54 | 55 | // cipher 56 | 57 | export type ICipherModule = IAes128CtrCipherModule; 58 | 59 | export interface IAes128CtrCipherModule { 60 | function: "aes-128-ctr"; 61 | params: { 62 | iv: string; 63 | }; 64 | message: string; 65 | } 66 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { hexToBytes } from "ethereum-cryptography/utils"; 3 | 4 | import { create, decrypt, verifyPassword, isValidKeystore, validateKeystore } from "../src" 5 | import {normalizePassword} from "../lib/password"; 6 | 7 | describe("BLS12-381 Keystore Test", () => { 8 | it("Roundtrip should work", async function () { 9 | this.timeout(10000) 10 | const testKeystore = await create("test", new Uint8Array(32), new Uint8Array(48), ""); 11 | expect(isValidKeystore(testKeystore)).to.be.true; 12 | expect(await verifyPassword(testKeystore, "test")).to.be.true; 13 | }); 14 | }); 15 | 16 | describe("Known Test Vectors", () => { 17 | it("Should be able to encrypt/decrypt Pbkdf2 keystores", async function () { 18 | this.timeout(100000) 19 | const keystores = [ 20 | require('./vectors/pbkdf2-0.json'), 21 | require('./vectors/pbkdf2-0.json'), 22 | ]; 23 | for (const keystore of keystores) { 24 | const password = keystore.password; 25 | const secret = hexToBytes(keystore.secret.slice(2)); 26 | 27 | expect(isValidKeystore(keystore)).to.be.true; 28 | expect(await verifyPassword(keystore, password)).to.be.true; 29 | expect(await decrypt(keystore, password)).to.deep.equal(secret); 30 | } 31 | }); 32 | 33 | it("Should be able to encrypt/decrypt Scrypt keystores", async function () { 34 | this.timeout(100000) 35 | const keystores = [ 36 | require('./vectors/scrypt-0.json'), 37 | require('./vectors/scrypt-1.json'), 38 | ]; 39 | for (const keystore of keystores) { 40 | const password = keystore.password; 41 | const secret = hexToBytes(keystore.secret.slice(2)); 42 | 43 | expect(isValidKeystore(keystore)).to.be.true; 44 | expect(await verifyPassword(keystore, password)).to.be.true; 45 | expect(await decrypt(keystore, password)).to.deep.equal(secret); 46 | } 47 | }); 48 | }); 49 | 50 | describe("Password Normalize Tests", () => { 51 | it("should filter out unaccepted control codes from Uint8Array", function() { 52 | //C1 codes lie between 0x00 - 0x1F (inclusive) | 0 - 31 53 | const codeBetween0x00And0x1F = [...Array(32).keys()] 54 | //C1 codes lie between 0x80 and 0x9F (inclusive) | 128 - 159 55 | const codeBetween0x80And0x9F = [...Array(32).keys()].map(code => code + 128) 56 | const passWithIllegalControl = new Uint8Array( 57 | [103, ...codeBetween0x00And0x1F, ...codeBetween0x80And0x9F, 111] 58 | ) 59 | 60 | const normalizedPassword = normalizePassword(passWithIllegalControl) 61 | expect(normalizedPassword).to.be.deep.equal(new Uint8Array([103, 111]), "Unaccepted control codes should be filtered out of password Uint8Array") 62 | }) 63 | 64 | it("should filter out unaccepted control codes from string", function() { 65 | //C1 codes lie between 0x00 - 0x1F (inclusive) | 0 - 31 66 | const codeBetween0x00And0x1F = [...Array(32).keys()] 67 | const passWithIllegalControl = new TextDecoder().decode(new Uint8Array( 68 | [103, ...codeBetween0x00And0x1F, 32, 111] // 32 represents space which is acceptable 69 | )); 70 | 71 | const normalizedPassword = new TextDecoder().decode(normalizePassword(passWithIllegalControl)) 72 | expect(normalizedPassword).to.equal("g o", "Unaccepted control codes should be filtered out of password string") 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /test/vectors/pbkdf2-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "crypto": { 3 | "kdf": { 4 | "function": "pbkdf2", 5 | "params": { 6 | "dklen": 32, 7 | "c": 262144, 8 | "prf": "hmac-sha256", 9 | "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" 10 | }, 11 | "message": "" 12 | }, 13 | "checksum": { 14 | "function": "sha256", 15 | "params": {}, 16 | "message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8" 17 | }, 18 | "cipher": { 19 | "function": "aes-128-ctr", 20 | "params": { 21 | "iv": "264daa3f303d7259501c93d997d84fe6" 22 | }, 23 | "message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48" 24 | } 25 | }, 26 | "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", 27 | "path": "m/12381/60/0/0", 28 | "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", 29 | "version": 4, 30 | "password": "testpassword", 31 | "secret": "0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" 32 | } 33 | -------------------------------------------------------------------------------- /test/vectors/pbkdf2-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "crypto": { 3 | "kdf": { 4 | "function": "pbkdf2", 5 | "params": { 6 | "dklen": 32, 7 | "c": 262144, 8 | "prf": "hmac-sha256", 9 | "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" 10 | }, 11 | "message": "" 12 | }, 13 | "checksum": { 14 | "function": "sha256", 15 | "params": {}, 16 | "message": "8a9f5d9912ed7e75ea794bc5a89bca5f193721d30868ade6f73043c6ea6febf1" 17 | }, 18 | "cipher": { 19 | "function": "aes-128-ctr", 20 | "params": { 21 | "iv": "264daa3f303d7259501c93d997d84fe6" 22 | }, 23 | "message": "cee03fde2af33149775b7223e7845e4fb2c8ae1792e5f99fe9ecf474cc8c16ad" 24 | } 25 | }, 26 | "description": "This is a test keystore that uses PBKDF2 to secure the secret.", 27 | "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", 28 | "path": "m/12381/60/0/0", 29 | "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", 30 | "version": 4, 31 | "password": "𝔱𝔢𝔰𝔱𝔭𝔞𝔰𝔰𝔴𝔬𝔯𝔡🔑", 32 | "secret": "0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" 33 | } 34 | -------------------------------------------------------------------------------- /test/vectors/scrypt-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "crypto": { 3 | "kdf": { 4 | "function": "scrypt", 5 | "params": { 6 | "dklen": 32, 7 | "n": 262144, 8 | "p": 1, 9 | "r": 8, 10 | "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" 11 | }, 12 | "message": "" 13 | }, 14 | "checksum": { 15 | "function": "sha256", 16 | "params": {}, 17 | "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" 18 | }, 19 | "cipher": { 20 | "function": "aes-128-ctr", 21 | "params": { 22 | "iv": "264daa3f303d7259501c93d997d84fe6" 23 | }, 24 | "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" 25 | } 26 | }, 27 | "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", 28 | "path": "m/12381/60/3141592653/589793238", 29 | "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", 30 | "version": 4, 31 | "password": "testpassword", 32 | "secret": "0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" 33 | } 34 | -------------------------------------------------------------------------------- /test/vectors/scrypt-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "crypto": { 3 | "kdf": { 4 | "function": "scrypt", 5 | "params": { 6 | "dklen": 32, 7 | "n": 262144, 8 | "p": 1, 9 | "r": 8, 10 | "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" 11 | }, 12 | "message": "" 13 | }, 14 | "checksum": { 15 | "function": "sha256", 16 | "params": {}, 17 | "message": "d2217fe5f3e9a1e34581ef8a78f7c9928e436d36dacc5e846690a5581e8ea484" 18 | }, 19 | "cipher": { 20 | "function": "aes-128-ctr", 21 | "params": { 22 | "iv": "264daa3f303d7259501c93d997d84fe6" 23 | }, 24 | "message": "06ae90d55fe0a6e9c5c3bc5b170827b2e5cce3929ed3f116c2811e6366dfe20f" 25 | } 26 | }, 27 | "description": "This is a test keystore that uses scrypt to secure the secret.", 28 | "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", 29 | "path": "m/12381/60/3141592653/589793238", 30 | "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", 31 | "version": 4, 32 | "password": "𝔱𝔢𝔰𝔱𝔭𝔞𝔰𝔰𝔴𝔬𝔯𝔡🔑", 33 | "secret": "0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "resolveJsonModule": true 7 | }, 8 | "include": ["src"], 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/web.ts", 3 | mode: "production", 4 | output: { 5 | hashFunction: 'xxhash64', 6 | filename: "bundle.js" 7 | }, 8 | devtool: "source-map", 9 | resolve: { 10 | extensions: [".ts", ".js"], 11 | fallback: { 12 | "crypto": false, 13 | }, 14 | }, 15 | module: { 16 | rules: [ 17 | {test: /\.ts$/, use: {loader: "ts-loader", options: {transpileOnly: true}}} 18 | ] 19 | } 20 | }; 21 | --------------------------------------------------------------------------------