Skip to content

Commit

Permalink
Merge pull request #23 from JincorTech/feature/issue-22
Browse files Browse the repository at this point in the history
Add ECIES encryption of recovery keys. Closes #22
  • Loading branch information
AlekNS authored Feb 8, 2018
2 parents bf344d7 + 4672b91 commit 9f5400c
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 82 deletions.
4 changes: 2 additions & 2 deletions apiary.apib
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ access to his wallet of ICO dashboard.
"address": "0x7EB6Bea6eCf5aF3eA9c7e9dcb4D71966CFA4c9Eb",
"tokens": [],
"balance": "0",
"mnemonic": "dice bleak brick",
"privateKey": "0x3b15f45e55612341231341234123412341235435231341234"
"mnemonic": "",
"privateKey": ""
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion src/entities/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type BooleanState = { [k: string]: boolean; };
// set true for unknown keys
function setState(allowedKeys: string[], passedState: BooleanState): BooleanState {
return allowedKeys.reduce((state, name) => {
state[name] = passedState[name] === undefined || passedState[name];
state[name] = passedState[name] === undefined || !!passedState[name];
return state;
}, {});
}
Expand Down
9 changes: 4 additions & 5 deletions src/services/app/transaction.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { VerificationInitiateContext } from '../external/verify.context.service'
import { buildScopeEmailVerificationInitiate, buildScopeGoogleAuthVerificationInitiate } from '../../verify.cases';
import { VerifyActionServiceType, VerifyActionService, Verifications, VerifyMethod } from '../external/verify.action.service';
import { EncodedTransaction } from 'web3/types';
import { toEthChecksumAddress, MasterKeySecret, decryptTextByRecoveryMasterKey, decryptTextByUserMasterKey } from '../crypto';
import { toEthChecksumAddress, MasterKeySecret, decryptTextByUserMasterKey } from '../crypto';

