diff --git a/examples/testNode.js b/examples/testNode.js index 12a92de..dbcf5d7 100644 --- a/examples/testNode.js +++ b/examples/testNode.js @@ -21,3 +21,7 @@ const c = Account.add(importKey); c.addShard('newShard'); console.log(c.getShardsCount); + +c.toFile('123').then((f) => { + c.fromFile(f, '123').then(console.log); +}); diff --git a/package.json b/package.json index b630696..cf77e07 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@types/jest": "^23.3.1", "@types/jest-json-schema": "^1.2.0", "@types/node": "^10.5.6", + "@types/pbkdf2": "^3.0.0", "@types/uuid": "^3.4.4", "@types/valid-url": "^1.0.2", "@types/webpack": "^4.4.17", diff --git a/packages/harmony-account/src/account.ts b/packages/harmony-account/src/account.ts index 276681c..925835a 100644 --- a/packages/harmony-account/src/account.ts +++ b/packages/harmony-account/src/account.ts @@ -3,6 +3,10 @@ import { getAddressFromPrivateKey, getPubkeyFromPrivateKey, toChecksumAddress, + encrypt, + decrypt, + EncryptOptions, + Keystore, } from '@harmony/crypto'; import { isPrivateKey } from '@harmony/utils'; @@ -49,8 +53,10 @@ class Account { } constructor(key?: string) { - if (key === null) { + if (!key) { this._new(); + } else { + this._import(key); } } @@ -61,16 +67,46 @@ class Account { addShard(id: ShardId): void { if (this.shards && this.shards.has('default')) { this.shards.set(id, ''); + } else { + throw new Error( + 'This account has no default shard or shard is not exist', + ); + } + } + + async toFile(password: string, options?: EncryptOptions): Promise { + if (this.privateKey && isPrivateKey(this.privateKey)) { + const file = await encrypt(this.privateKey, password, options); + return file; + } else { + throw new Error('Encryption failed because PrivateKey is not correct'); + } + } + + async fromFile(keyStore: string, password: string): Promise { + try { + if (!password) { + throw new Error('you must provide password'); + } + const file: Keystore = JSON.parse(keyStore); + const decyptedPrivateKey = await decrypt(file, password); + if (isPrivateKey(decyptedPrivateKey)) { + return Account.add(decyptedPrivateKey); + } else { + throw new Error('decrypted failed'); + } + } catch (error) { + throw error; } - throw new Error('this account has no default shard or shard is not exist'); } /** * @function getBalance get Account's balance * @return {type} {description} */ - getBalance() { + async getBalance(): Promise { // console.log() + return ''; } /** @@ -82,11 +118,7 @@ class Account { if (!isPrivateKey(prv)) { throw new Error('key gen failed'); } - this.privateKey = prv; - this.publicKey = getPubkeyFromPrivateKey(this.privateKey); - this.address = getAddressFromPrivateKey(this.privateKey); - - return this; + return this._import(prv); } /** @@ -101,7 +133,7 @@ class Account { this.privateKey = key; this.publicKey = getPubkeyFromPrivateKey(this.privateKey); this.address = getAddressFromPrivateKey(this.privateKey); - + this.shards = new Map().set('default', ''); return this; } } diff --git a/packages/harmony-crypto/package.json b/packages/harmony-crypto/package.json index 634dab6..fe4de10 100644 --- a/packages/harmony-crypto/package.json +++ b/packages/harmony-crypto/package.json @@ -17,6 +17,7 @@ }, "license": "ISC", "dependencies": { + "@harmony/utils":"^0.0.1", "aes-js": "^3.1.2", "bip39": "^2.5.0", "bn.js": "^4.11.8", diff --git a/packages/harmony-crypto/src/bytes.ts b/packages/harmony-crypto/src/bytes.ts index 1887099..df13895 100644 --- a/packages/harmony-crypto/src/bytes.ts +++ b/packages/harmony-crypto/src/bytes.ts @@ -402,3 +402,56 @@ export function joinSignature(signature: Signature): string { ]), ); } + +/** + * hexToByteArray + * + * Convers a hex string to a Uint8Array + * + * @param {string} hex + * @returns {Uint8Array} + */ +export const hexToByteArray = (hex: string): Uint8Array => { + const res = new Uint8Array(hex.length / 2); + + for (let i = 0; i < hex.length; i += 2) { + res[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + + return res; +}; + +/** + * hexToIntArray + * + * @param {string} hex + * @returns {number[]} + */ +export const hexToIntArray = (hex: string): number[] => { + if (!hex || !isHex(hex)) { + return []; + } + + const res = []; + + for (let i = 0; i < hex.length; i++) { + const c = hex.charCodeAt(i); + const hi = c >> 8; + const lo = c & 0xff; + + hi ? res.push(hi, lo) : res.push(lo); + } + + return res; +}; + +/** + * isHex + * + * @param {string} str - string to be tested + * @returns {boolean} + */ +export const isHex = (str: string): boolean => { + const plain = str.replace('0x', ''); + return /[0-9a-f]*$/i.test(plain); +}; diff --git a/packages/harmony-crypto/src/index.ts b/packages/harmony-crypto/src/index.ts index 57b07d3..2143720 100644 --- a/packages/harmony-crypto/src/index.ts +++ b/packages/harmony-crypto/src/index.ts @@ -1,2 +1,6 @@ export * from './random'; export * from './keyTool'; +export * from './keystore'; + +// export types +export * from './types'; diff --git a/packages/harmony-crypto/src/keystore.ts b/packages/harmony-crypto/src/keystore.ts index e69de29..e07559a 100644 --- a/packages/harmony-crypto/src/keystore.ts +++ b/packages/harmony-crypto/src/keystore.ts @@ -0,0 +1,171 @@ +import aes from 'aes-js'; +import scrypt from 'scrypt.js'; +import { pbkdf2Sync } from 'pbkdf2'; +import uuid from 'uuid'; +import { isPrivateKey } from '@harmony/utils'; +import { randomBytes } from './random'; +import { getAddressFromPrivateKey } from './keyTool'; +import { + concat, + // arrayify, + // hexDataLength, + // hexToByteArray, + hexToIntArray, +} from './bytes'; +import { keccak256 } from './keccak256'; +import { + KDF, + KDFParams, + EncryptOptions, + PBKDF2Params, + ScryptParams, + Keystore, +} from './types'; + +const DEFAULT_ALGORITHM = 'aes-128-ctr'; + +/** + * getDerivedKey + * + * NOTE: only scrypt and pbkdf2 are supported. + * + * @param {Buffer} key - the passphrase + * @param {KDF} kdf - the key derivation function to be used + * @param {KDFParams} params - params for the kdf + * + * @returns {Promise} + */ +async function getDerivedKey( + key: Buffer, + kdf: KDF, + params: KDFParams, +): Promise { + const salt = Buffer.from(params.salt, 'hex'); + + if (kdf === 'pbkdf2') { + const { c, dklen } = params as PBKDF2Params; + return pbkdf2Sync(key, salt, c, dklen, 'sha256'); + } + + if (kdf === 'scrypt') { + const { n, r, p, dklen } = params as ScryptParams; + return scrypt(key, salt, n, r, p, dklen); + } + + throw new Error('Only pbkdf2 and scrypt are supported'); +} + +/** + * This method will map the current Account object to V3Keystore object. + * + * @method encrypt + * + * @param {string} privateKey + * @param {string} password + * @param {object} options + * + * @return {{version, id, address, crypto}} + */ +export const encrypt = async ( + privateKey: string, + password: string, + options?: EncryptOptions, +): Promise => { + if (!isPrivateKey(privateKey)) { + throw new Error('privateKey is not correct'); + } + // TODO: should use isString() to implement this + + if (!password) { + throw new Error('password is not found'); + } + const address = getAddressFromPrivateKey(privateKey); + + const salt = randomBytes(32); + const iv = Buffer.from(randomBytes(16), 'hex'); + const kdf = + options !== undefined ? (options.kdf ? options.kdf : 'scrypt') : 'scrypt'; + const level = + options !== undefined ? (options.level ? options.level : 8192) : 8192; + + const uuidRandom = options !== undefined ? options.uuid : undefined; + + const n = kdf === 'pbkdf2' ? 262144 : level; + const kdfparams = { + salt, + n, + r: 8, + p: 1, + dklen: 32, + }; + + const derivedKey = await getDerivedKey(Buffer.from(password), kdf, kdfparams); + const cipher = new aes.ModeOfOperation.ctr( + derivedKey.slice(0, 16), + new aes.Counter(iv), + ); + + if (!cipher) { + throw new Error('Unsupported cipher'); + } + + const ciphertext = Buffer.from( + cipher.encrypt(Buffer.from(privateKey.replace('0x', ''), 'hex')), + ); + + const mac = keccak256(concat([derivedKey.slice(16, 32), ciphertext])); + + return JSON.stringify({ + version: 3, + id: uuid.v4({ random: uuidRandom || hexToIntArray(randomBytes(16)) }), + address: address.toLowerCase().replace('0x', ''), + crypto: { + ciphertext: ciphertext.toString('hex'), + cipherparams: { + iv: iv.toString('hex'), + }, + cipher: DEFAULT_ALGORITHM, + kdf, + kdfparams, + mac: mac.replace('0x', ''), + }, + }); +}; + +/** + * @function decrypt + * @param {Keystore} keystore - Keystore file + * @param {string} password - password string + * @return {string} privateKey + */ +export const decrypt = async ( + keystore: Keystore, + password: string, +): Promise => { + const ciphertext = Buffer.from(keystore.crypto.ciphertext, 'hex'); + const iv = Buffer.from(keystore.crypto.cipherparams.iv, 'hex'); + const { kdfparams } = keystore.crypto; + + const derivedKey = await getDerivedKey( + Buffer.from(password), + keystore.crypto.kdf, + kdfparams, + ); + + const mac = keccak256(concat([derivedKey.slice(16, 32), ciphertext])).replace( + '0x', + '', + ); + + if (mac.toUpperCase() !== keystore.crypto.mac.toUpperCase()) { + return Promise.reject(new Error('Failed to decrypt.')); + } + + const CTR = aes.ModeOfOperation.ctr; + + const cipher = new CTR(derivedKey.slice(0, 16), new aes.Counter(iv)); + + const decrypted = + '0x' + Buffer.from(cipher.decrypt(ciphertext)).toString('hex'); + return decrypted; +}; diff --git a/packages/harmony-crypto/src/types.ts b/packages/harmony-crypto/src/types.ts index e69de29..8e28d4a 100644 --- a/packages/harmony-crypto/src/types.ts +++ b/packages/harmony-crypto/src/types.ts @@ -0,0 +1,39 @@ +export type KDF = 'pbkdf2' | 'scrypt'; + +export interface PBKDF2Params { + salt: string; + dklen: number; + c: number; +} + +export interface ScryptParams { + salt: string; + dklen: number; + n: number; + r: number; + p: number; +} + +export type KDFParams = PBKDF2Params | ScryptParams; + +export interface EncryptOptions { + kdf?: KDF; + level?: number; + uuid?: number[]; +} + +export interface Keystore { + address: string; + crypto: { + cipher: string; + cipherparams: { + iv: string; + }; + ciphertext: string; + kdf: KDF; + kdfparams: KDFParams; + mac: string; + }; + id: string; + version: 3; +} diff --git a/packages/harmony-network/package.json b/packages/harmony-network/package.json index cce2657..2fa7d54 100644 --- a/packages/harmony-network/package.json +++ b/packages/harmony-network/package.json @@ -1,8 +1,13 @@ { - "name": "harmony-network", + "name": "@harmony/network", "version": "0.0.1", - "description": "network packages for harmony", - "main": "index.js", + "description": "network suites for harmony", + "main": "lib/index.js", + "node": "lib/index.js", + "browser": "dist/index.js", + "module": "dist/index.js", + "jsnext:main": "dist/index.js", + "typings":"lib/index.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/packages/harmony-network/tsconfig.json b/packages/harmony-network/tsconfig.json new file mode 100644 index 0000000..10487be --- /dev/null +++ b/packages/harmony-network/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src", "../../typings/**/*.d.ts"] +} diff --git a/scripts/packages.js b/scripts/packages.js index a10ad93..4e13a93 100644 --- a/scripts/packages.js +++ b/scripts/packages.js @@ -2,5 +2,5 @@ export default [ 'harmony-utils', 'harmony-crypto', 'harmony-account', - // 'harmony-network', + 'harmony-network', ]; diff --git a/scripts/packagesList.js b/scripts/packagesList.js index dbd5a9b..c4f995b 100644 --- a/scripts/packagesList.js +++ b/scripts/packagesList.js @@ -1,10 +1,5 @@ module.exports = [ - // { name: 'LaksaUtil', dest: 'laksa-utils' }, - // { name: 'LaksaZil', dest: 'laksa-zil' }, - // { name: 'LaksaCrypto', dest: 'laksa-core-crypto' }, - // { name: 'LaksaWallet', dest: 'laksa-wallet' }, - // { name: 'LaksaHDWallet', dest: 'laksa-hd-wallet' }, - // { name: 'LaksaContract', dest: 'laksa-core-contract' }, + { name: 'HarmonyNetwork', dest: 'harmony-network' }, { name: 'HarmonyUtils', dest: 'harmony-utils' }, { name: 'HarmonyCrypto', dest: 'harmony-crypto' }, { name: 'HarmonyAccount', dest: 'harmony-account' }, diff --git a/scripts/packagesTs.ts b/scripts/packagesTs.ts index 6b50330..2c8b042 100644 --- a/scripts/packagesTs.ts +++ b/scripts/packagesTs.ts @@ -2,7 +2,7 @@ const packages = [ 'harmony-utils', 'harmony-crypto', 'harmony-account', - // 'harmony-network', + 'harmony-network', ]; export { packages }; diff --git a/tsconfig.json b/tsconfig.json index 5ebd1ca..18f41fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "packages/harmony-account" }, { "path": "packages/harmony-crypto" }, - { "path": "packages/harmony-utils" } + { "path": "packages/harmony-utils" }, + { "path": "packages/harmony-network" } ] } diff --git a/typings/aes-js.d.ts b/typings/aes-js.d.ts new file mode 100644 index 0000000..799196f --- /dev/null +++ b/typings/aes-js.d.ts @@ -0,0 +1,16 @@ +declare module 'aes-js' { + export class Counter { + constructor(iv: Buffer); + setValue(value: number): void; + setBytes(bytes: Array | Buffer | string): void; + increment(): void; + } + + class CTR { + constructor(derivedKey: Buffer, iv: Counter); + encrypt(bytes: Buffer): Uint8Array; + decrypt(bytes: Buffer): Uint8Array; + } + + export const ModeOfOperation: { ctr: typeof CTR }; +} diff --git a/typings/scrypt.d.ts b/typings/scrypt.d.ts new file mode 100644 index 0000000..b6efa20 --- /dev/null +++ b/typings/scrypt.d.ts @@ -0,0 +1,11 @@ +declare module 'scrypt.js' { + export default function scrypt( + key: Buffer, + salt: Buffer, + n: number, + r: number, + p: number, + dklen: number, + progressCB?: (prog: any) => void, + ): Buffer; +}