// Copyright (C) 2018 Zilliqa // // This file is part of zilliqa-js // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . import aes from "aes-js"; import hashjs from "hash.js"; import { pbkdf2Sync } from "pbkdf2"; import scrypt from "scrypt-js"; import { v4 as uuidv4 } from "uuid"; import { Buffer } from "buffer"; /* tslint:disable:no-unused-variable */ import { bytes } from "@zilliqa-js/util"; import { randomBytes } from "./random"; import { KeystoreV3, KDF, KDFParams, PBKDF2Params, ScryptParams, } from "./types"; import { getAddressFromPrivateKey } from "./util"; const ALGO_IDENTIFIER = "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; const derivedKeyInt8Array = scrypt.syncScrypt(key, salt, n, r, p, dklen); return Buffer.from(derivedKeyInt8Array); } throw new Error("Only pbkdf2 and scrypt are supported"); } /** * encryptPrivateKey * * Encodes and encrypts an account in the format specified by * https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition. * However, note that, in keeping with the hash function used by Zilliqa's * core protocol, the MAC is generated using sha256 instead of keccak. * * NOTE: only scrypt and pbkdf2 are supported. * * @param {KDF} kdf - the key derivation function to be used * @param {string} privateKey - hex-encoded private key * @param {string} passphrase - a passphrase used for encryption * * @returns {Promise} */ export const encryptPrivateKey = async ( kdf: KDF, privateKey: string, passphrase: string ): Promise => { const address = getAddressFromPrivateKey(privateKey); const salt = randomBytes(32); const iv = Buffer.from(randomBytes(16), "hex"); const kdfparams = { salt, n: 8192, c: 262144, r: 8, p: 1, dklen: 32, }; const derivedKey = await getDerivedKey( Buffer.from(passphrase), kdf, kdfparams ); const cipher = new aes.ModeOfOperation.ctr( derivedKey.slice(0, 16), new aes.Counter(iv) ); const ciphertext = Buffer.from( cipher.encrypt(Buffer.from(privateKey, "hex")) ); return JSON.stringify({ address, crypto: { cipher: ALGO_IDENTIFIER, cipherparams: { iv: iv.toString("hex"), }, ciphertext: ciphertext.toString("hex"), kdf, kdfparams, mac: hashjs // @ts-ignore .hmac(hashjs.sha256, derivedKey, "hex") .update( Buffer.concat([ derivedKey.slice(16, 32), ciphertext, iv, Buffer.from(ALGO_IDENTIFIER), ]), "hex" ) .digest("hex"), }, id: uuidv4({ random: bytes.hexToIntArray(randomBytes(16)) }), version: 3, }); }; /** * decryptPrivateKey * * Recovers the private key from a keystore file using the given passphrase. * * @param {string} passphrase * @param {KeystoreV3} keystore * @returns {Promise} */ export const decryptPrivateKey = async ( passphrase: string, keystore: KeystoreV3 ): Promise => { const ciphertext = Buffer.from(keystore.crypto.ciphertext, "hex"); const iv = Buffer.from(keystore.crypto.cipherparams.iv, "hex"); const kdfparams = keystore.crypto.kdfparams; const derivedKey = await getDerivedKey( Buffer.from(passphrase), keystore.crypto.kdf, kdfparams ); const mac = hashjs // @ts-ignore .hmac(hashjs.sha256, derivedKey, "hex") .update( Buffer.concat([ derivedKey.slice(16, 32), ciphertext, iv, Buffer.from(ALGO_IDENTIFIER), ]), "hex" ) .digest("hex"); // we need to do a byte-by-byte comparison to avoid non-constant time side // channel attacks. if (!bytes.isEqual(mac.toUpperCase(), keystore.crypto.mac.toUpperCase())) { return Promise.reject("Failed to decrypt."); } const cipher = new aes.ModeOfOperation.ctr( derivedKey.slice(0, 16), new aes.Counter(iv) ); return Buffer.from(cipher.decrypt(ciphertext)).toString("hex"); };