From 0ad94cdf8137259bedb38c0dc949b61570bcdac0 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Thu, 27 Feb 2020 18:42:59 +0000 Subject: [PATCH] Added sync version of wallet decryption. --- packages/json-wallets/src.ts/index.ts | 16 +- packages/json-wallets/src.ts/keystore.ts | 183 +++++++++++++---------- packages/wallet/src.ts/index.ts | 6 +- 3 files changed, 120 insertions(+), 85 deletions(-) diff --git a/packages/json-wallets/src.ts/index.ts b/packages/json-wallets/src.ts/index.ts index 2d9db659..37e2ad3b 100644 --- a/packages/json-wallets/src.ts/index.ts +++ b/packages/json-wallets/src.ts/index.ts @@ -5,7 +5,7 @@ import { ExternallyOwnedAccount } from "@ethersproject/abstract-signer"; import { decrypt as decryptCrowdsale } from "./crowdsale"; import { getJsonWalletAddress, isCrowdsaleWallet, isKeystoreWallet } from "./inspect"; -import { decrypt as decryptKeystore, encrypt as encryptKeystore, EncryptOptions, ProgressCallback } from "./keystore"; +import { decrypt as decryptKeystore, decryptSync as decryptKeystoreSync, encrypt as encryptKeystore, EncryptOptions, ProgressCallback } from "./keystore"; function decryptJsonWallet(json: string, password: Bytes | string, progressCallback?: ProgressCallback): Promise { if (isCrowdsaleWallet(json)) { @@ -22,10 +22,23 @@ function decryptJsonWallet(json: string, password: Bytes | string, progressCallb return Promise.reject(new Error("invalid JSON wallet")); } +function decryptJsonWalletSync(json: string, password: Bytes | string): ExternallyOwnedAccount { + if (isCrowdsaleWallet(json)) { + return decryptCrowdsale(json, password) + } + + if (isKeystoreWallet(json)) { + return decryptKeystoreSync(json, password); + } + + throw new Error("invalid JSON wallet"); +} + export { decryptCrowdsale, decryptKeystore, + decryptKeystoreSync, encryptKeystore, isCrowdsaleWallet, @@ -33,6 +46,7 @@ export { getJsonWalletAddress, decryptJsonWallet, + decryptJsonWalletSync, ProgressCallback, EncryptOptions, diff --git a/packages/json-wallets/src.ts/keystore.ts b/packages/json-wallets/src.ts/keystore.ts index e09483fd..25c2712f 100644 --- a/packages/json-wallets/src.ts/keystore.ts +++ b/packages/json-wallets/src.ts/keystore.ts @@ -9,7 +9,7 @@ import { getAddress } from "@ethersproject/address"; import { arrayify, Bytes, BytesLike, concat, hexlify } from "@ethersproject/bytes"; import { defaultPath, entropyToMnemonic, HDNode, Mnemonic, mnemonicToEntropy } from "@ethersproject/hdnode"; import { keccak256 } from "@ethersproject/keccak256"; -import { pbkdf2 } from "@ethersproject/pbkdf2"; +import { pbkdf2 as _pbkdf2 } from "@ethersproject/pbkdf2"; import { randomBytes } from "@ethersproject/random"; import { Description } from "@ethersproject/properties"; import { computeAddress } from "@ethersproject/transactions"; @@ -61,100 +61,106 @@ export type EncryptOptions = { } } -export async function decrypt(json: string, password: Bytes | string, progressCallback?: ProgressCallback): Promise { - const data = JSON.parse(json); +function _decrypt(data: any, key: Uint8Array, ciphertext: Uint8Array): Uint8Array { + const cipher = searchPath(data, "crypto/cipher"); + if (cipher === "aes-128-ctr") { + const iv = looseArrayify(searchPath(data, "crypto/cipherparams/iv")) + const counter = new aes.Counter(iv); - const passwordBytes = getPassword(password); + const aesCtr = new aes.ModeOfOperation.ctr(key, counter); - const decrypt = function(key: Uint8Array, ciphertext: Uint8Array): Uint8Array { - const cipher = searchPath(data, "crypto/cipher"); - if (cipher === "aes-128-ctr") { - const iv = looseArrayify(searchPath(data, "crypto/cipherparams/iv")) - const counter = new aes.Counter(iv); + return arrayify(aesCtr.decrypt(ciphertext)); + } - const aesCtr = new aes.ModeOfOperation.ctr(key, counter); + return null; +} - return arrayify(aesCtr.decrypt(ciphertext)); +function _getAccount(data: any, key: Uint8Array): KeystoreAccount { + const ciphertext = looseArrayify(searchPath(data, "crypto/ciphertext")); + + const computedMAC = hexlify(keccak256(concat([ key.slice(16, 32), ciphertext ]))).substring(2); + if (computedMAC !== searchPath(data, "crypto/mac").toLowerCase()) { + throw new Error("invalid password"); + } + + const privateKey = _decrypt(data, key.slice(0, 16), ciphertext); + + if (!privateKey) { + logger.throwError("unsupported cipher", Logger.errors.UNSUPPORTED_OPERATION, { + operation: "decrypt" + }); + } + + const mnemonicKey = key.slice(32, 64); + + const address = computeAddress(privateKey); + if (data.address) { + let check = data.address.toLowerCase(); + if (check.substring(0, 2) !== "0x") { check = "0x" + check; } + + if (getAddress(check) !== address) { + throw new Error("address mismatch"); } + } - return null; + const account: _KeystoreAccount = { + _isKeystoreAccount: true, + address: address, + privateKey: hexlify(privateKey) }; - const computeMAC = function(derivedHalf: Uint8Array, ciphertext: Uint8Array) { - return keccak256(concat([ derivedHalf, ciphertext ])); - } + // Version 0.1 x-ethers metadata must contain an encrypted mnemonic phrase + if (searchPath(data, "x-ethers/version") === "0.1") { + const mnemonicCiphertext = looseArrayify(searchPath(data, "x-ethers/mnemonicCiphertext")); + const mnemonicIv = looseArrayify(searchPath(data, "x-ethers/mnemonicCounter")); - const getAccount = async function(key: Uint8Array): Promise { - const ciphertext = looseArrayify(searchPath(data, "crypto/ciphertext")); + const mnemonicCounter = new aes.Counter(mnemonicIv); + const mnemonicAesCtr = new aes.ModeOfOperation.ctr(mnemonicKey, mnemonicCounter); - const computedMAC = hexlify(computeMAC(key.slice(16, 32), ciphertext)).substring(2); - if (computedMAC !== searchPath(data, "crypto/mac").toLowerCase()) { - throw new Error("invalid password"); - } + const path = searchPath(data, "x-ethers/path") || defaultPath; + const locale = searchPath(data, "x-ethers/locale") || "en"; - const privateKey = decrypt(key.slice(0, 16), ciphertext); - const mnemonicKey = key.slice(32, 64); + const entropy = arrayify(mnemonicAesCtr.decrypt(mnemonicCiphertext)); - if (!privateKey) { - logger.throwError("unsupported cipher", Logger.errors.UNSUPPORTED_OPERATION, { - operation: "decrypt" - }); - } + try { + const mnemonic = entropyToMnemonic(entropy, locale); + const node = HDNode.fromMnemonic(mnemonic, null, locale).derivePath(path); - const address = computeAddress(privateKey); - if (data.address) { - let check = data.address.toLowerCase(); - if (check.substring(0, 2) !== "0x") { check = "0x" + check; } + if (node.privateKey != account.privateKey) { + throw new Error("mnemonic mismatch"); + } - if (getAddress(check) !== address) { - throw new Error("address mismatch"); + account.mnemonic = node.mnemonic; + + } catch (error) { + // If we don't have the locale wordlist installed to + // read this mnemonic, just bail and don't set the + // mnemonic + if (error.code !== Logger.errors.INVALID_ARGUMENT || error.argument !== "wordlist") { + throw error; } } - - const account: _KeystoreAccount = { - _isKeystoreAccount: true, - address: address, - privateKey: hexlify(privateKey) - }; - - // Version 0.1 x-ethers metadata must contain an encrypted mnemonic phrase - if (searchPath(data, "x-ethers/version") === "0.1") { - const mnemonicCiphertext = looseArrayify(searchPath(data, "x-ethers/mnemonicCiphertext")); - const mnemonicIv = looseArrayify(searchPath(data, "x-ethers/mnemonicCounter")); - - const mnemonicCounter = new aes.Counter(mnemonicIv); - const mnemonicAesCtr = new aes.ModeOfOperation.ctr(mnemonicKey, mnemonicCounter); - - const path = searchPath(data, "x-ethers/path") || defaultPath; - const locale = searchPath(data, "x-ethers/locale") || "en"; - - const entropy = arrayify(mnemonicAesCtr.decrypt(mnemonicCiphertext)); - - try { - const mnemonic = entropyToMnemonic(entropy, locale); - const node = HDNode.fromMnemonic(mnemonic, null, locale).derivePath(path); - - if (node.privateKey != account.privateKey) { - throw new Error("mnemonic mismatch"); - } - - account.mnemonic = node.mnemonic; - - } catch (error) { - // If we don't have the locale wordlist installed to - // read this mnemonic, just bail and don't set the - // mnemonic - if (error.code !== Logger.errors.INVALID_ARGUMENT || error.argument !== "wordlist") { - throw error; - } - } - } - - return new KeystoreAccount(account); } + return new KeystoreAccount(account); +} + +type ScryptFunc = (pw: Uint8Array, salt: Uint8Array, n: number, r: number, p: number, dkLen: number, callback?: ProgressCallback) => T; +type Pbkdf2Func = (pw: Uint8Array, salt: Uint8Array, c: number, dkLen: number, prfFunc: string) => T; + +function pbkdf2Sync(passwordBytes: Uint8Array, salt: Uint8Array, count: number, dkLen: number, prfFunc: string): Uint8Array { + return arrayify(_pbkdf2(passwordBytes, salt, count, dkLen, prfFunc)); +} + +function pbkdf2(passwordBytes: Uint8Array, salt: Uint8Array, count: number, dkLen: number, prfFunc: string): Promise { + return Promise.resolve(pbkdf2Sync(passwordBytes, salt, count, dkLen, prfFunc)); +} + +function _computeKdfKey(data: any, password: Bytes | string, pbkdf2Func: Pbkdf2Func, scryptFunc: ScryptFunc, progressCallback?: ProgressCallback): T { + const passwordBytes = getPassword(password); const kdf = searchPath(data, "crypto/kdf"); + if (kdf && typeof(kdf) === "string") { const throwError = function(name: string, value: any): never { return logger.throwArgumentError("invalid key-derivation function parameters", name, value); @@ -175,10 +181,7 @@ export async function decrypt(json: string, password: Bytes | string, progressCa const dkLen = parseInt(searchPath(data, "crypto/kdfparams/dklen")); if (dkLen !== 32) { throwError("dklen", dkLen); } - const key = await scrypt.scrypt(passwordBytes, salt, N, r, p, 64, progressCallback); - //key = arrayify(key); - - return getAccount(key); + return scryptFunc(passwordBytes, salt, N, r, p, 64, progressCallback); } else if (kdf.toLowerCase() === "pbkdf2") { @@ -194,20 +197,34 @@ export async function decrypt(json: string, password: Bytes | string, progressCa throwError("prf", prf); } - const c = parseInt(searchPath(data, "crypto/kdfparams/c")); + const count = parseInt(searchPath(data, "crypto/kdfparams/c")); const dkLen = parseInt(searchPath(data, "crypto/kdfparams/dklen")); if (dkLen !== 32) { throwError("dklen", dkLen); } - const key = arrayify(pbkdf2(passwordBytes, salt, c, dkLen, prfFunc)); - - return getAccount(key); + return pbkdf2Func(passwordBytes, salt, count, dkLen, prfFunc); } } return logger.throwArgumentError("unsupported key-derivation function", "kdf", kdf); } + +export function decryptSync(json: string, password: Bytes | string): KeystoreAccount { + const data = JSON.parse(json); + + const key = _computeKdfKey(data, password, pbkdf2Sync, scrypt.scryptSync); + return _getAccount(data, key); +} + +export async function decrypt(json: string, password: Bytes | string, progressCallback?: ProgressCallback): Promise { + const data = JSON.parse(json); + + const key = await _computeKdfKey(data, password, pbkdf2, scrypt.scrypt, progressCallback); + return _getAccount(data, key); +} + + export function encrypt(account: ExternallyOwnedAccount, password: Bytes | string, options?: EncryptOptions, progressCallback?: ProgressCallback): Promise { try { diff --git a/packages/wallet/src.ts/index.ts b/packages/wallet/src.ts/index.ts index 5bf42fb4..c27ca59d 100644 --- a/packages/wallet/src.ts/index.ts +++ b/packages/wallet/src.ts/index.ts @@ -10,7 +10,7 @@ import { keccak256 } from "@ethersproject/keccak256"; import { defineReadOnly, resolveProperties } from "@ethersproject/properties"; import { randomBytes } from "@ethersproject/random"; import { SigningKey } from "@ethersproject/signing-key"; -import { decryptJsonWallet, encryptKeystore, ProgressCallback } from "@ethersproject/json-wallets"; +import { decryptJsonWallet, decryptJsonWalletSync, encryptKeystore, ProgressCallback } from "@ethersproject/json-wallets"; import { computeAddress, recoverAddress, serialize, UnsignedTransaction } from "@ethersproject/transactions"; import { Wordlist } from "@ethersproject/wordlists"; @@ -159,6 +159,10 @@ export class Wallet extends Signer implements ExternallyOwnedAccount { }); } + static fromEncryptedJsonSync(json: string, password: Bytes | string): Wallet { + return new Wallet(decryptJsonWalletSync(json, password)); + } + static fromMnemonic(mnemonic: string, path?: string, wordlist?: Wordlist): Wallet { if (!path) { path = defaultPath; } return new Wallet(HDNode.fromMnemonic(mnemonic, null, wordlist).derivePath(path));