[feat] added keystore and encrypt/decrypt method to Account

@types
neeboo 6 years ago
parent d49ac9baf5
commit 0a43d48ee7
  1. 4
      examples/testNode.js
  2. 1
      package.json
  3. 50
      packages/harmony-account/src/account.ts
  4. 1
      packages/harmony-crypto/package.json
  5. 53
      packages/harmony-crypto/src/bytes.ts
  6. 4
      packages/harmony-crypto/src/index.ts
  7. 171
      packages/harmony-crypto/src/keystore.ts
  8. 39
      packages/harmony-crypto/src/types.ts
  9. 11
      packages/harmony-network/package.json
  10. 8
      packages/harmony-network/tsconfig.json
  11. 2
      scripts/packages.js
  12. 7
      scripts/packagesList.js
  13. 2
      scripts/packagesTs.ts
  14. 3
      tsconfig.json
  15. 16
      typings/aes-js.d.ts
  16. 11
      typings/scrypt.d.ts

@ -21,3 +21,7 @@ const c = Account.add(importKey);
c.addShard('newShard'); c.addShard('newShard');
console.log(c.getShardsCount); console.log(c.getShardsCount);
c.toFile('123').then((f) => {
c.fromFile(f, '123').then(console.log);
});

@ -52,6 +52,7 @@
"@types/jest": "^23.3.1", "@types/jest": "^23.3.1",
"@types/jest-json-schema": "^1.2.0", "@types/jest-json-schema": "^1.2.0",
"@types/node": "^10.5.6", "@types/node": "^10.5.6",
"@types/pbkdf2": "^3.0.0",
"@types/uuid": "^3.4.4", "@types/uuid": "^3.4.4",
"@types/valid-url": "^1.0.2", "@types/valid-url": "^1.0.2",
"@types/webpack": "^4.4.17", "@types/webpack": "^4.4.17",

@ -3,6 +3,10 @@ import {
getAddressFromPrivateKey, getAddressFromPrivateKey,
getPubkeyFromPrivateKey, getPubkeyFromPrivateKey,
toChecksumAddress, toChecksumAddress,
encrypt,
decrypt,
EncryptOptions,
Keystore,
} from '@harmony/crypto'; } from '@harmony/crypto';
import { isPrivateKey } from '@harmony/utils'; import { isPrivateKey } from '@harmony/utils';
@ -49,8 +53,10 @@ class Account {
} }
constructor(key?: string) { constructor(key?: string) {
if (key === null) { if (!key) {
this._new(); this._new();
} else {
this._import(key);
} }
} }
@ -61,16 +67,46 @@ class Account {
addShard(id: ShardId): void { addShard(id: ShardId): void {
if (this.shards && this.shards.has('default')) { if (this.shards && this.shards.has('default')) {
this.shards.set(id, ''); 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<string> {
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<Account> {
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 * @function getBalance get Account's balance
* @return {type} {description} * @return {type} {description}
*/ */
getBalance() { async getBalance(): Promise<string> {
// console.log() // console.log()
return '';
} }
/** /**
@ -82,11 +118,7 @@ class Account {
if (!isPrivateKey(prv)) { if (!isPrivateKey(prv)) {
throw new Error('key gen failed'); throw new Error('key gen failed');
} }
this.privateKey = prv; return this._import(prv);
this.publicKey = getPubkeyFromPrivateKey(this.privateKey);
this.address = getAddressFromPrivateKey(this.privateKey);
return this;
} }
/** /**
@ -101,7 +133,7 @@ class Account {
this.privateKey = key; this.privateKey = key;
this.publicKey = getPubkeyFromPrivateKey(this.privateKey); this.publicKey = getPubkeyFromPrivateKey(this.privateKey);
this.address = getAddressFromPrivateKey(this.privateKey); this.address = getAddressFromPrivateKey(this.privateKey);
this.shards = new Map().set('default', '');
return this; return this;
} }
} }

@ -17,6 +17,7 @@
}, },
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@harmony/utils":"^0.0.1",
"aes-js": "^3.1.2", "aes-js": "^3.1.2",
"bip39": "^2.5.0", "bip39": "^2.5.0",
"bn.js": "^4.11.8", "bn.js": "^4.11.8",

@ -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);
};

@ -1,2 +1,6 @@
export * from './random'; export * from './random';
export * from './keyTool'; export * from './keyTool';
export * from './keystore';
// export types
export * from './types';

@ -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<Buffer>}
*/
async function getDerivedKey(
key: Buffer,
kdf: KDF,
params: KDFParams,
): Promise<Buffer> {
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<string> => {
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<string> => {
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;
};

@ -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;
}

@ -1,8 +1,13 @@
{ {
"name": "harmony-network", "name": "@harmony/network",
"version": "0.0.1", "version": "0.0.1",
"description": "network packages for harmony", "description": "network suites for harmony",
"main": "index.js", "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": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": ["src", "../../typings/**/*.d.ts"]
}

@ -2,5 +2,5 @@ export default [
'harmony-utils', 'harmony-utils',
'harmony-crypto', 'harmony-crypto',
'harmony-account', 'harmony-account',
// 'harmony-network', 'harmony-network',
]; ];

@ -1,10 +1,5 @@
module.exports = [ module.exports = [
// { name: 'LaksaUtil', dest: 'laksa-utils' }, { name: 'HarmonyNetwork', dest: 'harmony-network' },
// { 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: 'HarmonyUtils', dest: 'harmony-utils' }, { name: 'HarmonyUtils', dest: 'harmony-utils' },
{ name: 'HarmonyCrypto', dest: 'harmony-crypto' }, { name: 'HarmonyCrypto', dest: 'harmony-crypto' },
{ name: 'HarmonyAccount', dest: 'harmony-account' }, { name: 'HarmonyAccount', dest: 'harmony-account' },

@ -2,7 +2,7 @@ const packages = [
'harmony-utils', 'harmony-utils',
'harmony-crypto', 'harmony-crypto',
'harmony-account', 'harmony-account',
// 'harmony-network', 'harmony-network',
]; ];
export { packages }; export { packages };

@ -3,6 +3,7 @@
"references": [ "references": [
{ "path": "packages/harmony-account" }, { "path": "packages/harmony-account" },
{ "path": "packages/harmony-crypto" }, { "path": "packages/harmony-crypto" },
{ "path": "packages/harmony-utils" } { "path": "packages/harmony-utils" },
{ "path": "packages/harmony-network" }
] ]
} }

@ -0,0 +1,16 @@
declare module 'aes-js' {
export class Counter {
constructor(iv: Buffer);
setValue(value: number): void;
setBytes(bytes: Array<number> | 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 };
}

@ -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;
}
Loading…
Cancel
Save