Skip to content

Commit

Permalink
Added sync version of wallet decryption.
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Feb 27, 2020
1 parent 2ac8c02 commit 0ad94cd
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 78 deletions.
16 changes: 15 additions & 1 deletion packages/json-wallets/src.ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExternallyOwnedAccount> {
if (isCrowdsaleWallet(json)) {
Expand All @@ -22,17 +22,31 @@ 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,
isKeystoreWallet,
getJsonWalletAddress,

decryptJsonWallet,
decryptJsonWalletSync,

ProgressCallback,
EncryptOptions,
Expand Down
169 changes: 93 additions & 76 deletions packages/json-wallets/src.ts/keystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -61,100 +61,106 @@ export type EncryptOptions = {
}
}

export async function decrypt(json: string, password: Bytes | string, progressCallback?: ProgressCallback): Promise<KeystoreAccount> {
const data = JSON.parse(json);

const passwordBytes = getPassword(password);
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 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);
const aesCtr = new aes.ModeOfOperation.ctr(key, counter);

const aesCtr = new aes.ModeOfOperation.ctr(key, counter);
return arrayify(aesCtr.decrypt(ciphertext));
}

return arrayify(aesCtr.decrypt(ciphertext));
}
return null;
}

return null;
};
function _getAccount(data: any, key: Uint8Array): KeystoreAccount {
const ciphertext = looseArrayify(searchPath(data, "crypto/ciphertext"));

const computeMAC = function(derivedHalf: Uint8Array, ciphertext: Uint8Array) {
return keccak256(concat([ derivedHalf, 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 getAccount = async function(key: Uint8Array): Promise<KeystoreAccount> {
const ciphertext = looseArrayify(searchPath(data, "crypto/ciphertext"));
const privateKey = _decrypt(data, key.slice(0, 16), ciphertext);

const computedMAC = hexlify(computeMAC(key.slice(16, 32), ciphertext)).substring(2);
if (computedMAC !== searchPath(data, "crypto/mac").toLowerCase()) {
throw new Error("invalid password");
}

const privateKey = decrypt(key.slice(0, 16), ciphertext);
const mnemonicKey = key.slice(32, 64);
if (!privateKey) {
logger.throwError("unsupported cipher", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "decrypt"
});
}

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; }
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");
}
if (getAddress(check) !== address) {
throw new Error("address mismatch");
}
}

const account: _KeystoreAccount = {
_isKeystoreAccount: true,
address: address,
privateKey: hexlify(privateKey)
};
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"));
// 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 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 path = searchPath(data, "x-ethers/path") || defaultPath;
const locale = searchPath(data, "x-ethers/locale") || "en";

const entropy = arrayify(mnemonicAesCtr.decrypt(mnemonicCiphertext));
const entropy = arrayify(mnemonicAesCtr.decrypt(mnemonicCiphertext));

try {
const mnemonic = entropyToMnemonic(entropy, locale);
const node = HDNode.fromMnemonic(mnemonic, null, locale).derivePath(path);
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");
}
if (node.privateKey != account.privateKey) {
throw new Error("mnemonic mismatch");
}

account.mnemonic = node.mnemonic;
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;
}
} 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<T> = (pw: Uint8Array, salt: Uint8Array, n: number, r: number, p: number, dkLen: number, callback?: ProgressCallback) => T;
type Pbkdf2Func<T> = (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<Uint8Array> {
return Promise.resolve(pbkdf2Sync(passwordBytes, salt, count, dkLen, prfFunc));
}

function _computeKdfKey<T>(data: any, password: Bytes | string, pbkdf2Func: Pbkdf2Func<T>, scryptFunc: ScryptFunc<T>, 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);
Expand All @@ -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") {

Expand All @@ -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<KeystoreAccount> {
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<string> {

try {
Expand Down
6 changes: 5 additions & 1 deletion packages/wallet/src.ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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));
Expand Down

0 comments on commit 0ad94cd

Please sign in to comment.