export interface TransactionSendData {
to: string;
Expand Down Expand Up @@ -140,15 +140,14 @@ export class TransactionApplication {
const msc = new MasterKeySecret();

const mnemonic = decryptTextByUserMasterKey(msc, user.wallets[0].mnemonic, paymantPassword, user.wallets[0].securityKey);
const salt = decryptTextByUserMasterKey(msc, user.wallets[0].salt, paymantPassword, user.wallets[0].securityKey);

if (!mnemonic.match(/^[\w ]+$/)) {
if (!mnemonic) {
throw new IncorrectMnemonic('Incorrect payment password');
}

const salt = decryptTextByUserMasterKey(msc, user.wallets[0].salt, paymantPassword, user.wallets[0].securityKey);
const account = this.web3Client.getAccountByMnemonicAndSalt(mnemonic, salt);
if (account.address !== user.wallets[0].address) {
throw new IncorrectMnemonic('Incorrect payment password');
throw new IncorrectMnemonic('Incorrect payment password, invalid address');
}

if (transData.type === ERC20_TRANSFER && !transData.contractAddress) {
Expand Down
39 changes: 27 additions & 12 deletions src/services/app/user/user.account.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { User } from '../../../entities/user';
import { VerifiedToken } from '../../../entities/verified.token';
import * as transformers from '../transformers';
import { generateMnemonic, MasterKeySecret, getSha256HexHash, getUserMasterKey, encryptText, getRecoveryMasterKey, decryptTextByRecoveryMasterKey } from '../../crypto';
import { generateMnemonic, MasterKeySecret, getUserMasterKey, encryptText, getRecoveryMasterKey, getSha256Hash } from '../../crypto';
import { Logger } from '../../../logger';
import { UserRepositoryType, UserRepositoryInterface } from '../../repositories/user.repository';
import { RegisteredTokenRepository, RegisteredTokenRepositoryType, RegisteredTokenRepositoryInterface, RegisteredTokenScope } from '../../repositories/registered.tokens.repository';
Expand Down Expand Up @@ -81,13 +81,18 @@ export class UserAccountApplication {
// should be created every time for fresh master key
const msc = new MasterKeySecret();

const recoveryKey = getRecoveryMasterKey(msc);

user.addWallet(Wallet.createWallet({
ticker: 'ETH',
address: account.address,
balance: '0',
tokens: [],
// hacky way
securityKey: JSON.stringify([getUserMasterKey(msc, paymentPassword), getRecoveryMasterKey(msc)]),
securityKey: JSON.stringify([getUserMasterKey(msc, paymentPassword), {
mac: recoveryKey.mac.toString('base64'),
pubkey: recoveryKey.pubkey.toString('base64'),
msg: recoveryKey.msg.toString('base64')
}]),
salt: encryptText(msc, salt),
mnemonic: encryptText(msc, mnemonic)
}));
Expand Down Expand Up @@ -153,6 +158,21 @@ export class UserAccountApplication {
}));
}

private getRecoveryNameForUser(user: User): string {
return getSha256Hash(new Buffer(user.email + user.wallets[0].address.toLowerCase(), 'utf-8')).toString('hex');
}

private async saveRecoveryKey(recoveryKey: string, user: User) {
// @TODO: Save in more safe space
writeFile(
join(
config.crypto.recoveryFolder,
this.getRecoveryNameForUser(user)
),
JSON.stringify(recoveryKey)
);
}

private async activateUserAndGetNewWallets(user: User): Promise<NewWallet[]> {
this.logger.debug('Save user state', user.email);

Expand All @@ -164,24 +184,19 @@ export class UserAccountApplication {

this.logger.debug('Save recovery key for', user.email);

// @TODO: Save in more secure space
writeFile(join(config.crypto.recoveryFolder, getSha256HexHash(user.email)), recoveryKey);
this.saveRecoveryKey(recoveryKey, user);

this.logger.debug('Prepare response wallets for', user.email);

const msc = new MasterKeySecret();
const mnemonic = decryptTextByRecoveryMasterKey(msc, user.wallets[0].mnemonic, recoveryKey);
const salt = decryptTextByRecoveryMasterKey(msc, user.wallets[0].salt, recoveryKey);

const account = this.web3Client.getAccountByMnemonicAndSalt(mnemonic, salt);

return [{
ticker: 'ETH',
address: account.address,
address: user.wallets[0].address,
tokens: [],
balance: '0',
mnemonic,
privateKey: account.privateKey
mnemonic: '', // now it's empty
privateKey: '' // now it's empty
}];
}

Expand Down
1 change: 1 addition & 0 deletions src/services/app/user/user.common.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class UserCommonApplication {
* @param user
*/
async getUserInfo(user: User): Promise<any> {
// @TODO: Refactor
const preferences = user.preferences || {};
if (!Object.keys(preferences['notifications'] || {}).length) {
preferences['notifications'] = getAllNotifications().reduce((p, c) => (p[c] = true, p), {});
Expand Down
140 changes: 79 additions & 61 deletions src/services/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,20 @@ export function getPrivateKeyByMnemonicAndSalt(mnemonic: string, salt: string) {
*
* @param text
*/
export function getSha256HexHash(text: string): string {
return crypto.createHash('sha256').update(text).digest('hex');
export function getSha256Hash(text: Buffer): Buffer {
return crypto.createHash('sha256').update(text).digest();
}

export function getSha512Hash(text: Buffer): Buffer {
return crypto.createHash('sha512').update(text).digest();
}

export function getHmacSha256(key: Buffer, msg: Buffer): Buffer {
return crypto.createHmac('sha256', key).update(msg).digest();
}

export function getHmacSha512(key: Buffer, msg: Buffer): Buffer {
return crypto.createHmac('sha512', key).update(msg).digest();
}

/**
Expand All @@ -63,11 +75,11 @@ export function getSha256HexHash(text: string): string {
* console.log('rk>', encMasterKey2.toString('hex'));
*
*/
function encrypt(inputBuffer: Buffer, keys: Buffer[]): Buffer {
function encryptAes256ctr(inputBuffer: Buffer, keys: Buffer[]): Buffer {
let outBuffer: Buffer;
keys.map((key) => {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-ctr', Buffer.alloc(32).fill(key), iv);
const cipher = crypto.createCipheriv('aes-256-ctr', getSha256Hash(key), iv);
outBuffer = Buffer.concat([cipher.update(inputBuffer), cipher.final()]);
inputBuffer = Buffer.concat([iv, outBuffer]);
});
Expand All @@ -79,17 +91,54 @@ function encrypt(inputBuffer: Buffer, keys: Buffer[]): Buffer {
* @param inputBuffer
* @param keys
*/
function decrypt(inputBuffer: Buffer, keys: Buffer[]): Buffer {
function decryptAes256ctr(inputBuffer: Buffer, keys: Buffer[]): Buffer {
let outBuffer: Buffer;
keys.map((key) => {
const iv = inputBuffer.slice(0, 16);
const decipher = crypto.createDecipheriv('aes-256-ctr', Buffer.alloc(32).fill(key), iv);
const decipher = crypto.createDecipheriv('aes-256-ctr', getSha256Hash(key), iv);
outBuffer = Buffer.concat([decipher.update(inputBuffer.slice(16)), decipher.final()]);
inputBuffer = Buffer.from(outBuffer);
});
return inputBuffer;
}

/**
*
* @param pubkeyTo
* @param msg
*/
export function encryptEcies(pubkeyTo: Buffer, msg: Buffer): { mac: Buffer; pubkey: Buffer; msg: Buffer; } {
const ecdh = crypto.createECDH('secp521r1');
ecdh.generateKeys();
const keyHash = getSha512Hash(ecdh.computeSecret(pubkeyTo));
const ciphertext = encryptAes256ctr(msg, [keyHash.slice(0, 32)]);

return {
mac: getHmacSha256(keyHash.slice(32), Buffer.concat([ecdh.getPublicKey(), ciphertext])),
pubkey: ecdh.getPublicKey(),
msg: ciphertext
};
}

/**
*
* @param privkey
* @param encryptedPack
*/
export function decryptEcies(privkey: Buffer, encryptedPack: { mac: Buffer; pubkey: Buffer; msg: Buffer; }): Buffer {
const ecdh = crypto.createECDH('secp521r1');
ecdh.setPrivateKey(privkey);
const keyHash = getSha512Hash(ecdh.computeSecret(encryptedPack.pubkey));
const currentHmac = getHmacSha256(keyHash.slice(32), Buffer.concat([encryptedPack.pubkey, encryptedPack.msg]));

if (!currentHmac.equals(encryptedPack.mac)) {
return null;
}

return decryptAes256ctr(encryptedPack.msg, [keyHash.slice(0, 32)]);
}


/**
*
*/
Expand All @@ -100,32 +149,27 @@ export class MasterKeySecret {
*
*/
constructor() {
this.key = this.generateRandomKey();
}

/**
*
*/
generateRandomKey(): Buffer {
return crypto.randomBytes(32);
this.key = crypto.randomBytes(32);
}

/**
*
* @param keys
*/
getEncryptedMasterKey(keys: Buffer[]): Buffer {
let outBuffer: Buffer;
return encrypt(this.key, keys);
return encryptAes256ctr(this.key, keys);
}

/**
*
* @param inputBuffer
*/
encrypt(inputBuffer: Buffer): Buffer {
let outBuffer: Buffer;
return encrypt(inputBuffer, [this.key]);
const encryptedData = encryptAes256ctr(inputBuffer, [this.key]);
return Buffer.concat([
getHmacSha256(this.key, encryptedData),
encryptedData
]);
}

/**
Expand All @@ -135,8 +179,18 @@ export class MasterKeySecret {
* @param encryptedMasterKey
*/
decrypt(inputBuffer: Buffer, keys: Buffer[], encryptedMasterKey: Buffer): Buffer {
let outBuffer: Buffer;
return decrypt(inputBuffer, [decrypt(encryptedMasterKey, keys.concat().reverse())]);
const mac = inputBuffer.slice(0, 32);

const data = inputBuffer.slice(32);
if (!data.length) {
return null;
}

const masterKey = decryptAes256ctr(encryptedMasterKey, keys.concat().reverse());
if (!getHmacSha256(masterKey, data).equals(mac)) {
return null;
}
return decryptAes256ctr(data, [masterKey]);
}
}

Expand All @@ -156,35 +210,12 @@ export function encryptText(msc: MasterKeySecret, text: string): string {
* @param userPassword
*/
export function decryptUserMasterKey(msc: MasterKeySecret, base64EncMK: string, userPassword: string) {
msc.key = decrypt(new Buffer(base64EncMK, 'base64'), [
msc.key = decryptAes256ctr(new Buffer(base64EncMK, 'base64'), [
new Buffer(userPassword, 'utf-8'),
new Buffer(config.crypto.globalKey, 'hex')
]);
}

/**
*
* @param msc
* @param hexEncMK
*/
export function recoveryUserMasterKey(msc: MasterKeySecret, hexEncMK: string) {
msc.key = decrypt(new Buffer(hexEncMK, 'hex'), [new Buffer(config.crypto.recoveryKey, 'hex')]);
}

/**
*
* @param msc
* @param newUserPassword
* @param hexEncMK
*/
export function resetUserMasterKey(msc: MasterKeySecret, newUserPassword: string, hexEncMK: string) {
recoveryUserMasterKey(msc, hexEncMK);
return msc.getEncryptedMasterKey([
new Buffer(newUserPassword, 'utf-8'),
new Buffer(config.crypto.globalKey, 'hex')
]);
}

/**
*
* @param msc
Expand All @@ -202,9 +233,7 @@ export function getUserMasterKey(msc: MasterKeySecret, userPassword: string) {
* @param msc
*/
export function getRecoveryMasterKey(msc: MasterKeySecret) {
return msc.getEncryptedMasterKey([
new Buffer(config.crypto.recoveryKey, 'hex')
]).toString('hex');
return encryptEcies(new Buffer(config.crypto.recoveryKey, 'hex'), msc.getEncryptedMasterKey([]));
}

/**
Expand All @@ -215,20 +244,9 @@ export function getRecoveryMasterKey(msc: MasterKeySecret) {
* @param base64EncMK
*/
export function decryptTextByUserMasterKey(msc: MasterKeySecret, text: string, userPassword: string, base64EncMK: string): string {
return msc.decrypt(new Buffer(text, 'base64'), [
const data = msc.decrypt(new Buffer(text, 'base64'), [
new Buffer(userPassword, 'utf-8'),
new Buffer(config.crypto.globalKey, 'hex')
], new Buffer(base64EncMK, 'base64')).toString('utf-8');
}

/**
*
* @param msc
* @param text
* @param hexEncMK
*/
export function decryptTextByRecoveryMasterKey(msc: MasterKeySecret, text: string, hexEncMK: string): string {
return msc.decrypt(new Buffer(text, 'base64'), [
new Buffer(config.crypto.recoveryKey, 'hex')
], new Buffer(hexEncMK, 'hex')).toString('utf-8');
], new Buffer(base64EncMK, 'base64'))
return data && data.toString('utf-8') || null;
}
Loading

0 comments on commit 9f5400c

Please sign in to comment.