├── .gitignore ├── src ├── model │ ├── keytypes.ts │ ├── initialize.ts │ ├── keys.ts │ └── hdkeys.ts ├── logic │ ├── message.ts │ ├── error.ts │ ├── crypto.ts │ ├── hdstorage.ts │ └── storage.ts ├── __test__ │ ├── meta.spec.ts │ ├── cckey.spec.ts │ ├── volatile.spec.ts │ ├── migrate.spec.ts │ ├── hdwallet.spec.ts │ └── platform.spec.ts ├── types.ts ├── context.ts └── index.ts ├── .travis.yml ├── .editorconfig ├── jest.config.js ├── tslint.json ├── tsconfig.json ├── LICENSE ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | /dist/ 3 | /lib/ 4 | node_modules/ 5 | .DS_Store 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /src/model/keytypes.ts: -------------------------------------------------------------------------------- 1 | export enum KeyType { 2 | Platform = "platform", 3 | Asset = "asset", 4 | HDWSeed = "hdwseed" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "8" 5 | before_install: 6 | - yarn install 7 | script: 8 | - yarn test 9 | - yarn build 10 | cache: yarn 11 | -------------------------------------------------------------------------------- /src/model/initialize.ts: -------------------------------------------------------------------------------- 1 | import * as lowdb from "lowdb"; 2 | 3 | export async function initialize(db: lowdb.LowdbAsync): Promise { 4 | await db 5 | .defaults({ 6 | meta: "", 7 | platform: [], 8 | asset: [], 9 | hdwseed: [] 10 | }) 11 | .write(); 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | insert_final_newline = true 7 | 8 | [*.yml] 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.{ts,json,js}] 13 | trim_trailing_whitespace = true 14 | indent_style = space 15 | indent_size = 4 16 | 17 | # npm inserts a final newline. 18 | [package.json] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "transform": { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 9 | "moduleFileExtensions": [ 10 | "ts", 11 | "tsx", 12 | "js", 13 | "jsx", 14 | "json", 15 | "node" 16 | ], 17 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": false, 5 | "no-console": false, 6 | "object-literal-sort-keys": false, 7 | "no-var-requires": false, 8 | "array-type": false 9 | }, 10 | "jsRules": { 11 | "no-console": false, 12 | "object-literal-sort-keys": false 13 | }, 14 | "linterOptions": { 15 | "exclude": ["node_modules/**/*.ts", "public/javascripts/lib/*"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/logic/message.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../context"; 2 | import { ErrorCode, KeystoreError } from "./error"; 3 | 4 | export function errorMessage(context: Context, error: KeystoreError): string { 5 | switch (error.code) { 6 | case ErrorCode.Unknown: 7 | return "Unknown server error. Please retry after some minuates later"; 8 | default: 9 | console.error("Invalid error code " + error.code); 10 | return "Unknown server error. Please retry some minuates later"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/logic/error.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorCode { 2 | Unknown = 0, 3 | NoSuchKey = 1, 4 | NoSuchSeedHash = 2, 5 | DecryptionFailed = 3, 6 | DBError = 4, 7 | WrongSeedLength = 5, 8 | WrongMnemonicString = 6 9 | } 10 | 11 | export class KeystoreError extends Error { 12 | public code: ErrorCode; 13 | public codeName: string; 14 | public name: string; 15 | 16 | constructor(code: ErrorCode) { 17 | super(ErrorCode[code]); 18 | this.code = code; 19 | this.codeName = ErrorCode[code]; 20 | this.name = "KeystoreError"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/__test__/meta.spec.ts: -------------------------------------------------------------------------------- 1 | import { CCKey } from "../index"; 2 | 3 | describe("meta", () => { 4 | let cckey: CCKey; 5 | 6 | beforeEach(async () => { 7 | cckey = await CCKey.create({ dbType: "in-memory" }); 8 | }); 9 | 10 | afterEach(async () => { 11 | cckey.close(); 12 | }); 13 | 14 | test("defaultEmptyString", async () => { 15 | const meta = await cckey.getMeta(); 16 | expect(meta).toBe(""); 17 | }); 18 | 19 | test("setMeta", async () => { 20 | await cckey.setMeta("new meta"); 21 | const meta = await cckey.getMeta(); 22 | expect(meta).toBe("new meta"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "moduleResolution": "node", 11 | "declaration": true, 12 | "rootDir": "src", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true 20 | }, 21 | "include": [ 22 | "./src/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "dist" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/logic/crypto.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | 3 | export function pbkdf2Async( 4 | passphrase: string | Buffer | NodeJS.TypedArray | DataView, 5 | salt: string | Buffer | NodeJS.TypedArray | DataView, 6 | iterations: number, 7 | keylen: number, 8 | digest: string 9 | ): Promise { 10 | return new Promise((resolve, reject) => { 11 | crypto.pbkdf2( 12 | passphrase, 13 | salt, 14 | iterations, 15 | keylen, 16 | digest, 17 | (err, drived) => { 18 | if (err) { 19 | reject(err); 20 | } else { 21 | resolve(drived); 22 | } 23 | } 24 | ); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018 Kodebox Inc. 4 | 5 | Permission to use, copy, modify, and distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CodeChain keystore [![Build Status](https://travis-ci.org/CodeChain-io/codechain-keystore-js.svg?branch=master)](https://travis-ci.org/CodeChain-io/codechain-keystore-js) 2 | =================== 3 | 4 | CodeChain keystore is a private key management library. It saves CodeChain's asset transfer address safely in a disk. If you want to manage CodeChain keys using nodejs, you should use this. 5 | 6 | Example 7 | ----------- 8 | 9 | ```js 10 | var CCKey = require('codechain-keystore'); 11 | 12 | async function example() { 13 | const cckey = await CCKey.create(); 14 | const savedKeys = await cckey.platform.getKeys(); 15 | console.dir(savedKeys); 16 | await cckey.platform.createKey({ passphrase: "my password" }); 17 | const savedKeys_ = await cckey.platform.getKeys(); 18 | console.dir(savedKeys_); 19 | 20 | await cckey.close(); 21 | }; 22 | example(); 23 | 24 | ``` 25 | 26 | How your private key is saved 27 | ------------------- 28 | 29 | We use a JSON file to save an encrypted private key. You can find the file in `./keystore.db` 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Key is a type which is used to query the KeyStore. 3 | * Each KeyType has different key generation algorithm. 4 | * Key is generated from public key. 5 | */ 6 | export type Key = AccountId | PublicKeyHash; 7 | export type PublicKeyHash = string; 8 | export type AccountId = string; 9 | export type PublicKey = string; 10 | export type PrivateKey = string; 11 | export type Seed = string; 12 | export type SeedHash = string; 13 | 14 | export interface SecretStorage { 15 | crypto: { 16 | cipher: string; 17 | cipherparams: { 18 | iv: string; 19 | }; 20 | ciphertext: string; 21 | kdf: string; 22 | kdfparams: { 23 | c: number; 24 | dklen: number; 25 | prf: string; 26 | salt: string; 27 | }; 28 | mac: string; 29 | }; 30 | meta: string; 31 | address?: string; 32 | id: string; 33 | version: number; 34 | } 35 | 36 | export interface SecretSeedStorage { 37 | crypto: { 38 | cipher: string; 39 | cipherparams: { 40 | iv: string; 41 | }; 42 | ciphertext: string; 43 | kdf: string; 44 | kdfparams: { 45 | c: number; 46 | dklen: number; 47 | prf: string; 48 | salt: string; 49 | }; 50 | mac: string; 51 | }; 52 | meta: string; 53 | seedHash: SeedHash; 54 | id: string; 55 | version: number; 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codechain-keystore", 3 | "version": "0.6.3", 4 | "author": "CodeChain Team ", 5 | "license": "ISC", 6 | "scripts": { 7 | "build": "tsc -p .", 8 | "test": "yarn lint && jest --env node", 9 | "lint": "tslint -p . && prettier 'src/**/*.{ts,js,json}' -l", 10 | "fmt": "tslint -p . --fix && prettier 'src/**/*.{ts,js,json}' --write" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/CodeChain-io/codechain-keystore.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/codechain-io/codechain-keystore/issues" 18 | }, 19 | "homepage": "https://github.com/CodeChain-io/codechain-keystore#readme", 20 | "main": "./lib/index.js", 21 | "typings": "./lib/index.d.ts", 22 | "files": [ 23 | "lib" 24 | ], 25 | "dependencies": { 26 | "bitcore-mnemonic": "^8.6.0", 27 | "codechain-primitives": "^1.0.0", 28 | "config": "^2.0.1", 29 | "lodash": "^4.17.10", 30 | "lowdb": "^1.0.0", 31 | "lowdb-session-storage-adapter": "^1.0.0", 32 | "uuid": "^3.3.2" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "^23.3.1", 36 | "@types/lodash": "^4.14.116", 37 | "@types/lowdb": "^1.0.5", 38 | "@types/node": "^10.9.1", 39 | "@types/uuid": "^3.4.4", 40 | "jest": "^23.5.0", 41 | "prettier": "1.14.2", 42 | "ts-jest": "^23.1.3", 43 | "ts-node": "^7.0.0", 44 | "tsc": "^1.20150623.0", 45 | "tslint": "^5.11.0", 46 | "tslint-config-prettier": "^1.14.0", 47 | "typescript": "^3.7.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/__test__/cckey.spec.ts: -------------------------------------------------------------------------------- 1 | import { CCKey } from "../index"; 2 | 3 | describe("cckey", () => { 4 | let cckey: CCKey; 5 | 6 | beforeEach(async () => { 7 | cckey = await CCKey.create({ dbType: "in-memory" }); 8 | }); 9 | 10 | afterEach(async () => { 11 | cckey.close(); 12 | }); 13 | 14 | test("saveLoad", async () => { 15 | const passphrase = "satoshi"; 16 | const platformKey1 = await cckey.platform.createKey({ passphrase }); 17 | const platformKey2 = await cckey.platform.createKey({ passphrase }); 18 | const assetKey = await cckey.asset.createKey({ passphrase }); 19 | const seedHash = await cckey.hdwseed.createSeed({ passphrase }); 20 | await cckey.setMeta("new meta data"); 21 | 22 | const saveData = await cckey.save(); 23 | const newCckey = await CCKey.create({ dbType: "in-memory" }); 24 | await newCckey.load(saveData); 25 | 26 | expect(await newCckey.platform.getKeys()).toEqual([ 27 | platformKey1, 28 | platformKey2 29 | ]); 30 | expect(await newCckey.asset.getKeys()).toEqual([assetKey]); 31 | expect(await newCckey.hdwseed.getSeedHashes()).toEqual([seedHash]); 32 | expect(await newCckey.getMeta()).toBe("new meta data"); 33 | }); 34 | 35 | test("clear removes key", async () => { 36 | const createdKey = await cckey.platform.createKey({ 37 | passphrase: "satoshi" 38 | }); 39 | expect(await cckey.platform.getKeys()).toEqual([createdKey]); 40 | await cckey.clear(); 41 | expect(await cckey.platform.getKeys()).toEqual([]); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__test__/volatile.spec.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | import { CCKey } from "../index"; 3 | 4 | describe("volatile", () => { 5 | const dbPath = crypto.randomBytes(4).toString("hex"); 6 | const dbType = "volatile"; 7 | const params = { dbPath, dbType }; 8 | beforeEach(async () => { 9 | const cckey = await CCKey.create(params); 10 | await cckey.close(); 11 | }); 12 | 13 | test("volatile db removes data on close", async () => { 14 | const cckey = await CCKey.create(params); 15 | const createdKey = await cckey.platform.createKey({ meta: "meta" }); 16 | await expect(await cckey.platform.getKeys()).toEqual([createdKey]); 17 | expect(await cckey.platform.getMeta({ key: createdKey })).toBe("meta"); 18 | await cckey.close(); 19 | 20 | const cckey2 = await CCKey.create(params); 21 | await expect(await cckey2.platform.getKeys()).toEqual([]); 22 | await expect( 23 | cckey2.platform.getMeta({ key: createdKey }) 24 | ).rejects.toThrow(); 25 | }); 26 | 27 | test("false if not exist", async () => { 28 | expect(await CCKey.exist(params)).toBe(false); 29 | }); 30 | 31 | test("false if there is no change", async () => { 32 | await CCKey.create(params); 33 | expect(await CCKey.exist(params)).toBe(false); 34 | }); 35 | 36 | test("true if exist", async () => { 37 | const cckey = await CCKey.create(params); 38 | await cckey.platform.createKey({}); 39 | expect(await CCKey.exist(params)).toBe(true); 40 | }); 41 | 42 | test("doesn't exist if volatile db closed", async () => { 43 | const cckey = await CCKey.create(params); 44 | await cckey.close(); 45 | expect(await CCKey.exist(params)).toBe(false); 46 | }); 47 | 48 | test("doesn't exist if db cleared", async () => { 49 | const cckey = await CCKey.create(params); 50 | await cckey.clear(); 51 | expect(await CCKey.exist(params)).toBe(false); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import * as Lowdb from "lowdb"; 2 | import lowdb = require("lowdb"); 3 | import * as os from "os"; 4 | import { clear as hdClear } from "./model/hdkeys"; 5 | import { initialize as dbInitialize } from "./model/initialize"; 6 | import { clear } from "./model/keys"; 7 | import { KeyType } from "./model/keytypes"; 8 | 9 | declare var window: any; 10 | function isBrowser() { 11 | return typeof window !== "undefined"; 12 | } 13 | 14 | const memoryAdapter = require("lowdb/adapters/Memory"); 15 | const persistentAdapter = isBrowser() 16 | ? require("lowdb/adapters/LocalStorage") 17 | : require("lowdb/adapters/FileSync"); 18 | const volatileAdapter = isBrowser() 19 | ? require("lowdb-session-storage-adapter") 20 | : require("lowdb/adapters/FileSync"); 21 | 22 | export interface Context { 23 | db: Lowdb.LowdbAsync; 24 | isVolatile: boolean; 25 | } 26 | 27 | function getAdapter(params: { dbPath: string; dbType: string }) { 28 | switch (params.dbType) { 29 | case "persistent": 30 | return new persistentAdapter(params.dbPath); 31 | case "volatile": { 32 | const dbPath = isBrowser() 33 | ? params.dbPath 34 | : `${os.tmpdir()}/${params.dbPath}`; 35 | return new volatileAdapter(dbPath); 36 | } 37 | case "in-memory": 38 | return new memoryAdapter(params.dbPath); 39 | default: 40 | throw new Error(`Not expected type: ${params.dbType}`); 41 | } 42 | } 43 | 44 | export async function storageExist(params: { 45 | dbType: string; 46 | dbPath: string; 47 | }): Promise { 48 | const db = await lowdb(getAdapter(params)); 49 | const meta = db.get("meta").value(); 50 | const platform = db.get(KeyType.Platform).value(); 51 | const asset = db.get(KeyType.Asset).value(); 52 | const hdwseed = db.get(KeyType.HDWSeed).value(); 53 | 54 | return ( 55 | (meta != null && meta !== "") || 56 | (platform != null && platform.length !== 0) || 57 | (asset != null && asset.length !== 0) || 58 | (hdwseed != null && hdwseed.length !== 0) 59 | ); 60 | } 61 | 62 | export async function createContext(params: { 63 | dbType: string; 64 | dbPath: string; 65 | debug?: boolean; 66 | }): Promise { 67 | const db = await lowdb(getAdapter(params)); 68 | 69 | await dbInitialize(db); 70 | 71 | return { 72 | db, 73 | isVolatile: params.dbType === "volatile" 74 | }; 75 | } 76 | 77 | export async function closeContext(context: Context): Promise { 78 | if (context.isVolatile) { 79 | await context.db.unset("meta").write(); 80 | await clear(context, { keyType: KeyType.Asset }); 81 | await clear(context, { keyType: KeyType.Platform }); 82 | await hdClear(context); 83 | } 84 | return Promise.resolve(context.db.write()); 85 | } 86 | -------------------------------------------------------------------------------- /src/logic/hdstorage.ts: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2015 Alex Beregszaszi 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 | 23 | import { blake256 } from "codechain-primitives"; 24 | import * as crypto from "crypto"; 25 | import * as uuid from "uuid"; 26 | import { SecretSeedStorage, Seed } from "../types"; 27 | import { pbkdf2Async } from "./crypto"; 28 | import { ErrorCode, KeystoreError } from "./error"; 29 | 30 | // copy code from https://github.com/ethereumjs/ethereumjs-wallet/blob/4c7cbfc12e142491eb5acc98e612f079aabe092e/src/index.js#L109 31 | export async function encode( 32 | seed: Seed, 33 | passphrase: string, 34 | meta: string 35 | ): Promise { 36 | const seedHash = blake256(Buffer.from(seed, "hex")); 37 | const salt = crypto.randomBytes(32); 38 | const iv = crypto.randomBytes(16); 39 | 40 | const kdf = "pbkdf2"; 41 | const kdfparams = { 42 | dklen: 32, 43 | salt: salt.toString("hex"), 44 | c: 262144, 45 | prf: "hmac-sha256" 46 | }; 47 | const derivedKey = await pbkdf2Async( 48 | Buffer.from(passphrase), 49 | salt, 50 | kdfparams.c, 51 | kdfparams.dklen, 52 | "sha256" 53 | ); 54 | const cipher = crypto.createCipheriv( 55 | "aes-128-ctr", 56 | derivedKey.slice(0, 16), 57 | iv 58 | ); 59 | const ciphertext: any = Buffer.concat([ 60 | cipher.update(Buffer.from(seed, "hex")), 61 | cipher.final() 62 | ]); 63 | 64 | const mac = blake256( 65 | Buffer.concat([ 66 | derivedKey.slice(16, 32), 67 | Buffer.from(ciphertext, "hex") 68 | ]) 69 | ); 70 | 71 | return { 72 | crypto: { 73 | ciphertext: ciphertext.toString("hex"), 74 | cipherparams: { 75 | iv: iv.toString("hex") 76 | }, 77 | cipher: "aes-128-ctr", 78 | kdf, 79 | kdfparams, 80 | mac 81 | }, 82 | id: uuid.v4({ 83 | random: Array.prototype.slice.call(crypto.randomBytes(16), 0) 84 | }), 85 | version: 3, 86 | seedHash, 87 | meta 88 | }; 89 | } 90 | 91 | export async function decode( 92 | json: SecretSeedStorage, 93 | passphrase: string 94 | ): Promise { 95 | const kdfparams = json.crypto.kdfparams; 96 | const derivedKey = await pbkdf2Async( 97 | Buffer.from(passphrase), 98 | Buffer.from(kdfparams.salt, "hex"), 99 | kdfparams.c, 100 | kdfparams.dklen, 101 | "sha256" 102 | ); 103 | const ciphertext = Buffer.from(json.crypto.ciphertext, "hex"); 104 | const mac = blake256(Buffer.concat([derivedKey.slice(16, 32), ciphertext])); 105 | if (mac !== json.crypto.mac) { 106 | throw new KeystoreError(ErrorCode.DecryptionFailed); 107 | } 108 | const decipher = crypto.createDecipheriv( 109 | json.crypto.cipher, 110 | derivedKey.slice(0, 16), 111 | Buffer.from(json.crypto.cipherparams.iv, "hex") 112 | ); 113 | const seed = decipherBuffer(decipher, ciphertext); 114 | return seed.toString("hex"); 115 | } 116 | 117 | function decipherBuffer(decipher: crypto.Decipher, data: Buffer): Buffer { 118 | return Buffer.concat([decipher.update(data), decipher.final()]); 119 | } 120 | -------------------------------------------------------------------------------- /src/logic/storage.ts: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2015 Alex Beregszaszi 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 | 23 | import { blake256, getPublicFromPrivate } from "codechain-primitives"; 24 | import * as crypto from "crypto"; 25 | import * as uuid from "uuid"; 26 | import { SecretStorage } from ".."; 27 | import { keyFromPublicKey } from "../model/keys"; 28 | import { KeyType } from "../model/keytypes"; 29 | import { PrivateKey } from "../types"; 30 | import { pbkdf2Async } from "./crypto"; 31 | import { ErrorCode, KeystoreError } from "./error"; 32 | 33 | // copy code from https://github.com/ethereumjs/ethereumjs-wallet/blob/4c7cbfc12e142491eb5acc98e612f079aabe092e/src/index.js#L109 34 | export async function encode( 35 | privateKey: PrivateKey, 36 | keyType: KeyType, 37 | passphrase: string, 38 | meta: string 39 | ): Promise { 40 | const publicKey = getPublicFromPrivate(privateKey); 41 | const address = keyFromPublicKey(keyType, publicKey); 42 | const salt = crypto.randomBytes(32); 43 | const iv = crypto.randomBytes(16); 44 | 45 | const kdf = "pbkdf2"; 46 | const kdfparams = { 47 | dklen: 32, 48 | salt: salt.toString("hex"), 49 | c: 262144, 50 | prf: "hmac-sha256" 51 | }; 52 | const derivedKey = await pbkdf2Async( 53 | Buffer.from(passphrase), 54 | salt, 55 | kdfparams.c, 56 | kdfparams.dklen, 57 | "sha256" 58 | ); 59 | const cipher = crypto.createCipheriv( 60 | "aes-128-ctr", 61 | derivedKey.slice(0, 16), 62 | iv 63 | ); 64 | const ciphertext: any = Buffer.concat([ 65 | cipher.update(Buffer.from(privateKey, "hex")), 66 | cipher.final() 67 | ]); 68 | 69 | const mac = blake256( 70 | Buffer.concat([ 71 | derivedKey.slice(16, 32), 72 | Buffer.from(ciphertext, "hex") 73 | ]) 74 | ); 75 | 76 | return { 77 | crypto: { 78 | ciphertext: ciphertext.toString("hex"), 79 | cipherparams: { 80 | iv: iv.toString("hex") 81 | }, 82 | cipher: "aes-128-ctr", 83 | kdf, 84 | kdfparams, 85 | mac 86 | }, 87 | id: uuid.v4({ 88 | random: Array.prototype.slice.call(crypto.randomBytes(16), 0) 89 | }), 90 | version: 3, 91 | address, 92 | meta 93 | }; 94 | } 95 | 96 | export async function decode( 97 | json: SecretStorage, 98 | passphrase: string 99 | ): Promise { 100 | const kdfparams = json.crypto.kdfparams; 101 | const derivedKey = await pbkdf2Async( 102 | Buffer.from(passphrase), 103 | Buffer.from(kdfparams.salt, "hex"), 104 | kdfparams.c, 105 | kdfparams.dklen, 106 | "sha256" 107 | ); 108 | const ciphertext = Buffer.from(json.crypto.ciphertext, "hex"); 109 | const mac = blake256(Buffer.concat([derivedKey.slice(16, 32), ciphertext])); 110 | if (mac !== json.crypto.mac) { 111 | throw new KeystoreError(ErrorCode.DecryptionFailed); 112 | } 113 | const decipher = crypto.createDecipheriv( 114 | json.crypto.cipher, 115 | derivedKey.slice(0, 16), 116 | Buffer.from(json.crypto.cipherparams.iv, "hex") 117 | ); 118 | const privateKey = decipherBuffer(decipher, ciphertext); 119 | return privateKey.toString("hex"); 120 | } 121 | 122 | function decipherBuffer(decipher: crypto.Decipher, data: Buffer): Buffer { 123 | return Buffer.concat([decipher.update(data), decipher.final()]); 124 | } 125 | -------------------------------------------------------------------------------- /src/model/keys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | blake160, 3 | generatePrivateKey, 4 | getAccountIdFromPublic, 5 | getPublicFromPrivate, 6 | H160, 7 | signEcdsa 8 | } from "codechain-primitives"; 9 | import * as _ from "lodash"; 10 | import { Context } from "../context"; 11 | import { ErrorCode, KeystoreError } from "../logic/error"; 12 | import { decode, encode } from "../logic/storage"; 13 | import { Key, PrivateKey, PublicKey, SecretStorage } from "../types"; 14 | import { KeyType } from "./keytypes"; 15 | 16 | export async function getKeys( 17 | context: Context, 18 | params: { keyType: KeyType } 19 | ): Promise { 20 | const rows: any = await context.db.get(params.keyType).value(); 21 | return _.map(rows, (secret: SecretStorage) => secret.address) as Key[]; 22 | } 23 | 24 | export async function getPublicKey( 25 | context: Context, 26 | params: { key: Key; passphrase: string; keyType: KeyType } 27 | ): Promise { 28 | const secret = await getSecretStorage(context, params); 29 | if (secret == null) { 30 | return null; 31 | } 32 | const privateKey = await decode(secret, params.passphrase); 33 | return getPublicFromPrivate(privateKey); 34 | } 35 | 36 | export function importRaw( 37 | context: Context, 38 | params: { 39 | privateKey: PrivateKey; 40 | passphrase?: string; 41 | keyType: KeyType; 42 | meta?: string; 43 | } 44 | ): Promise { 45 | return createKeyFromPrivateKey(context, params); 46 | } 47 | 48 | export async function exportKey( 49 | context: Context, 50 | params: { key: Key; passphrase: string; keyType: KeyType } 51 | ): Promise { 52 | const secret = await getSecretStorage(context, params); 53 | if (secret == null) { 54 | throw new KeystoreError(ErrorCode.NoSuchKey); 55 | } 56 | await decode(secret, params.passphrase); // Throws an error if the passphrase is incorrect. 57 | return secret; 58 | } 59 | 60 | export async function importKey( 61 | context: Context, 62 | params: { secret: SecretStorage; passphrase: string; keyType: KeyType } 63 | ): Promise { 64 | const privateKey = await decode(params.secret, params.passphrase); 65 | return importRaw(context, { 66 | privateKey, 67 | passphrase: params.passphrase, 68 | keyType: params.keyType, 69 | meta: params.secret.meta 70 | }); 71 | } 72 | 73 | export function createKey( 74 | context: Context, 75 | params: { passphrase?: string; keyType: KeyType; meta?: string } 76 | ): Promise { 77 | const privateKey = generatePrivateKey(); 78 | return createKeyFromPrivateKey(context, { 79 | ...params, 80 | privateKey 81 | }); 82 | } 83 | 84 | async function createKeyFromPrivateKey( 85 | context: Context, 86 | params: { 87 | privateKey: PrivateKey; 88 | passphrase?: string; 89 | keyType: KeyType; 90 | meta?: string; 91 | } 92 | ): Promise { 93 | const publicKey = getPublicFromPrivate(params.privateKey); 94 | const passphrase = params.passphrase || ""; 95 | const meta = params.meta || "{}"; 96 | 97 | const secret = await encode( 98 | params.privateKey, 99 | params.keyType, 100 | passphrase, 101 | meta 102 | ); 103 | const rows: any = context.db.get(params.keyType); 104 | await rows.push(secret).write(); 105 | return keyFromPublicKey(params.keyType, publicKey); 106 | } 107 | 108 | export function keyFromPublicKey(type: KeyType, publicKey: PublicKey): Key { 109 | switch (type) { 110 | case KeyType.Platform: 111 | return getAccountIdFromPublic(publicKey); 112 | case KeyType.Asset: 113 | return H160.ensure(blake160(publicKey)).value; 114 | default: 115 | throw new Error("Invalid key type"); 116 | } 117 | } 118 | 119 | export async function deleteKey( 120 | context: Context, 121 | params: { key: Key; keyType: KeyType } 122 | ): Promise { 123 | const secret = await getSecretStorage(context, params); 124 | if (secret == null) { 125 | return false; 126 | } 127 | 128 | await removeKey(context, params); 129 | return true; 130 | } 131 | 132 | async function getSecretStorage( 133 | context: Context, 134 | params: { key: Key; keyType: KeyType } 135 | ): Promise { 136 | const collection: any = context.db.get(params.keyType); 137 | const secret = await collection 138 | .find( 139 | (secretStorage: SecretStorage) => 140 | secretStorage.address === params.key 141 | ) 142 | .value(); 143 | 144 | if (secret == null) { 145 | return null; 146 | } 147 | return secret as SecretStorage; 148 | } 149 | 150 | async function removeKey( 151 | context: Context, 152 | params: { key: Key; keyType: KeyType } 153 | ): Promise { 154 | const collection: any = context.db.get(params.keyType); 155 | await collection 156 | .remove((secret: SecretStorage) => secret.address === params.key) 157 | .write(); 158 | } 159 | 160 | export async function exportRawKey( 161 | context: Context, 162 | params: { key: Key; passphrase: string; keyType: KeyType } 163 | ) { 164 | const secret = await getSecretStorage(context, params); 165 | if (secret == null) { 166 | throw new KeystoreError(ErrorCode.NoSuchKey); 167 | } 168 | 169 | return decode(secret, params.passphrase); 170 | } 171 | 172 | export async function sign( 173 | context: Context, 174 | params: { 175 | key: Key; 176 | message: string; 177 | passphrase: string; 178 | keyType: KeyType; 179 | } 180 | ): Promise { 181 | const secret = await getSecretStorage(context, params); 182 | if (secret == null) { 183 | throw new KeystoreError(ErrorCode.NoSuchKey); 184 | } 185 | 186 | const privateKey = await decode(secret, params.passphrase); 187 | return signEcdsa(params.message, privateKey); 188 | } 189 | 190 | export async function getMeta( 191 | context: Context, 192 | params: { key: Key; keyType: KeyType } 193 | ): Promise { 194 | const secret = await getSecretStorage(context, params); 195 | if (secret == null) { 196 | throw new KeystoreError(ErrorCode.NoSuchKey); 197 | } 198 | return secret.meta; 199 | } 200 | 201 | export function save( 202 | context: Context, 203 | params: { 204 | keyType: KeyType; 205 | } 206 | ): Promise { 207 | return context.db.get(params.keyType).value(); 208 | } 209 | 210 | export function load( 211 | context: Context, 212 | value: SecretStorage[], 213 | params: { 214 | keyType: KeyType; 215 | } 216 | ): Promise { 217 | return context.db.set(params.keyType, value).write(); 218 | } 219 | 220 | export async function clear( 221 | context: Context, 222 | params: { keyType: KeyType } 223 | ): Promise { 224 | await context.db.unset(params.keyType).write(); 225 | } 226 | -------------------------------------------------------------------------------- /src/__test__/migrate.spec.ts: -------------------------------------------------------------------------------- 1 | import { CCKey } from "../index"; 2 | 3 | describe("migrate", () => { 4 | test("migrate", async () => { 5 | const cckey = await CCKey.create({ dbType: "in-memory" }); 6 | const oldFormat = JSON.stringify({ 7 | platform_keys: [ 8 | { 9 | secret: 10 | '{"crypto":{"ciphertext":"f4139cc2021a4f839574602d9825e95c2f8d05dc52b50203d64f0b6ff53f4550","cipherparams":{"iv":"280af65c7138efa041ea7994a37c62cd"},"cipher":"aes-128-ctr","kdf":"pbkdf2","kdfparams":{"dklen":32,"salt":"05b56bc53ec8838575e5f243587f0a5b9ad2a2c3aab0df0e400dc78dbc81e4d1","c":262144,"prf":"hmac-sha256"},"mac":"1aa9cc11f124855b88638e97392350f230ac54c3d2efaf214a4246bbadd6c569"},"id":"253f7017-aa61-4797-94ea-7ac4402fd239","version":3}', 11 | publicKey: 12 | "b71f1a9a5fb63155b7ccc12841867e95a33da91c305158045a6c7c5e575f204828adec3980387a12ef9f159721c853e47e64a37f61407e0131e9e62983cd6d2e" 13 | }, 14 | { 15 | secret: 16 | '{"crypto":{"ciphertext":"44e864dae8cb46d580bbd3dc20406c8e4237675572b38402f1c243d3f6e44bc6","cipherparams":{"iv":"399d7384431cf49327596fd08b9a9728"},"cipher":"aes-128-ctr","kdf":"pbkdf2","kdfparams":{"dklen":32,"salt":"38c580bfad2fd102da367e9ee81a92e8cba0ff43911eb329e9dea1ea53c28abf","c":262144,"prf":"hmac-sha256"},"mac":"d564c4a1122ae1cb080519fe0345f678b68c16eb47a92351afce77e6c3fad04e"},"id":"e4eaa554-c0c4-48dd-842d-99b3c92419a1","version":3}', 17 | publicKey: 18 | "4c00868624282ee68380f07f1d3368be18447453913eb6b91955975d55b9887a91d632b6f8295434d29c2f7910800e4edd1e867fad6ed21aab1f57f1b56845b9" 19 | } 20 | ], 21 | asset_keys: [ 22 | { 23 | secret: 24 | '{"crypto":{"ciphertext":"a3a9c3201cd59668b8c50059bf615ad79bc7e3d7242ed8bedbecb4a539adc824","cipherparams":{"iv":"15d160ab147a3143f7a0f424f0a8818a"},"cipher":"aes-128-ctr","kdf":"pbkdf2","kdfparams":{"dklen":32,"salt":"f050b633d76bbb081925adc4bc2d3871e02fa873a4123cbc1069450b492de2aa","c":262144,"prf":"hmac-sha256"},"mac":"fe025d8bcc9bfe77761f7928500b79643ed89543ff9e4905e083efe2ec716d60"},"id":"e5b859ca-9efd-4a5d-abc7-fd0ad721f57b","version":3}', 25 | publicKey: 26 | "7a57a8c36aa652015f5f736b74655acb89a640e18fcbd1ef91402ca6322b34ea2ca10f97a2456e667d47d60c140380eedb9b350128085da8b788bc75906436a2" 27 | } 28 | ], 29 | mapping: { 30 | "9c6913351145c88cebd1d16fe47720cb98f796b8": 31 | "b71f1a9a5fb63155b7ccc12841867e95a33da91c305158045a6c7c5e575f204828adec3980387a12ef9f159721c853e47e64a37f61407e0131e9e62983cd6d2e", 32 | "3c25ad001aa929131c2924275b95ef1f86d61a48": 33 | "4c00868624282ee68380f07f1d3368be18447453913eb6b91955975d55b9887a91d632b6f8295434d29c2f7910800e4edd1e867fad6ed21aab1f57f1b56845b9", 34 | df87b2faf36eacdf6d4136ad111ce97cfc87170a: 35 | "7a57a8c36aa652015f5f736b74655acb89a640e18fcbd1ef91402ca6322b34ea2ca10f97a2456e667d47d60c140380eedb9b350128085da8b788bc75906436a2" 36 | } 37 | }); 38 | const result = JSON.parse( 39 | await cckey.migrate(oldFormat, { 40 | platformPassphrase: ["a", "b"], 41 | assetPassphrase: ["c"] 42 | }) 43 | ); 44 | 45 | const newFormat = { 46 | platform: [ 47 | { 48 | crypto: { 49 | ciphertext: 50 | "f4139cc2021a4f839574602d9825e95c2f8d05dc52b50203d64f0b6ff53f4550", 51 | cipherparams: { 52 | iv: "280af65c7138efa041ea7994a37c62cd" 53 | }, 54 | cipher: "aes-128-ctr", 55 | kdf: "pbkdf2", 56 | kdfparams: { 57 | dklen: 32, 58 | salt: 59 | "05b56bc53ec8838575e5f243587f0a5b9ad2a2c3aab0df0e400dc78dbc81e4d1", 60 | c: 262144, 61 | prf: "hmac-sha256" 62 | }, 63 | mac: 64 | "1aa9cc11f124855b88638e97392350f230ac54c3d2efaf214a4246bbadd6c569" 65 | }, 66 | address: "9c6913351145c88cebd1d16fe47720cb98f796b8", 67 | id: "253f7017-aa61-4797-94ea-7ac4402fd239", 68 | version: 3 69 | }, 70 | { 71 | crypto: { 72 | ciphertext: 73 | "44e864dae8cb46d580bbd3dc20406c8e4237675572b38402f1c243d3f6e44bc6", 74 | cipherparams: { 75 | iv: "399d7384431cf49327596fd08b9a9728" 76 | }, 77 | cipher: "aes-128-ctr", 78 | kdf: "pbkdf2", 79 | kdfparams: { 80 | dklen: 32, 81 | salt: 82 | "38c580bfad2fd102da367e9ee81a92e8cba0ff43911eb329e9dea1ea53c28abf", 83 | c: 262144, 84 | prf: "hmac-sha256" 85 | }, 86 | mac: 87 | "d564c4a1122ae1cb080519fe0345f678b68c16eb47a92351afce77e6c3fad04e" 88 | }, 89 | address: "3c25ad001aa929131c2924275b95ef1f86d61a48", 90 | id: "e4eaa554-c0c4-48dd-842d-99b3c92419a1", 91 | version: 3 92 | } 93 | ], 94 | asset: [ 95 | { 96 | crypto: { 97 | ciphertext: 98 | "a3a9c3201cd59668b8c50059bf615ad79bc7e3d7242ed8bedbecb4a539adc824", 99 | cipherparams: { 100 | iv: "15d160ab147a3143f7a0f424f0a8818a" 101 | }, 102 | cipher: "aes-128-ctr", 103 | kdf: "pbkdf2", 104 | kdfparams: { 105 | dklen: 32, 106 | salt: 107 | "f050b633d76bbb081925adc4bc2d3871e02fa873a4123cbc1069450b492de2aa", 108 | c: 262144, 109 | prf: "hmac-sha256" 110 | }, 111 | mac: 112 | "fe025d8bcc9bfe77761f7928500b79643ed89543ff9e4905e083efe2ec716d60" 113 | }, 114 | address: "df87b2faf36eacdf6d4136ad111ce97cfc87170a", 115 | id: "e5b859ca-9efd-4a5d-abc7-fd0ad721f57b", 116 | version: 3 117 | } 118 | ], 119 | hdwseed: [], 120 | meta: "{}" 121 | }; 122 | expect(result).toEqual(newFormat); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/__test__/hdwallet.spec.ts: -------------------------------------------------------------------------------- 1 | import { getPublicFromPrivate } from "codechain-primitives"; 2 | import { CCKey } from "../index"; 3 | 4 | describe("HD wallet test", () => { 5 | let cckey: CCKey; 6 | const passphrase = "satoshi"; 7 | 8 | beforeEach(async () => { 9 | cckey = await CCKey.create({ dbType: "in-memory" }); 10 | }); 11 | 12 | afterEach(async () => { 13 | cckey.close(); 14 | }); 15 | 16 | test("create and delete", async () => { 17 | const seedHash = await cckey.hdwseed.createSeed({ passphrase }); 18 | expect(await cckey.hdwseed.getSeedHashes()).toEqual([seedHash]); 19 | await cckey.hdwseed.deleteSeed({ seedHash }); 20 | expect(await cckey.hdwseed.getSeedHashes()).toEqual([]); 21 | }); 22 | 23 | test.each(["m/0", "M/0", "m/0/1/2/3/4", "m/0'", "m/0'/1'/2'/3'/4'"])( 24 | "get public key & private key - %p", 25 | async path => { 26 | const seedHash = await cckey.hdwseed.createSeed({ passphrase }); 27 | const publicKey = await cckey.hdwseed.getPublicKeyFromSeed({ 28 | seedHash, 29 | path, 30 | passphrase 31 | }); 32 | const privateKey = await cckey.hdwseed.getPrivateKeyFromSeed({ 33 | seedHash, 34 | path, 35 | passphrase 36 | }); 37 | 38 | expect(getPublicFromPrivate(privateKey)).toEqual(publicKey); 39 | } 40 | ); 41 | 42 | test("export and import", async () => { 43 | const seedHash = await cckey.hdwseed.createSeed({ passphrase }); 44 | const masterKey = await cckey.hdwseed.getPrivateKeyFromSeed({ 45 | seedHash, 46 | path: "m", 47 | passphrase 48 | }); 49 | const secret = await cckey.hdwseed.exportSeed({ 50 | seedHash, 51 | passphrase 52 | }); 53 | 54 | await cckey.hdwseed.deleteSeed({ seedHash }); 55 | expect(await cckey.hdwseed.getSeedHashes()).not.toContain(seedHash); 56 | 57 | expect(await cckey.hdwseed.importSeed({ secret, passphrase })).toEqual( 58 | seedHash 59 | ); 60 | expect( 61 | await await cckey.hdwseed.getPrivateKeyFromSeed({ 62 | seedHash, 63 | path: "m", 64 | passphrase 65 | }) 66 | ).toEqual(masterKey); 67 | }); 68 | 69 | test("export raw and import raw", async () => { 70 | const seedHash = await cckey.hdwseed.createSeed({ passphrase }); 71 | const masterKey = await cckey.hdwseed.getPrivateKeyFromSeed({ 72 | seedHash, 73 | path: "m", 74 | passphrase 75 | }); 76 | const seed = await cckey.hdwseed.exportRawSeed({ 77 | seedHash, 78 | passphrase 79 | }); 80 | 81 | await cckey.hdwseed.deleteSeed({ seedHash }); 82 | expect(await cckey.hdwseed.getSeedHashes()).not.toContain(seedHash); 83 | 84 | expect(await cckey.hdwseed.importRawSeed({ seed, passphrase })).toEqual( 85 | seedHash 86 | ); 87 | expect( 88 | await await cckey.hdwseed.getPrivateKeyFromSeed({ 89 | seedHash, 90 | path: "m", 91 | passphrase 92 | }) 93 | ).toEqual(masterKey); 94 | }); 95 | 96 | test("export mnemonic and import mnemonic", async () => { 97 | const seedHash = await cckey.hdwseed.createSeed({ passphrase }); 98 | const masterKey = await cckey.hdwseed.getPrivateKeyFromSeed({ 99 | seedHash, 100 | path: "m", 101 | passphrase 102 | }); 103 | const mnemonic = await cckey.hdwseed.exportMnemonic({ 104 | seedHash, 105 | passphrase 106 | }); 107 | 108 | await cckey.hdwseed.deleteSeed({ seedHash }); 109 | expect(await cckey.hdwseed.getSeedHashes()).not.toContain(seedHash); 110 | 111 | expect( 112 | await cckey.hdwseed.importMnemonic({ mnemonic, passphrase }) 113 | ).toEqual(seedHash); 114 | expect( 115 | await await cckey.hdwseed.getPrivateKeyFromSeed({ 116 | seedHash, 117 | path: "m", 118 | passphrase 119 | }) 120 | ).toEqual(masterKey); 121 | }); 122 | 123 | test.each([ 124 | [ 125 | "d0a8182a44b71802ba85d35fad626c6275a82afb943dfee10d851140a61f74df", 126 | "03013cde78fc0c9a1075585fc5b8d911063d10cb2c9afe9969eb37e64e1225e631ee32fd24b85a293012d5cd75aef8e832158aee4f9dc516d506983f6cb65974", 127 | "88c4410d6003f5b1f0a69d02b6df78b0f65b52eed496ac8d9779f31c7e73b864", 128 | "11d0905febae1234a37f66330f4548effe4076f7aee9aeb8370cb247ceb7ae9138cb626f955db35a24c51a4f881927968e37cad808c17a41abea5f07ac45b83800" 129 | ], 130 | [ 131 | "0000000000000000000000000000000000000000000000000000000000000000", 132 | "5660b70c8770245fb97ce9a811885e8045a1f333a799dcd3035788606cc557544965a0da8b76d6292adff9e9b38190b842629184e62c76ee028e97bc845f59bc", 133 | "235b34cd7c9f6d7e4595ffe9ae4b1cb5606df8aca2b527d20a07c8f56b2342f4", 134 | "6ca5db955e9c82e07529d3ebee8336dec4a44837cf9c453618841f08f841f6f46a4083a01c322c1a89d7fc9a05bf4e2d0cacc2f0da67fda35a83fd4ce739ce4401" 135 | ], 136 | [ 137 | "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 138 | "115e0cb655e65c687cd435a03c73503da216a50176023d2537f54334534bad27c2bc9f76b34a2fe68fc2a3c3c9af2922dceb904c9cd18a9e994ee2243b5da861", 139 | "8472fc35dbe9f8ccf7ed306295e84902c0e606e576e5cb3f6c32d98537a21282", 140 | "36106ab36b67d3947a072bd097613c9dd7612c1bb88ad5b3ce01c3e84e1216bf17d796a797c976e5e8f6180ca3503b6baeeefe80a3dcbedb1e9a0edcdd1343e900" 141 | ], 142 | [ 143 | "ffffffffffffffffffffffffffffffff", 144 | "e01aa32243f6744a2607466304e326adc7cb77e67363341967bf4700be45391dc9a0066b1c6664a432505a20f714f474abe1a7350988c69ea886840b48af2570", 145 | "f1897f8c5fd5e11814f80c63eb21cc0c63d1623008f895e83c0e9a770fb66544", 146 | "11fca78e1985e951c9cc7a866626741cefe972a24dd1b68a5c53940436b33a026e0c4736b6b6bc4b3b50600cf8c780e4ca01772272d3664939c571d29fa5a1d800" 147 | ] 148 | ])( 149 | "import fixed raw seed and check: %p", 150 | async (seed, pubkey, privkey, sign) => { 151 | const message = 152 | "0000000000000000000000000000000000000000000000000000000000000000"; 153 | const seedHash = await cckey.hdwseed.importRawSeed({ 154 | seed, 155 | passphrase 156 | }); 157 | expect( 158 | await cckey.hdwseed.getPublicKeyFromSeed({ 159 | seedHash, 160 | path: "m", 161 | passphrase 162 | }) 163 | ).toEqual(pubkey); 164 | expect( 165 | await cckey.hdwseed.getPrivateKeyFromSeed({ 166 | seedHash, 167 | path: "m", 168 | passphrase 169 | }) 170 | ).toEqual(privkey); 171 | expect( 172 | await cckey.hdwseed.signFromSeed({ 173 | seedHash, 174 | path: "m", 175 | message, 176 | passphrase 177 | }) 178 | ).toEqual(sign); 179 | } 180 | ); 181 | }); 182 | -------------------------------------------------------------------------------- /src/model/hdkeys.ts: -------------------------------------------------------------------------------- 1 | import { getPublicFromPrivate, signEcdsa } from "codechain-primitives"; 2 | import * as _ from "lodash"; 3 | import { Context } from "../context"; 4 | import { ErrorCode, KeystoreError } from "../logic/error"; 5 | import { decode, encode } from "../logic/hdstorage"; 6 | import { 7 | PrivateKey, 8 | PublicKey, 9 | SecretSeedStorage, 10 | Seed, 11 | SeedHash 12 | } from "../types"; 13 | import { KeyType } from "./keytypes"; 14 | 15 | const Mnemonic = require("bitcore-mnemonic"); 16 | const Random = Mnemonic.bitcore.crypto.Random; 17 | 18 | // Seed: Seed(entropy) of a mnemonic code. Not a direct seed of a HD wallet. 19 | // See the definition: 20 | // https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki 21 | 22 | export async function getSeedHashes(context: Context): Promise { 23 | const rows: any = await context.db.get(KeyType.HDWSeed).value(); 24 | return _.map( 25 | rows, 26 | (storage: SecretSeedStorage) => storage.seedHash 27 | ) as SeedHash[]; 28 | } 29 | 30 | export async function exportSeed( 31 | context: Context, 32 | params: { seedHash: SeedHash; passphrase: string } 33 | ): Promise { 34 | const secret = await getSecretSeedStorage(context, params); 35 | if (secret == null) { 36 | throw new KeystoreError(ErrorCode.NoSuchSeedHash); 37 | } 38 | await decode(secret, params.passphrase); 39 | return secret; 40 | } 41 | 42 | export async function exportRawSeed( 43 | context: Context, 44 | params: { seedHash: SeedHash; passphrase: string } 45 | ): Promise { 46 | const secret = await getSecretSeedStorage(context, params); 47 | if (secret == null) { 48 | throw new KeystoreError(ErrorCode.NoSuchSeedHash); 49 | } 50 | return decode(secret, params.passphrase); 51 | } 52 | 53 | export async function exportMnemonic( 54 | context: Context, 55 | params: { 56 | seedHash: SeedHash; 57 | passphrase: string; 58 | } 59 | ): Promise { 60 | const secret = await getSecretSeedStorage(context, params); 61 | if (secret == null) { 62 | throw new KeystoreError(ErrorCode.NoSuchSeedHash); 63 | } 64 | const seed = await decode(secret, params.passphrase); 65 | const code = new Mnemonic(Buffer.from(seed, "hex")); 66 | 67 | return code.toString(); 68 | } 69 | 70 | export async function importSeed( 71 | context: Context, 72 | params: { 73 | secret: SecretSeedStorage; 74 | passphrase: string; 75 | } 76 | ): Promise { 77 | const seed = await decode(params.secret, params.passphrase); 78 | return importSeedToDB(context, { 79 | seed, 80 | passphrase: params.passphrase, 81 | meta: params.secret.meta 82 | }); 83 | } 84 | 85 | export function importRawSeed( 86 | context: Context, 87 | params: { 88 | seed: Seed; 89 | passphrase?: string; 90 | meta?: string; 91 | } 92 | ): Promise { 93 | return importSeedToDB(context, params); 94 | } 95 | 96 | export async function importMnemonic( 97 | context: Context, 98 | params: { 99 | mnemonic: string; 100 | passphrase?: string; 101 | meta?: string; 102 | } 103 | ): Promise { 104 | // This code is from Mnemonic.isValid(). 105 | // There's no way to get a seed from a code in bitcore-mnemonic, 106 | // (.toSeed() generates the seed of the HD wallet) 107 | // so copied .isValid() code and reused buf variable to convert. 108 | const { mnemonic } = params; 109 | const wordlist = Mnemonic._getDictionary(mnemonic); 110 | 111 | if (!wordlist) { 112 | throw new KeystoreError(ErrorCode.WrongMnemonicString); 113 | } 114 | 115 | const words = mnemonic.split(" "); 116 | let bin = ""; 117 | for (const word of words) { 118 | const ind = wordlist.indexOf(word); 119 | if (ind < 0) { 120 | throw new KeystoreError(ErrorCode.WrongMnemonicString); 121 | } 122 | bin = bin + ("00000000000" + ind.toString(2)).slice(-11); 123 | } 124 | 125 | const cs = bin.length / 33; 126 | const hashBits = bin.slice(-cs); 127 | const nonhashBits = bin.slice(0, bin.length - cs); 128 | const buf = new Buffer(nonhashBits.length / 8); 129 | for (let i = 0; i < nonhashBits.length / 8; i++) { 130 | buf.writeUInt8(parseInt(bin.slice(i * 8, (i + 1) * 8), 2), i); 131 | } 132 | const expectedHashBits = Mnemonic._entropyChecksum(buf); 133 | if (expectedHashBits !== hashBits) { 134 | throw new KeystoreError(ErrorCode.WrongMnemonicString); 135 | } 136 | 137 | const seed = buf.toString("hex"); 138 | return importSeedToDB(context, { 139 | ...params, 140 | seed 141 | }); 142 | } 143 | 144 | export async function createSeed( 145 | context: Context, 146 | params: { seedLength?: number; passphrase?: string; meta?: string } 147 | ): Promise { 148 | const { seedLength = 128 } = params; 149 | if (seedLength % 32 !== 0 || seedLength < 128) { 150 | throw new KeystoreError(ErrorCode.WrongSeedLength); 151 | } 152 | const seed = Random.getRandomBuffer(seedLength / 8).toString("hex"); 153 | return importSeedToDB(context, { 154 | ...params, 155 | seed 156 | }); 157 | } 158 | 159 | export async function deleteSeed( 160 | context: Context, 161 | params: { seedHash: SeedHash } 162 | ): Promise { 163 | const secret = await getSecretSeedStorage(context, params); 164 | if (secret == null) { 165 | return false; 166 | } 167 | 168 | await removeKey(context, params); 169 | return true; 170 | } 171 | 172 | export async function getPrivateKeyFromSeed( 173 | context: Context, 174 | params: { 175 | seedHash: SeedHash; 176 | path: string; 177 | passphrase: string; 178 | } 179 | ): Promise { 180 | const secret = await getSecretSeedStorage(context, params); 181 | if (secret == null) { 182 | throw new KeystoreError(ErrorCode.NoSuchSeedHash); 183 | } 184 | 185 | const seed = await decode(secret, params.passphrase); 186 | const code = new Mnemonic(Buffer.from(seed, "hex")); 187 | const masterKey = code.toHDPrivateKey(); 188 | const derivedKey = masterKey.derive(params.path); 189 | const privateKey = derivedKey.privateKey.toString(); 190 | 191 | return privateKey; 192 | } 193 | 194 | export async function getPublicKeyFromSeed( 195 | context: Context, 196 | params: { 197 | seedHash: SeedHash; 198 | path: string; 199 | passphrase: string; 200 | } 201 | ): Promise { 202 | const privateKey = await getPrivateKeyFromSeed(context, params); 203 | const publicKey = getPublicFromPrivate(privateKey); 204 | 205 | return publicKey; 206 | } 207 | 208 | export async function signFromSeed( 209 | context: Context, 210 | params: { 211 | seedHash: SeedHash; 212 | path: string; 213 | message: string; 214 | passphrase: string; 215 | } 216 | ): Promise { 217 | const privateKey = await getPrivateKeyFromSeed(context, params); 218 | 219 | return signEcdsa(params.message, privateKey); 220 | } 221 | 222 | async function importSeedToDB( 223 | context: Context, 224 | params: { 225 | seed: Seed; 226 | passphrase?: string; 227 | meta?: string; 228 | } 229 | ): Promise { 230 | const passphrase = params.passphrase || ""; 231 | const meta = params.meta || "{}"; 232 | 233 | const secret = await encode(params.seed, passphrase, meta); 234 | const rows: any = context.db.get(KeyType.HDWSeed); 235 | await rows.push(secret).write(); 236 | return secret.seedHash; 237 | } 238 | 239 | async function getSecretSeedStorage( 240 | context: Context, 241 | params: { seedHash: SeedHash } 242 | ): Promise { 243 | const collection: any = context.db.get(KeyType.HDWSeed); 244 | const secret = await collection 245 | .find( 246 | (secretStorage: SecretSeedStorage) => 247 | secretStorage.seedHash === params.seedHash 248 | ) 249 | .value(); 250 | 251 | if (secret == null) { 252 | return null; 253 | } 254 | return secret as SecretSeedStorage; 255 | } 256 | 257 | async function removeKey( 258 | context: Context, 259 | params: { seedHash: SeedHash } 260 | ): Promise { 261 | const collection: any = context.db.get(KeyType.HDWSeed); 262 | await collection 263 | .remove( 264 | (secret: SecretSeedStorage) => secret.seedHash === params.seedHash 265 | ) 266 | .write(); 267 | } 268 | 269 | export async function getMeta( 270 | context: Context, 271 | params: { seedHash: SeedHash } 272 | ): Promise { 273 | const secret = await getSecretSeedStorage(context, params); 274 | if (secret == null) { 275 | throw new KeystoreError(ErrorCode.NoSuchKey); 276 | } 277 | return secret.meta; 278 | } 279 | 280 | export async function save(context: Context): Promise { 281 | return await context.db.get(KeyType.HDWSeed).value(); 282 | } 283 | 284 | export async function load( 285 | context: Context, 286 | value: SecretSeedStorage[] 287 | ): Promise { 288 | return context.db.set(KeyType.HDWSeed, value).write(); 289 | } 290 | 291 | export async function clear(context: Context): Promise { 292 | await context.db.unset(KeyType.HDWSeed).write(); 293 | } 294 | -------------------------------------------------------------------------------- /src/__test__/platform.spec.ts: -------------------------------------------------------------------------------- 1 | import { CCKey } from "../index"; 2 | import { keyFromPublicKey } from "../model/keys"; 3 | import { KeyType } from "../model/keytypes"; 4 | 5 | describe("platform", () => { 6 | let cckey: CCKey; 7 | beforeEach(async () => { 8 | cckey = await CCKey.create({ dbType: "in-memory" }); 9 | }); 10 | 11 | afterEach(async () => { 12 | cckey.close(); 13 | }); 14 | 15 | test("importRaw", async () => { 16 | const privateKey = 17 | "a05f81608217738d99da8fd227897b87e8890d3c9159b559c7c8bbd408e5fb6e"; 18 | const key = await cckey.platform.importRaw({ 19 | privateKey, 20 | passphrase: "satoshi" 21 | }); 22 | const publicKey = 23 | "0eb7cad828f1b48c97571ac5fde6add42a7f9285a204291cdc2a03007480dc70639d80c57d80ba6bb02fc2237fec1bb357e405e13b7fb8ed4f947fd8f4900abd"; 24 | expect(key).toBe(keyFromPublicKey(KeyType.Platform, publicKey)); 25 | }); 26 | 27 | test("importKey", async () => { 28 | const secret = { 29 | crypto: { 30 | ciphertext: 31 | "4f870523e834408c08ace7df91671a2b603761f0dbbfd93fa31a5dcda9947515", 32 | cipherparams: { iv: "c47d44a36824ee5207cf435795e7e583" }, 33 | cipher: "aes-128-ctr", 34 | kdf: "pbkdf2", 35 | kdfparams: { 36 | dklen: 32, 37 | salt: 38 | "d187b8eaacbed337261728f33d1dbd51f9532dda82d8c7b8abe4860d2505c43f", 39 | c: 262144, 40 | prf: "hmac-sha256" 41 | }, 42 | mac: 43 | "d75a7c45d4c2c4fa4a7b81319e94e27848b188cfed949524f9e6f3b83c66d518" 44 | }, 45 | id: "31ea5ae9-dad4-4a5a-9a8e-9a9de80d619e", 46 | version: 3, 47 | meta: "some meta info" 48 | }; 49 | const key = await cckey.platform.importKey({ 50 | secret, 51 | passphrase: "satoshi" 52 | }); 53 | const publicKey = 54 | "0eb7cad828f1b48c97571ac5fde6add42a7f9285a204291cdc2a03007480dc70639d80c57d80ba6bb02fc2237fec1bb357e405e13b7fb8ed4f947fd8f4900abd"; 55 | expect(key).toBe(keyFromPublicKey(KeyType.Platform, publicKey)); 56 | }); 57 | 58 | test("exportKey", async () => { 59 | const privateKey = 60 | "a05f81608217738d99da8fd227897b87e8890d3c9159b559c7c8bbd408e5fb6e"; 61 | const key = await cckey.platform.importRaw({ 62 | privateKey, 63 | passphrase: "satoshi" 64 | }); 65 | const storage = await cckey.platform.exportKey({ 66 | key, 67 | passphrase: "satoshi" 68 | }); 69 | expect(storage).toHaveProperty("crypto"); 70 | expect(storage.crypto).toHaveProperty("cipher"); 71 | expect(storage.crypto).toHaveProperty("cipherparams"); 72 | expect(storage.crypto).toHaveProperty("ciphertext"); 73 | expect(storage.crypto).toHaveProperty("kdf"); 74 | expect(storage.crypto).toHaveProperty("kdfparams"); 75 | expect(storage.crypto).toHaveProperty("mac"); 76 | expect(storage).toHaveProperty("meta"); 77 | }); 78 | 79 | test("importKeyWithMeta", async () => { 80 | const secret = { 81 | crypto: { 82 | ciphertext: 83 | "4f870523e834408c08ace7df91671a2b603761f0dbbfd93fa31a5dcda9947515", 84 | cipherparams: { iv: "c47d44a36824ee5207cf435795e7e583" }, 85 | cipher: "aes-128-ctr", 86 | kdf: "pbkdf2", 87 | kdfparams: { 88 | dklen: 32, 89 | salt: 90 | "d187b8eaacbed337261728f33d1dbd51f9532dda82d8c7b8abe4860d2505c43f", 91 | c: 262144, 92 | prf: "hmac-sha256" 93 | }, 94 | mac: 95 | "d75a7c45d4c2c4fa4a7b81319e94e27848b188cfed949524f9e6f3b83c66d518" 96 | }, 97 | id: "31ea5ae9-dad4-4a5a-9a8e-9a9de80d619e", 98 | version: 3, 99 | meta: "some meta info" 100 | }; 101 | const key = await cckey.platform.importKey({ 102 | secret, 103 | passphrase: "satoshi" 104 | }); 105 | const storage = await cckey.platform.exportKey({ 106 | key, 107 | passphrase: "satoshi" 108 | }); 109 | expect(storage.meta).toBe("some meta info"); 110 | }); 111 | 112 | test("exportRawKey", async () => { 113 | const privateKey = 114 | "a05f81608217738d99da8fd227897b87e8890d3c9159b559c7c8bbd408e5fb6e"; 115 | const key = await cckey.platform.importRaw({ 116 | privateKey, 117 | passphrase: "satoshi" 118 | }); 119 | const exportedPrivateKey = await cckey.platform.exportRawKey({ 120 | key, 121 | passphrase: "satoshi" 122 | }); 123 | expect(exportedPrivateKey).toBe(privateKey); 124 | }); 125 | 126 | test("createKey", async () => { 127 | const key = await cckey.platform.createKey({ passphrase: "satoshi" }); 128 | expect(key).toBeTruthy(); 129 | expect(key.length).toBe(40); 130 | }); 131 | 132 | test("createKey with an empty passphrase", async () => { 133 | const key = await cckey.platform.createKey({ passphrase: "" }); 134 | expect(key).toBeTruthy(); 135 | expect(key.length).toBe(40); 136 | }); 137 | 138 | test("getKeys", async () => { 139 | let keys = await cckey.platform.getKeys(); 140 | expect(keys.length).toBe(0); 141 | 142 | const key1 = await cckey.platform.createKey({ passphrase: "satoshi" }); 143 | const key2 = await cckey.platform.createKey({ passphrase: "satoshi" }); 144 | keys = await cckey.platform.getKeys(); 145 | expect(keys).toEqual([key1, key2]); 146 | }); 147 | 148 | test("deleteKey", async () => { 149 | const passphrase = "satoshi"; 150 | const key1 = await cckey.platform.createKey({ passphrase }); 151 | const key2 = await cckey.platform.createKey({ passphrase }); 152 | const originPublicKey2 = await cckey.platform.getPublicKey({ 153 | key: key2, 154 | passphrase 155 | }); 156 | await cckey.platform.deleteKey({ key: key1 }); 157 | 158 | const keys = await cckey.platform.getKeys(); 159 | expect(keys).toEqual([key2]); 160 | 161 | const publicKey1 = await cckey.platform.getPublicKey({ 162 | key: key1, 163 | passphrase 164 | }); 165 | const publicKey2 = await cckey.platform.getPublicKey({ 166 | key: key2, 167 | passphrase 168 | }); 169 | expect(publicKey1).toEqual(null); 170 | expect(publicKey2).toEqual(originPublicKey2); 171 | }); 172 | 173 | test("exportAndImport", async () => { 174 | const createdKey = await cckey.platform.createKey({ 175 | passphrase: "satoshi" 176 | }); 177 | expect(createdKey).toBeTruthy(); 178 | expect(createdKey.length).toBe(40); 179 | 180 | const secret = await cckey.platform.exportKey({ 181 | key: createdKey, 182 | passphrase: "satoshi" 183 | }); 184 | expect(secret).toHaveProperty("crypto"); 185 | expect(secret.crypto).toHaveProperty("cipher"); 186 | expect(secret.crypto).toHaveProperty("cipherparams"); 187 | expect(secret.crypto).toHaveProperty("ciphertext"); 188 | expect(secret.crypto).toHaveProperty("kdf"); 189 | expect(secret.crypto).toHaveProperty("kdfparams"); 190 | expect(secret.crypto).toHaveProperty("mac"); 191 | 192 | const importedKey = await cckey.platform.importKey({ 193 | secret, 194 | passphrase: "satoshi" 195 | }); 196 | expect(createdKey).toBe(importedKey); 197 | }); 198 | 199 | test("createWithoutMeta", async () => { 200 | const createdKey = await cckey.platform.createKey({ 201 | passphrase: "satoshi" 202 | }); 203 | const meta = await cckey.platform.getMeta({ key: createdKey }); 204 | expect(meta).toBe("{}"); 205 | }); 206 | 207 | test("createWithMeta", async () => { 208 | const createdKey = await cckey.platform.createKey({ 209 | passphrase: "satoshi", 210 | meta: '{"name": "test"}' 211 | }); 212 | const meta = await cckey.platform.getMeta({ key: createdKey }); 213 | expect(meta).toBe('{"name": "test"}'); 214 | }); 215 | 216 | test("clear removes key", async () => { 217 | const createdKey = await cckey.platform.createKey({ 218 | passphrase: "satoshi" 219 | }); 220 | expect(await cckey.platform.getKeys()).toEqual([createdKey]); 221 | await cckey.platform.clear(); 222 | expect(await cckey.platform.getKeys()).toEqual([]); 223 | }); 224 | 225 | test("getPublicKey", async () => { 226 | const privateKey = 227 | "a05f81608217738d99da8fd227897b87e8890d3c9159b559c7c8bbd408e5fb6e"; 228 | const expectedPublicKey = 229 | "0eb7cad828f1b48c97571ac5fde6add42a7f9285a204291cdc2a03007480dc70639d80c57d80ba6bb02fc2237fec1bb357e405e13b7fb8ed4f947fd8f4900abd"; 230 | const passphrase = "satoshi"; 231 | const key = await cckey.platform.importRaw({ 232 | privateKey, 233 | passphrase 234 | }); 235 | const publicKey = await cckey.platform.getPublicKey({ 236 | key, 237 | passphrase 238 | }); 239 | if (publicKey == null) { 240 | throw Error("Cannot get a public key"); 241 | } 242 | expect(publicKey).toBe(expectedPublicKey); 243 | expect(publicKey.length).toBe(128); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { getPublicFromPrivate } from "codechain-primitives"; 2 | import { closeContext, Context, createContext, storageExist } from "./context"; 3 | import { decode } from "./logic/storage"; 4 | import * as HDKeys from "./model/hdkeys"; 5 | import { initialize as dbInitialize } from "./model/initialize"; 6 | import * as Keys from "./model/keys"; 7 | 8 | import { KeyType } from "./model/keytypes"; 9 | import { 10 | Key, 11 | PrivateKey, 12 | PublicKey, 13 | SecretSeedStorage, 14 | SecretStorage, 15 | Seed, 16 | SeedHash 17 | } from "./types"; 18 | 19 | export { SecretStorage }; 20 | 21 | export interface KeyStore { 22 | getKeys(): Promise; 23 | importRaw(params: { 24 | privateKey: PrivateKey; 25 | passphrase?: string; 26 | meta?: string; 27 | }): Promise; 28 | exportKey(params: { key: Key; passphrase: string }): Promise; 29 | importKey(params: { 30 | secret: SecretStorage; 31 | passphrase: string; 32 | }): Promise; 33 | exportRawKey(params: { key: Key; passphrase: string }): Promise; 34 | getPublicKey(params: { 35 | key: Key; 36 | passphrase: string; 37 | }): Promise; 38 | createKey(params: { passphrase?: string; meta?: string }): Promise; 39 | deleteKey(params: { key: Key }): Promise; 40 | sign(params: { 41 | key: Key; 42 | message: string; 43 | passphrase: string; 44 | }): Promise; 45 | 46 | getMeta(params: { key: Key }): Promise; 47 | 48 | save(): Promise; 49 | load(value: SecretStorage[]): Promise; 50 | 51 | clear(): Promise; 52 | } 53 | 54 | export interface HDWKeyStore { 55 | getSeedHashes(): Promise; 56 | importSeed(params: { 57 | secret: SecretSeedStorage; 58 | passphrase?: string; 59 | }): Promise; 60 | importRawSeed(params: { 61 | seed: Seed; 62 | passphrase?: string; 63 | meta?: string; 64 | }): Promise; 65 | importMnemonic(params: { 66 | mnemonic: string; 67 | passphrase?: string; 68 | meta?: string; 69 | }): Promise; 70 | exportSeed(params: { 71 | seedHash: SeedHash; 72 | passphrase: string; 73 | }): Promise; 74 | exportRawSeed(params: { 75 | seedHash: SeedHash; 76 | passphrase: string; 77 | }): Promise; 78 | exportMnemonic(params: { 79 | seedHash: SeedHash; 80 | passphrase: string; 81 | }): Promise; 82 | createSeed(params: { 83 | seedLength?: number; 84 | passphrase?: string; 85 | meta?: string; 86 | }): Promise; 87 | deleteSeed(params: { seedHash: SeedHash }): Promise; 88 | getPublicKeyFromSeed(params: { 89 | seedHash: SeedHash; 90 | path: string; 91 | passphrase?: string; 92 | }): Promise; 93 | getPrivateKeyFromSeed(params: { 94 | seedHash: SeedHash; 95 | path: string; 96 | passphrase?: string; 97 | }): Promise; 98 | signFromSeed(params: { 99 | seedHash: SeedHash; 100 | path: string; 101 | message: string; 102 | passphrase: string; 103 | }): Promise; 104 | 105 | getMeta(params: { seedHash: SeedHash }): Promise; 106 | 107 | save(): Promise; 108 | load(value: SecretSeedStorage[]): Promise; 109 | 110 | clear(): Promise; 111 | } 112 | 113 | class CCKey { 114 | public static CCKey = CCKey; 115 | 116 | public static async create( 117 | params: { 118 | dbType?: string; 119 | dbPath?: string; 120 | } = {} 121 | ): Promise { 122 | const dbType = params.dbType || "persistent"; 123 | const dbPath = params.dbPath || "keystore.db"; 124 | const context = await createContext({ 125 | dbType, 126 | dbPath 127 | }); 128 | return new CCKey(context); 129 | } 130 | public static async exist( 131 | params: { 132 | dbType?: string; 133 | dbPath?: string; 134 | } = {} 135 | ): Promise { 136 | const dbType = params.dbType || "persistent"; 137 | const dbPath = params.dbPath || "keystore.db"; 138 | return storageExist({ dbType, dbPath }); 139 | } 140 | 141 | public platform: KeyStore = createKeyStore(this.context, KeyType.Platform); 142 | public asset: KeyStore = createKeyStore(this.context, KeyType.Asset); 143 | public hdwseed: HDWKeyStore = createHDKeyStore(this.context); 144 | 145 | private constructor(private context: Context) {} 146 | 147 | public getMeta(): Promise { 148 | return this.context.db.get("meta").value(); 149 | } 150 | 151 | public setMeta(meta: string): Promise { 152 | return this.context.db.set("meta", meta).write(); 153 | } 154 | 155 | public close(): Promise { 156 | return closeContext(this.context); 157 | } 158 | 159 | public async migrate( 160 | data: string, 161 | params: { assetPassphrase: string[]; platformPassphrase: string[] } 162 | ): Promise { 163 | const old = JSON.parse(data); 164 | const platform_keys: any[] = old.platform_keys; 165 | const asset_keys: any[] = old.asset_keys; 166 | if (platform_keys.length !== params.platformPassphrase.length) { 167 | throw new Error( 168 | "The length of platform key doesn't match with the length of passphrase" 169 | ); 170 | } 171 | if (asset_keys.length !== params.assetPassphrase.length) { 172 | throw new Error( 173 | "The length of asset key doesn't match with the length of passphrase" 174 | ); 175 | } 176 | const platform = await Promise.all( 177 | platform_keys 178 | .map(key => JSON.parse(key.secret)) 179 | .map(async (storage, i) => { 180 | const passphrase = params.platformPassphrase[i]; 181 | const privateKey = await decode(storage, passphrase); 182 | const publicKey = getPublicFromPrivate(privateKey); 183 | storage.address = Keys.keyFromPublicKey( 184 | KeyType.Platform, 185 | publicKey 186 | ); 187 | return storage; 188 | }) 189 | ); 190 | const asset = await Promise.all( 191 | asset_keys 192 | .map(key => JSON.parse(key.secret)) 193 | .map(async (storage, i) => { 194 | const passphrase = params.assetPassphrase[i]; 195 | const privateKey = await decode(storage, passphrase); 196 | const publicKey = getPublicFromPrivate(privateKey); 197 | storage.address = Keys.keyFromPublicKey( 198 | KeyType.Asset, 199 | publicKey 200 | ); 201 | return storage; 202 | }) 203 | ); 204 | return JSON.stringify({ 205 | meta: "{}", 206 | platform, 207 | asset, 208 | hdwseed: [] 209 | }); 210 | } 211 | 212 | public async save(): Promise { 213 | const meta = await this.getMeta(); 214 | const platform = await this.platform.save(); 215 | const asset = await this.asset.save(); 216 | const hdwseed = await this.hdwseed.save(); 217 | return JSON.stringify({ 218 | meta, 219 | platform, 220 | asset, 221 | hdwseed 222 | }); 223 | } 224 | 225 | public async load(value: string): Promise { 226 | const data = JSON.parse(value); 227 | await this.setMeta(data.meta); 228 | await this.platform.load(data.platform); 229 | await this.asset.load(data.asset); 230 | await this.hdwseed.load(data.hdwseed); 231 | } 232 | 233 | public async clear(): Promise { 234 | await this.context.db.unset("meta").write(); 235 | await this.platform.clear(); 236 | await this.asset.clear(); 237 | await this.hdwseed.clear(); 238 | await dbInitialize(this.context.db); 239 | } 240 | } 241 | 242 | function createKeyStore(context: Context, keyType: KeyType): KeyStore { 243 | return { 244 | getKeys: () => { 245 | return Keys.getKeys(context, { keyType }); 246 | }, 247 | 248 | importRaw: (params: { 249 | privateKey: PrivateKey; 250 | passphrase?: string; 251 | meta?: string; 252 | }) => { 253 | return Keys.importRaw(context, { ...params, keyType }); 254 | }, 255 | 256 | exportKey: (params: { key: Key; passphrase: string }) => { 257 | return Keys.exportKey(context, { ...params, keyType }); 258 | }, 259 | 260 | importKey: (params: { secret: SecretStorage; passphrase: string }) => { 261 | return Keys.importKey(context, { ...params, keyType }); 262 | }, 263 | 264 | exportRawKey: (params: { key: Key; passphrase: string }) => { 265 | return Keys.exportRawKey(context, { ...params, keyType }); 266 | }, 267 | 268 | getPublicKey: (params: { key: Key; passphrase: string }) => { 269 | return Keys.getPublicKey(context, { ...params, keyType }); 270 | }, 271 | 272 | createKey: (params: { passphrase?: string; meta?: string }) => { 273 | return Keys.createKey(context, { ...params, keyType }); 274 | }, 275 | 276 | deleteKey: (params: { key: Key }) => { 277 | return Keys.deleteKey(context, { ...params, keyType }); 278 | }, 279 | 280 | sign: (params: { key: Key; message: string; passphrase: string }) => { 281 | return Keys.sign(context, { ...params, keyType }); 282 | }, 283 | 284 | getMeta: (params: { key: Key }) => { 285 | return Keys.getMeta(context, { ...params, keyType }); 286 | }, 287 | 288 | save: () => { 289 | return Keys.save(context, { keyType }); 290 | }, 291 | 292 | load: (value: SecretStorage[]) => { 293 | return Keys.load(context, value, { keyType }); 294 | }, 295 | 296 | clear: () => { 297 | return Keys.clear(context, { keyType }); 298 | } 299 | }; 300 | } 301 | 302 | function createHDKeyStore(context: Context): HDWKeyStore { 303 | return { 304 | getSeedHashes: () => { 305 | return HDKeys.getSeedHashes(context); 306 | }, 307 | 308 | importSeed: (params: { 309 | secret: SecretSeedStorage; 310 | passphrase: string; 311 | }) => { 312 | return HDKeys.importSeed(context, params); 313 | }, 314 | 315 | importRawSeed: (params: { 316 | seed: Seed; 317 | passphrase?: string; 318 | meta?: string; 319 | }) => { 320 | return HDKeys.importRawSeed(context, params); 321 | }, 322 | 323 | importMnemonic: (params: { 324 | mnemonic: string; 325 | passphrase?: string; 326 | meta?: string; 327 | }) => { 328 | return HDKeys.importMnemonic(context, params); 329 | }, 330 | 331 | exportSeed: (params: { seedHash: SeedHash; passphrase: string }) => { 332 | return HDKeys.exportSeed(context, params); 333 | }, 334 | 335 | exportRawSeed: (params: { seedHash: SeedHash; passphrase: string }) => { 336 | return HDKeys.exportRawSeed(context, params); 337 | }, 338 | 339 | exportMnemonic: (params: { 340 | seedHash: SeedHash; 341 | passphrase: string; 342 | }) => { 343 | return HDKeys.exportMnemonic(context, params); 344 | }, 345 | 346 | createSeed: (params: { 347 | seedLength?: number; 348 | passphrase?: string; 349 | meta?: string; 350 | }) => { 351 | return HDKeys.createSeed(context, params); 352 | }, 353 | 354 | deleteSeed: (params: { seedHash: SeedHash }) => { 355 | return HDKeys.deleteSeed(context, params); 356 | }, 357 | 358 | getPublicKeyFromSeed: (params: { 359 | seedHash: SeedHash; 360 | path: string; 361 | passphrase: string; 362 | }) => { 363 | return HDKeys.getPublicKeyFromSeed(context, params); 364 | }, 365 | 366 | getPrivateKeyFromSeed: (params: { 367 | seedHash: SeedHash; 368 | path: string; 369 | passphrase: string; 370 | }) => { 371 | return HDKeys.getPrivateKeyFromSeed(context, params); 372 | }, 373 | 374 | signFromSeed: (params: { 375 | seedHash: SeedHash; 376 | path: string; 377 | message: string; 378 | passphrase: string; 379 | }) => { 380 | return HDKeys.signFromSeed(context, params); 381 | }, 382 | 383 | getMeta: (params: { seedHash: SeedHash }) => { 384 | return HDKeys.getMeta(context, params); 385 | }, 386 | 387 | save: () => { 388 | return HDKeys.save(context); 389 | }, 390 | 391 | load: (value: SecretSeedStorage[]) => { 392 | return HDKeys.load(context, value); 393 | }, 394 | 395 | clear: () => { 396 | return HDKeys.clear(context); 397 | } 398 | }; 399 | } 400 | 401 | export { CCKey }; 402 | 403 | module.exports = CCKey; 404 | --------------------------------------------------------------------------------