From 75895fa1491e7542c755a102f4e4c190685fd2b6 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Wed, 13 Nov 2019 21:47:08 +0900 Subject: [PATCH] Migrated to scrypt-js v3. --- packages/cli/package.json | 2 +- packages/cli/src.ts/cli.ts | 21 +- packages/cli/thirdparty.d.ts | 8 +- packages/experimental/package.json | 3 +- packages/experimental/src.ts/brain-wallet.ts | 25 +- packages/experimental/thirdparty.d.ts | 8 +- packages/json-wallets/package.json | 2 +- packages/json-wallets/src.ts/keystore.ts | 300 ++++++++----------- packages/json-wallets/thirdparty.d.ts | 8 +- 9 files changed, 151 insertions(+), 226 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 924c823ee8..06f533a15d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -15,7 +15,7 @@ "@ethersproject/basex": ">=5.0.0-beta.127", "ethers": ">=5.0.0-beta.156", "mime-types": "2.1.11", - "scrypt-js": "2.0.4", + "scrypt-js": "3.0.0", "solc": "0.5.10", "solidity-parser-antlr": "^0.3.2" }, diff --git a/packages/cli/src.ts/cli.ts b/packages/cli/src.ts/cli.ts index 5125749d70..d92248e497 100644 --- a/packages/cli/src.ts/cli.ts +++ b/packages/cli/src.ts/cli.ts @@ -4,7 +4,7 @@ import fs from "fs"; import { basename } from "path"; import { ethers } from "ethers"; -import scrypt from "scrypt-js"; +import { scrypt } from "scrypt-js"; import { getChoice, getPassword, getProgressBar } from "./prompt"; @@ -421,20 +421,11 @@ async function loadAccount(arg: string, plugin: Plugin, preventFile?: boolean): let saltBytes = ethers.utils.arrayify(ethers.utils.HDNode.fromMnemonic(mnemonic).privateKey); let progressBar = getProgressBar("Decrypting"); - return (>(new Promise((resolve, reject) => { - scrypt(passwordBytes, saltBytes, (1 << 20), 8, 1, 32, (error, progress, key) => { - if (error) { - reject(error); - } else { - progressBar(progress); - if (key) { - let derivedPassword = ethers.utils.hexlify(key).substring(2); - let node = ethers.utils.HDNode.fromMnemonic(mnemonic, derivedPassword).derivePath(ethers.utils.defaultPath); - resolve(new ethers.Wallet(node.privateKey, plugin.provider)); - } - } - }); - }))); + return scrypt(passwordBytes, saltBytes, (1 << 20), 8, 1, 32, progressBar).then((key) => { + const derivedPassword = ethers.utils.hexlify(key).substring(2); + const node = ethers.utils.HDNode.fromMnemonic(mnemonic, derivedPassword).derivePath(ethers.utils.defaultPath); + return new ethers.Wallet(node.privateKey, plugin.provider); + }); }); } else { diff --git a/packages/cli/thirdparty.d.ts b/packages/cli/thirdparty.d.ts index a49e518b27..bdd00807f7 100644 --- a/packages/cli/thirdparty.d.ts +++ b/packages/cli/thirdparty.d.ts @@ -1,7 +1,5 @@ declare module "scrypt-js" { - export class ScryptError extends Error { - progress: number; - } - export type ScryptCallback = (error: ScryptError, progress: number, key: Uint8Array) => void; - export default function(password: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number, callback: ScryptCallback): void; + export type ProgressCallback = (progress: number) => boolean | void; + export function scrypt(password: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number, callback?: ProgressCallback): Promise; + export function scryptSync(password: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number): Uint8Array; } diff --git a/packages/experimental/package.json b/packages/experimental/package.json index 5865ce7640..ed69275592 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -8,8 +8,9 @@ }, "dependencies": { "@ethersproject/web": ">=5.0.0-beta.129", + "@ensdomains/address-encoder": "^0.1.2", "ethers": ">=5.0.0-beta.156", - "scrypt-js": "2.0.4" + "scrypt-js": "3.0.0" }, "keywords": [ "Ethereum", diff --git a/packages/experimental/src.ts/brain-wallet.ts b/packages/experimental/src.ts/brain-wallet.ts index adc943f525..1494a3d44d 100644 --- a/packages/experimental/src.ts/brain-wallet.ts +++ b/packages/experimental/src.ts/brain-wallet.ts @@ -2,7 +2,7 @@ import { ethers } from "ethers"; -import scrypt from "scrypt-js"; +import { scrypt } from "scrypt-js"; import { version } from "./_version"; @@ -34,24 +34,13 @@ export class BrainWallet extends ethers.Wallet { passwordBytes = ethers.utils.arrayify(password); } - return new Promise((resolve, reject) => { - scrypt(passwordBytes, usernameBytes, (1 << 18), 8, 1, 32, (error: Error, progress: number, key: Uint8Array) => { - if (error) { - reject(error); + return scrypt(passwordBytes, usernameBytes, (1 << 18), 8, 1, 32, progressCallback).then((key: Uint8Array) => { + if (legacy) { + return new BrainWallet(key); - } else if (key) { - if (legacy) { - resolve(new BrainWallet(key)); - - } else { - let mnemonic = ethers.utils.entropyToMnemonic(ethers.utils.arrayify(key).slice(0, 16)); - resolve(new BrainWallet(ethers.Wallet.fromMnemonic(mnemonic))); - } - - } else if (progressCallback) { - return progressCallback(progress); - } - }); + } + const mnemonic = ethers.utils.entropyToMnemonic(ethers.utils.arrayify(key).slice(0, 16)); + return new BrainWallet(ethers.Wallet.fromMnemonic(mnemonic)); }); } diff --git a/packages/experimental/thirdparty.d.ts b/packages/experimental/thirdparty.d.ts index a49e518b27..bdd00807f7 100644 --- a/packages/experimental/thirdparty.d.ts +++ b/packages/experimental/thirdparty.d.ts @@ -1,7 +1,5 @@ declare module "scrypt-js" { - export class ScryptError extends Error { - progress: number; - } - export type ScryptCallback = (error: ScryptError, progress: number, key: Uint8Array) => void; - export default function(password: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number, callback: ScryptCallback): void; + export type ProgressCallback = (progress: number) => boolean | void; + export function scrypt(password: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number, callback?: ProgressCallback): Promise; + export function scryptSync(password: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number): Uint8Array; } diff --git a/packages/json-wallets/package.json b/packages/json-wallets/package.json index a971cea73f..8c4913f0b1 100644 --- a/packages/json-wallets/package.json +++ b/packages/json-wallets/package.json @@ -19,7 +19,7 @@ "@ethersproject/strings": ">=5.0.0-beta.130", "@ethersproject/transactions": ">=5.0.0-beta.128", "aes-js": "3.0.0", - "scrypt-js": "2.0.4", + "scrypt-js": "3.0.0", "uuid": "2.0.1" }, "keywords": [ diff --git a/packages/json-wallets/src.ts/keystore.ts b/packages/json-wallets/src.ts/keystore.ts index a2363e8db7..ce67c33542 100644 --- a/packages/json-wallets/src.ts/keystore.ts +++ b/packages/json-wallets/src.ts/keystore.ts @@ -1,7 +1,7 @@ "use strict"; import aes from "aes-js"; -import scrypt from "scrypt-js"; +import { scrypt } from "scrypt-js"; import uuid from "uuid"; import { ExternallyOwnedAccount } from "@ethersproject/abstract-signer"; @@ -46,7 +46,7 @@ export type EncryptOptions = { } } -export function decrypt(json: string, password: Bytes | string, progressCallback?: ProgressCallback): Promise { +export async function decrypt(json: string, password: Bytes | string, progressCallback?: ProgressCallback): Promise { const data = JSON.parse(json); const passwordBytes = getPassword(password); @@ -69,21 +69,19 @@ export function decrypt(json: string, password: Bytes | string, progressCallback return keccak256(concat([ derivedHalf, ciphertext ])); } - const getAccount = function(key: Uint8Array, reject: (error?: Error) => void) { + const getAccount = async function(key: Uint8Array): Promise { const ciphertext = looseArrayify(searchPath(data, "crypto/ciphertext")); const computedMAC = hexlify(computeMAC(key.slice(16, 32), ciphertext)).substring(2); if (computedMAC !== searchPath(data, "crypto/mac").toLowerCase()) { - reject(new Error("invalid password")); - return null; + throw new Error("invalid password"); } const privateKey = decrypt(key.slice(0, 16), ciphertext); const mnemonicKey = key.slice(32, 64); if (!privateKey) { - reject(new Error("unsupported cipher")); - return null; + throw new Error("unsupported cipher"); } const address = computeAddress(privateKey); @@ -91,12 +89,9 @@ export function decrypt(json: string, password: Bytes | string, progressCallback let check = data.address.toLowerCase(); if (check.substring(0, 2) !== "0x") { check = "0x" + check; } - try { - if (getAddress(check) !== address) { - reject(new Error("address mismatch")); - return null; - } - } catch (e) { } + if (getAddress(check) !== address) { + throw new Error("address mismatch"); + } } const account: any = { @@ -121,8 +116,7 @@ export function decrypt(json: string, password: Bytes | string, progressCallback const node = HDNode.fromMnemonic(mnemonic).derivePath(path); if (node.privateKey != account.privateKey) { - reject(new Error("mnemonic mismatch")); - return null; + throw new Error("mnemonic mismatch"); } account.mnemonic = node.mnemonic; @@ -133,89 +127,59 @@ export function decrypt(json: string, password: Bytes | string, progressCallback } - return new Promise(function(resolve, reject) { - const kdf = searchPath(data, "crypto/kdf"); - if (kdf && typeof(kdf) === "string") { - if (kdf.toLowerCase() === "scrypt") { - const salt = looseArrayify(searchPath(data, "crypto/kdfparams/salt")); - const N = parseInt(searchPath(data, "crypto/kdfparams/n")); - const r = parseInt(searchPath(data, "crypto/kdfparams/r")); - const p = parseInt(searchPath(data, "crypto/kdfparams/p")); - if (!N || !r || !p) { - reject(new Error("unsupported key-derivation function parameters")); - return; - } - - // Make sure N is a power of 2 - if ((N & (N - 1)) !== 0) { - reject(new Error("unsupported key-derivation function parameter value for N")); - return; - } - - const dkLen = parseInt(searchPath(data, "crypto/kdfparams/dklen")); - if (dkLen !== 32) { - reject( new Error("unsupported key-derivation derived-key length")); - return; - } - - if (progressCallback) { progressCallback(0); } - scrypt(passwordBytes, salt, N, r, p, 64, function(error, progress, key) { - if (error) { - error.progress = progress; - reject(error); - - } else if (key) { - key = arrayify(key); - - const account = getAccount(key, reject); - if (!account) { return; } - - if (progressCallback) { progressCallback(1); } - resolve(account); - - } else if (progressCallback) { - return progressCallback(progress); - } - }); - - } else if (kdf.toLowerCase() === "pbkdf2") { - - const salt = looseArrayify(searchPath(data, "crypto/kdfparams/salt")); - - let prfFunc: string = null; - const prf = searchPath(data, "crypto/kdfparams/prf"); - if (prf === "hmac-sha256") { - prfFunc = "sha256"; - } else if (prf === "hmac-sha512") { - prfFunc = "sha512"; - } else { - reject(new Error("unsupported prf")); - return; - } - - const c = parseInt(searchPath(data, "crypto/kdfparams/c")); - - const dkLen = parseInt(searchPath(data, "crypto/kdfparams/dklen")); - if (dkLen !== 32) { - reject( new Error("unsupported key-derivation derived-key length")); - return; - } - - const key = arrayify(pbkdf2(passwordBytes, salt, c, dkLen, prfFunc)); - - const account = getAccount(key, reject); - if (!account) { return; } - - resolve(account); + const kdf = searchPath(data, "crypto/kdf"); + if (kdf && typeof(kdf) === "string") { + if (kdf.toLowerCase() === "scrypt") { + const salt = looseArrayify(searchPath(data, "crypto/kdfparams/salt")); + const N = parseInt(searchPath(data, "crypto/kdfparams/n")); + const r = parseInt(searchPath(data, "crypto/kdfparams/r")); + const p = parseInt(searchPath(data, "crypto/kdfparams/p")); + if (!N || !r || !p) { + throw new Error("unsupported key-derivation function parameters"); + } + + // Make sure N is a power of 2 + if ((N & (N - 1)) !== 0) { + throw new Error("unsupported key-derivation function parameter value for N"); + } + + const dkLen = parseInt(searchPath(data, "crypto/kdfparams/dklen")); + if (dkLen !== 32) { + throw new Error("unsupported key-derivation derived-key length"); + } + + const key = await scrypt(passwordBytes, salt, N, r, p, 64, progressCallback); + //key = arrayify(key); + + return getAccount(key); + + } else if (kdf.toLowerCase() === "pbkdf2") { + + const salt = looseArrayify(searchPath(data, "crypto/kdfparams/salt")); + let prfFunc: string = null; + const prf = searchPath(data, "crypto/kdfparams/prf"); + if (prf === "hmac-sha256") { + prfFunc = "sha256"; + } else if (prf === "hmac-sha512") { + prfFunc = "sha512"; } else { - reject(new Error("unsupported key-derivation function")); + throw new Error("unsupported prf"); + } + + const c = parseInt(searchPath(data, "crypto/kdfparams/c")); + + const dkLen = parseInt(searchPath(data, "crypto/kdfparams/dklen")); + if (dkLen !== 32) { + throw new Error("unsupported key-derivation derived-key length"); } - } else { - reject(new Error("unsupported key-derivation function")); + const key = arrayify(pbkdf2(passwordBytes, salt, c, dkLen, prfFunc)); + + return getAccount(key); } - }); + } + throw new Error("unsupported key-derivation function"); } export function encrypt(account: ExternallyOwnedAccount, password: Bytes | string, options?: EncryptOptions, progressCallback?: ProgressCallback): Promise { @@ -293,88 +257,74 @@ export function encrypt(account: ExternallyOwnedAccount, password: Bytes | strin if (options.scrypt.p) { p = options.scrypt.p; } } - return new Promise(function(resolve, reject) { - if (progressCallback) { progressCallback(0); } - - // We take 64 bytes: - // - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix) - // - 32 bytes AES key to encrypt mnemonic with (required here to be Ethers Wallet) - scrypt(passwordBytes, salt, N, r, p, 64, function(error, progress, key) { - if (error) { - error.progress = progress; - reject(error); - - } else if (key) { - key = arrayify(key); - - // This will be used to encrypt the wallet (as per Web3 secret storage) - const derivedKey = key.slice(0, 16); - const macPrefix = key.slice(16, 32); - - // This will be used to encrypt the mnemonic phrase (if any) - const mnemonicKey = key.slice(32, 64); - - // Encrypt the private key - const counter = new aes.Counter(iv); - const aesCtr = new aes.ModeOfOperation.ctr(derivedKey, counter); - const ciphertext = arrayify(aesCtr.encrypt(privateKey)); - - // Compute the message authentication code, used to check the password - const mac = keccak256(concat([macPrefix, ciphertext])) - - // See: https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition - const data: { [key: string]: any } = { - address: account.address.substring(2).toLowerCase(), - id: uuid.v4({ random: uuidRandom }), - version: 3, - Crypto: { - cipher: "aes-128-ctr", - cipherparams: { - iv: hexlify(iv).substring(2), - }, - ciphertext: hexlify(ciphertext).substring(2), - kdf: "scrypt", - kdfparams: { - salt: hexlify(salt).substring(2), - n: N, - dklen: 32, - p: p, - r: r - }, - mac: mac.substring(2) - } - }; - - // If we have a mnemonic, encrypt it into the JSON wallet - if (entropy) { - const mnemonicIv = randomBytes(16); - const mnemonicCounter = new aes.Counter(mnemonicIv); - const mnemonicAesCtr = new aes.ModeOfOperation.ctr(mnemonicKey, mnemonicCounter); - const mnemonicCiphertext = arrayify(mnemonicAesCtr.encrypt(entropy)); - const now = new Date(); - const timestamp = (now.getUTCFullYear() + "-" + - zpad(now.getUTCMonth() + 1, 2) + "-" + - zpad(now.getUTCDate(), 2) + "T" + - zpad(now.getUTCHours(), 2) + "-" + - zpad(now.getUTCMinutes(), 2) + "-" + - zpad(now.getUTCSeconds(), 2) + ".0Z" - ); - data["x-ethers"] = { - client: client, - gethFilename: ("UTC--" + timestamp + "--" + data.address), - mnemonicCounter: hexlify(mnemonicIv).substring(2), - mnemonicCiphertext: hexlify(mnemonicCiphertext).substring(2), - path: path, - version: "0.1" - }; - } - - if (progressCallback) { progressCallback(1); } - resolve(JSON.stringify(data)); - - } else if (progressCallback) { - return progressCallback(progress); + // We take 64 bytes: + // - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix) + // - 32 bytes AES key to encrypt mnemonic with (required here to be Ethers Wallet) + return scrypt(passwordBytes, salt, N, r, p, 64, progressCallback).then((key) => { + key = arrayify(key); + + // This will be used to encrypt the wallet (as per Web3 secret storage) + const derivedKey = key.slice(0, 16); + const macPrefix = key.slice(16, 32); + + // This will be used to encrypt the mnemonic phrase (if any) + const mnemonicKey = key.slice(32, 64); + + // Encrypt the private key + const counter = new aes.Counter(iv); + const aesCtr = new aes.ModeOfOperation.ctr(derivedKey, counter); + const ciphertext = arrayify(aesCtr.encrypt(privateKey)); + + // Compute the message authentication code, used to check the password + const mac = keccak256(concat([macPrefix, ciphertext])) + + // See: https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition + const data: { [key: string]: any } = { + address: account.address.substring(2).toLowerCase(), + id: uuid.v4({ random: uuidRandom }), + version: 3, + Crypto: { + cipher: "aes-128-ctr", + cipherparams: { + iv: hexlify(iv).substring(2), + }, + ciphertext: hexlify(ciphertext).substring(2), + kdf: "scrypt", + kdfparams: { + salt: hexlify(salt).substring(2), + n: N, + dklen: 32, + p: p, + r: r + }, + mac: mac.substring(2) } - }); + }; + + // If we have a mnemonic, encrypt it into the JSON wallet + if (entropy) { + const mnemonicIv = randomBytes(16); + const mnemonicCounter = new aes.Counter(mnemonicIv); + const mnemonicAesCtr = new aes.ModeOfOperation.ctr(mnemonicKey, mnemonicCounter); + const mnemonicCiphertext = arrayify(mnemonicAesCtr.encrypt(entropy)); + const now = new Date(); + const timestamp = (now.getUTCFullYear() + "-" + + zpad(now.getUTCMonth() + 1, 2) + "-" + + zpad(now.getUTCDate(), 2) + "T" + + zpad(now.getUTCHours(), 2) + "-" + + zpad(now.getUTCMinutes(), 2) + "-" + + zpad(now.getUTCSeconds(), 2) + ".0Z" + ); + data["x-ethers"] = { + client: client, + gethFilename: ("UTC--" + timestamp + "--" + data.address), + mnemonicCounter: hexlify(mnemonicIv).substring(2), + mnemonicCiphertext: hexlify(mnemonicCiphertext).substring(2), + path: path, + version: "0.1" + }; + } + + return JSON.stringify(data); }); } diff --git a/packages/json-wallets/thirdparty.d.ts b/packages/json-wallets/thirdparty.d.ts index 80d666d486..8186557aab 100644 --- a/packages/json-wallets/thirdparty.d.ts +++ b/packages/json-wallets/thirdparty.d.ts @@ -22,11 +22,9 @@ declare module "aes-js" { } declare module "scrypt-js" { - export class ScryptError extends Error { - progress: number; - } - export type ScryptCallback = (error: ScryptError, progress: number, key: Uint8Array) => void; - export default function(password: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number, callback: ScryptCallback): void; + export type ProgressCallback = (progress: number) => boolean | void; + export function scrypt(password: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number, callback?: ProgressCallback): Promise; + export function scryptSync(password: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number): Uint8Array; } declare module "uuid" {