From 695da18e4418261a86e22e28199089056fdfe599 Mon Sep 17 00:00:00 2001 From: Jack Cook Date: Thu, 27 Jun 2019 19:10:15 -0700 Subject: [PATCH 1/7] converted files to ts, fixed tests, implemented ethereumjs standards for coverage, ts compiler, and tslint --- .babelrc | 7 - .gitignore | 9 +- .nycrc | 3 + .prettierignore | 5 + CHANGELOG.md | 27 ++- README.md | 61 +++--- package.json | 52 ++--- src/hdkey.js | 51 ----- src/hdkey.ts | 41 ++++ src/index.js | 311 ----------------------------- src/index.ts | 444 +++++++++++++++++++++++++++++++++++++++++ src/provider-engine.js | 26 --- src/provider-engine.ts | 22 ++ src/test/hdkey.js | 75 ------- src/test/index.js | 301 ---------------------------- src/thirdparty.js | 234 ---------------------- src/thirdparty.ts | 247 +++++++++++++++++++++++ test/hdkey.ts | 115 +++++++++++ test/index.ts | 365 +++++++++++++++++++++++++++++++++ test/mocha.opts | 4 + tsconfig.json | 4 + tsconfig.prod.json | 7 + tslint.json | 3 + 23 files changed, 1347 insertions(+), 1067 deletions(-) delete mode 100644 .babelrc create mode 100644 .nycrc create mode 100644 .prettierignore delete mode 100644 src/hdkey.js create mode 100644 src/hdkey.ts delete mode 100644 src/index.js create mode 100644 src/index.ts delete mode 100644 src/provider-engine.js create mode 100644 src/provider-engine.ts delete mode 100644 src/test/hdkey.js delete mode 100644 src/test/index.js delete mode 100644 src/thirdparty.js create mode 100644 src/thirdparty.ts create mode 100644 test/hdkey.ts create mode 100644 test/index.ts create mode 100644 test/mocha.opts create mode 100644 tsconfig.json create mode 100644 tsconfig.prod.json create mode 100644 tslint.json diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 65f6205..0000000 --- a/.babelrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "presets": [ - [ - "env" - ] - ] -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index e057797..6a61354 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ lib-cov # Coverage directory used by tools like istanbul coverage +.nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt @@ -39,4 +40,10 @@ package-lock.json # backwards compatibility reasons, JS files from root and root test/ folder # are excluded /*.js -/test + +# IDE and text editor config files +.idea +.vscode + +# build output +dist diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..b54064b --- /dev/null +++ b/.nycrc @@ -0,0 +1,3 @@ +{ + "extends": "@ethereumjs/config-nyc" +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a69149e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules +.vscode +package.json +dist +.nyc_output diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c3194..1e7db14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,30 @@ # Changelog + All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -(modification: no type change headlines) and this project adheres to +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +(modification: no type change headlines) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - ## [0.6.3] - 2018-12-19 + - Fixed installation errors for certain packaging tools, PR [#67](https://github.com/ethereumjs/ethereumjs-wallet/pull/67) -- Remove dependency on ``crypto.randomBytes`` and use ``randombytes`` package instead, PR [#63](https://github.com/ethereumjs/ethereumjs-wallet/pull/63) -- Add comprehensive test coverage for ``fromV3``, PR [#62](https://github.com/ethereumjs/ethereumjs-wallet/pull/62) -- Remove excess parameter from ``decipherBuffer`` usage, PR [#77](https://github.com/ethereumjs/ethereumjs-wallet/pull/77) -- Update dependencies, including a fixed ``scrypt.js``, which should resolve more installation issues, PR [#78](https://github.com/ethereumjs/ethereumjs-wallet/pull/78) +- Remove dependency on `crypto.randomBytes` and use `randombytes` package instead, PR [#63](https://github.com/ethereumjs/ethereumjs-wallet/pull/63) +- Add comprehensive test coverage for `fromV3`, PR [#62](https://github.com/ethereumjs/ethereumjs-wallet/pull/62) +- Remove excess parameter from `decipherBuffer` usage, PR [#77](https://github.com/ethereumjs/ethereumjs-wallet/pull/77) +- Update dependencies, including a fixed `scrypt.js`, which should resolve more installation issues, PR [#78](https://github.com/ethereumjs/ethereumjs-wallet/pull/78) [0.6.3]: https://github.com/ethereumjs/ethereumjs-wallet/compare/v0.6.2...v0.6.3 ## [0.6.2] - 2018-08-08 -- [PLEASE UPDATE!] Fixes a critical import bug introduced in ``v0.6.1`` accidentally + +- [PLEASE UPDATE!] Fixes a critical import bug introduced in `v0.6.1` accidentally changing the import path for the different submodules, see PR [#65](https://github.com/ethereumjs/ethereumjs-wallet/pull/65) [0.6.2]: https://github.com/ethereumjs/ethereumjs-wallet/compare/v0.6.1...v0.6.2 ## [0.6.1] - 2018-07-28 [DEPRECATED] + - Added support for vanity address generation, PR [#5](https://github.com/ethereumjs/ethereumjs-wallet/pull/5) - Fixed typo in provider-engine, PR [#16](https://github.com/ethereumjs/ethereumjs-wallet/pull/16) - Accept the true range of addresses for ICAP direct, PR [#6](https://github.com/ethereumjs/ethereumjs-wallet/pull/6) @@ -32,24 +35,28 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) [0.6.1]: https://github.com/ethereumjs/ethereumjs-wallet/compare/v0.6.0...v0.6.1 ## [0.6.0] - 2016-04-27 + - Added provider-engine integration, PR [#7](https://github.com/ethereumjs/ethereumjs-wallet/pull/7) [0.6.0]: https://github.com/ethereumjs/ethereumjs-wallet/compare/v0.5.2...v0.6.0 ## [0.5.2] - 2016-04-25 + - Dependency updates [0.5.2]: https://github.com/ethereumjs/ethereumjs-wallet/compare/v0.5.1...v0.5.2 ## [0.5.1] - 2016-03-26 -- Bugfix for ``EthereumHDKey.privateExtendedKey()`` + +- Bugfix for `EthereumHDKey.privateExtendedKey()` - Added travis and coveralls support - Documentation and test improvements [0.5.1]: https://github.com/ethereumjs/ethereumjs-wallet/compare/v0.5.0...v0.5.1 ## [0.5.0] - 2016-03-23 -- Support HD keys using ``cryptocoinjs/hdkey`` + +- Support HD keys using `cryptocoinjs/hdkey` - Ensure private keys are valid according to the curve - Support instantation with public keys - Support importing BIP32 xpub/xpriv diff --git a/README.md b/README.md index 2921c28..a41811d 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,13 @@ A lightweight wallet implementation. At the moment it supports key creation and conversion between various formats. It is complemented by the following packages: + - [ethereumjs-tx](https://github.com/ethereumjs/ethereumjs-tx) to sign transactions - [ethereumjs-icap](https://github.com/ethereumjs/ethereumjs-icap) to manipulate ICAP addresses - [store.js](https://github.com/marcuswestin/store.js) to use browser storage Motivations are: + - be lightweight - work in a browser - use a single, maintained version of crypto library (and that should be in line with `ethereumjs-util` and `ethereumjs-tx`) @@ -20,6 +22,7 @@ Motivations are: - support BIP32 HD keys Features not supported: + - signing transactions - managing storage (neither in node.js or the browser) @@ -27,15 +30,15 @@ Features not supported: Constructors: -* `generate([icap])` - create an instance based on a new random key (setting `icap` to true will generate an address suitable for the `ICAP Direct mode`) -* `generateVanityAddress(pattern)` - create an instance where the address is valid against the supplied pattern (**this will be very slow**) -* `fromPrivateKey(input)` - create an instance based on a raw private key -* `fromExtendedPrivateKey(input)` - create an instance based on a BIP32 extended private key (xprv) -* `fromPublicKey(input, [nonStrict])` - create an instance based on a public key (certain methods will not be available) -* `fromExtendedPublicKey(input)` - create an instance based on a BIP32 extended public key (xpub) -* `fromV1(input, password)` - import a wallet (Version 1 of the Ethereum wallet format) -* `fromV3(input, password, [nonStrict])` - import a wallet (Version 3 of the Ethereum wallet format). Set `nonStrict` true to accept files with mixed-caps. -* `fromEthSale(input, password)` - import an Ethereum Pre Sale wallet +- `generate([icap])` - create an instance based on a new random key (setting `icap` to true will generate an address suitable for the `ICAP Direct mode`) +- `generateVanityAddress(pattern)` - create an instance where the address is valid against the supplied pattern (**this will be very slow**) +- `fromPrivateKey(input)` - create an instance based on a raw private key +- `fromExtendedPrivateKey(input)` - create an instance based on a BIP32 extended private key (xprv) +- `fromPublicKey(input, [nonStrict])` - create an instance based on a public key (certain methods will not be available) +- `fromExtendedPublicKey(input)` - create an instance based on a BIP32 extended public key (xpub) +- `fromV1(input, password)` - import a wallet (Version 1 of the Ethereum wallet format) +- `fromV3(input, password, [nonStrict])` - import a wallet (Version 3 of the Ethereum wallet format). Set `nonStrict` true to accept files with mixed-caps. +- `fromEthSale(input, password)` - import an Ethereum Pre Sale wallet For the V1, V3 and EthSale formats the input is a JSON serialized string. All these formats require a password. @@ -43,12 +46,12 @@ Note: `fromPublicKey()` only accepts uncompressed Ethereum-style public keys, un Instance methods: -* `getPrivateKey()` - return the private key -* `getPublicKey()` - return the public key -* `getAddress()` - return the address -* `getChecksumAddressString()` - return the [address with checksum](https://github.com/ethereum/EIPs/issues/55) -* `getV3Filename([timestamp])` - return the suggested filename for V3 keystores -* `toV3(password, [options])` - return the wallet as a JSON string (Version 3 of the Ethereum wallet format) +- `getPrivateKey()` - return the private key +- `getPublicKey()` - return the public key +- `getAddress()` - return the address +- `getChecksumAddressString()` - return the [address with checksum](https://github.com/ethereum/EIPs/issues/55) +- `getV3Filename([timestamp])` - return the suggested filename for V3 keystores +- `toV3(password, [options])` - return the wallet as a JSON string (Version 3 of the Ethereum wallet format) All of the above instance methods return a Buffer or JSON. Use the `String` suffixed versions for a string output, such as `getPrivateKeyString()`. @@ -62,10 +65,10 @@ Importing various third party wallets is possible through the `thirdparty` submo Constructors: -* `fromEtherCamp(passphrase)` - import a brain wallet used by Ether.Camp -* `fromEtherWallet(input, password)` - import a wallet generated by EtherWallet -* `fromKryptoKit(seed)` - import a wallet from a KryptoKit seed -* `fromQuorumWallet(passphrase, userid)` - import a brain wallet used by Quorum Wallet +- `fromEtherCamp(passphrase)` - import a brain wallet used by Ether.Camp +- `fromEtherWallet(input, password)` - import a wallet generated by EtherWallet +- `fromKryptoKit(seed)` - import a wallet from a KryptoKit seed +- `fromQuorumWallet(passphrase, userid)` - import a brain wallet used by Quorum Wallet ## HD Wallet API @@ -75,18 +78,18 @@ To use BIP32 HD wallets, first include the `hdkey` submodule: Constructors: -* `fromMasterSeed(seed)` - create an instance based on a seed -* `fromExtendedKey(key)` - create an instance based on a BIP32 extended private or public key +- `fromMasterSeed(seed)` - create an instance based on a seed +- `fromExtendedKey(key)` - create an instance based on a BIP32 extended private or public key -For the seed we suggest to use [bip39](https://npmjs.org/package/bip39) to create one from a BIP39 mnemonic. +For the seed we suggest to use [bip39](https://npmjs.org/package/bip39) to create one from a BIP39 mnemonic. Instance methods: -* `privateExtendedKey()` - return a BIP32 extended private key (xprv) -* `publicExtendedKey()` - return a BIP32 extended public key (xpub) -* `derivePath(path)` - derive a node based on a path (e.g. m/44'/0'/0/1) -* `deriveChild(index)` - derive a node based on a child index -* `getWallet()` - return a `Wallet` instance as seen above +- `privateExtendedKey()` - return a BIP32 extended private key (xprv) +- `publicExtendedKey()` - return a BIP32 extended public key (xpub) +- `derivePath(path)` - derive a node based on a path (e.g. m/44'/0'/0/1) +- `deriveChild(index)` - derive a node based on a child index +- `getWallet()` - return a `Wallet` instance as seen above ## Provider Engine @@ -103,6 +106,7 @@ Note it only supports the basic wallet. With a HD Wallet, call `getWallet()` fir ### Remarks about `toV3` The `options` is an optional object hash, where all the serialization parameters can be fine tuned: + - uuid - UUID. One is randomly generated. - salt - Random salt for the `kdf`. Size must match the requirements of the KDF (key derivation function). Random number generated via `crypto.getRandomBytes` if nothing is supplied. - iv - Initialization vector for the `cipher`. Size must match the requirements of the cipher. Random number generated via `crypto.getRandomBytes` if nothing is supplied. @@ -113,15 +117,18 @@ The `options` is an optional object hash, where all the serialization parameters Depending on the `kdf` selected, the following options are available too. For `pbkdf2`: + - `c` - Number of iterations. Defaults to 262144. - `prf` - The only supported (and default) value is `hmac-sha256`. So no point changing it. For `scrypt`: + - `n` - Iteration count. Defaults to 262144. - `r` - Block size for the underlying hash. Defaults to 8. - `p` - Parallelization factor. Defaults to 1. The following settings are favoured by the Go Ethereum implementation and we default to the same: + - `kdf`: `scrypt` - `dklen`: `32` - `n`: `262144` diff --git a/package.json b/package.json index ccda3d3..0b5a1ba 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,23 @@ "name": "ethereumjs-wallet", "version": "0.6.3", "description": "Utilities for handling Ethereum keys", - "main": "index.js", + "main": "dist/index.js", + "types": "./dist/index.d.ts", "files": [ - "*.js", - "test/" + "dist" ], "scripts": { - "coverage": "istanbul cover _mocha", - "coveralls": "npm run build:dist && npm run coverage && coveralls ): V3Params { + const v3Defaults: V3Params = { + cipher: 'aes-128-ctr', + kdf: 'scrypt', + salt: randomBytes(32), + iv: randomBytes(16), + uuid: randomBytes(16), + dklen: 32, + c: 262144, + n: 262144, + r: 8, + p: 1, + } + + if (!params) { + return v3Defaults + } + return { + cipher: params.cipher || 'aes-128-ctr', + kdf: params.kdf || 'scrypt', + salt: params.salt || randomBytes(32), + iv: params.iv || randomBytes(16), + uuid: params.uuid || randomBytes(16), + dklen: params.dklen || 32, + c: params.c || 262144, + n: params.n || 262144, + r: params.r || 8, + p: params.p || 1, + } +} + +interface KDFParams { + c: number + prf: string + dklen: number + n: number + r: number + p: number + salt: string +} + +/** + * Based on the parameter list passed to the Wallet.prototype.toV3() method this + * returns a list of parameters for running the key derivation function. + * @param params params passed into the .toV3() method + */ +function mergeKDFParamsWithDefaults(params: V3Params): KDFParams { + const kdfDefaults = { + c: 262144, + prf: 'hmac-sha256', + n: 262144, + r: 8, + p: 1, + salt: params.salt.toString('hex'), + } + + return { + dklen: params.dklen, + salt: kdfDefaults.salt, + c: params.c || kdfDefaults.c, + prf: kdfDefaults.prf, + n: params.n || kdfDefaults.n, + r: params.r || kdfDefaults.r, + p: params.p || kdfDefaults.c, + } +} + +function stripUnusedKDFParamsForPBKDF2(params: KDFParams): Partial { + delete params.n + delete params.r + delete params.p + return params +} + +function stripUnusedKDFParamsForScrypt(params: KDFParams): Partial { + delete params.c + delete params.prf + return params +} + +export class Wallet { + // static methods + + public static generate(icapDirect: boolean = false): Wallet { + if (icapDirect) { + const max = new ethUtil.BN('088f924eeceeda7fe92e1f5b0fffffffffffffff', 16) + while (true) { + const privateKey = randomBytes(32) + if (new ethUtil.BN(ethUtil.privateToAddress(privateKey)).lte(max)) { + return new Wallet(privateKey) + } + } + } else { + return new Wallet(randomBytes(32)) + } + } + + public static generateVanityAddress(pattern: RegExp | string): Wallet { + if (!(pattern instanceof RegExp)) { + pattern = new RegExp(pattern) + } + + while (true) { + const privateKey = randomBytes(32) + const address = ethUtil.privateToAddress(privateKey) + + if (pattern.test(address.toString('hex'))) { + return new Wallet(privateKey) + } + } + } + + public static fromPublicKey(publicKey: Buffer, nonStrict: boolean = false): Wallet { + if (nonStrict) { + publicKey = ethUtil.importPublic(publicKey) + } + return new Wallet(undefined, publicKey) + } + + public static fromExtendedPublicKey(extendedPublicKey: string): Wallet { + if (extendedPublicKey.slice(0, 4) !== 'xpub') { + throw new Error('Not an extended public key') + } + const publicKey: Buffer = bs58check.decode(extendedPublicKey).slice(45) + // Convert to an Ethereum public key + return Wallet.fromPublicKey(publicKey, true) + } + + public static fromPrivateKey(privateKey: Buffer): Wallet { + return new Wallet(privateKey) + } + + public static fromExtendedPrivateKey(extendedPrivateKey: string): Wallet { + if (extendedPrivateKey.slice(0, 4) !== 'xprv') { + throw new Error('Not an extended private key') + } + const tmp: Buffer = bs58check.decode(extendedPrivateKey) + if (tmp[45] !== 0) { + throw new Error('Invalid extended private key') + } + return Wallet.fromPrivateKey(tmp.slice(46)) + } + + // https://github.com/ethereum/go-ethereum/wiki/Passphrase-protected-key-store-spec + public static fromV1(input: string | Object, password: string): Wallet { + const json = typeof input === 'object' ? input : JSON.parse(input) + + if (json.Version !== '1') { + throw new Error('Not a V1 Wallet') + } + if (json.Crypto.KeyHeader.Kdf !== 'scrypt') { + throw new Error('Unsupported key derivation scheme') + } + + const kdfparams = json.Crypto.KeyHeader.KdfParams + const derivedKey = scryptsy( + Buffer.from(password), + Buffer.from(json.Crypto.Salt, 'hex'), + kdfparams.N, + kdfparams.R, + kdfparams.P, + kdfparams.DkLen, + ) + + const ciphertext = Buffer.from(json.Crypto.CipherText, 'hex') + const mac = ethUtil.keccak256(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) + + if (mac.toString('hex') !== json.Crypto.MAC) { + throw new Error('Key derivation failed - possibly wrong passphrase') + } + + const decipher = crypto.createDecipheriv( + 'aes-128-cbc', + ethUtil.keccak256(derivedKey.slice(0, 16)).slice(0, 16), + Buffer.from(json.Crypto.IV, 'hex'), + ) + const seed = runCipherBuffer(decipher, ciphertext) + + return new Wallet(seed) + } + + public static fromV3( + input: string | Object, + password: string, + nonStrict: boolean = false, + ): Wallet { + const json = + typeof input === 'object' ? input : JSON.parse(nonStrict ? input.toLowerCase() : input) + + if (json.version !== 3) { + throw new Error('Not a V3 wallet') + } + + let derivedKey: Buffer, kdfparams: any + if (json.crypto.kdf === 'scrypt') { + kdfparams = json.crypto.kdfparams + + // FIXME: support progress reporting callback + derivedKey = scryptsy( + Buffer.from(password), + Buffer.from(kdfparams.salt, 'hex'), + kdfparams.n, + kdfparams.r, + kdfparams.p, + kdfparams.dklen, + ) + } else if (json.crypto.kdf === 'pbkdf2') { + kdfparams = json.crypto.kdfparams + + if (kdfparams.prf !== 'hmac-sha256') { + throw new Error('Unsupported parameters to PBKDF2') + } + + derivedKey = crypto.pbkdf2Sync( + Buffer.from(password), + Buffer.from(kdfparams.salt, 'hex'), + kdfparams.c, + kdfparams.dklen, + 'sha256', + ) + } else { + throw new Error('Unsupported key derivation scheme') + } + + const ciphertext = Buffer.from(json.crypto.ciphertext, 'hex') + const mac = ethUtil.keccak256(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) + if (mac.toString('hex') !== json.crypto.mac) { + throw new Error('Key derivation failed - possibly wrong passphrase') + } + + const decipher = crypto.createDecipheriv( + json.crypto.cipher, + derivedKey.slice(0, 16), + Buffer.from(json.crypto.cipherparams.iv, 'hex'), + ) + const seed = runCipherBuffer(decipher, ciphertext) + return new Wallet(seed) + } + + /* + * Based on https://github.com/ethereum/pyethsaletool/blob/master/pyethsaletool.py + * JSON fields: encseed, ethaddr, btcaddr, email + */ + public static fromEthSale(input: string | Object, password: string): Wallet { + const json = typeof input === 'object' ? input : JSON.parse(input) + + const encseed = Buffer.from(json.encseed, 'hex') + + // key derivation + const derivedKey = crypto.pbkdf2Sync(password, password, 2000, 32, 'sha256').slice(0, 16) + + // seed decoding (IV is first 16 bytes) + // NOTE: crypto (derived from openssl) when used with aes-*-cbc will handle PKCS#7 padding internally + // see also http://stackoverflow.com/a/31614770/4964819 + const decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, encseed.slice(0, 16)) + const seed = runCipherBuffer(decipher, encseed.slice(16)) + + const wallet = new Wallet(ethUtil.keccak256(seed)) + if (wallet.getAddress().toString('hex') !== json.ethaddr) { + throw new Error('Decoded key mismatch - possibly wrong passphrase') + } + return wallet + } + + // private getters + + private get pubKey(): Buffer { + if (!keyExists(this.publicKey)) { + this.publicKey = ethUtil.privateToPublic(this.privateKey as Buffer) + } + return this.publicKey + } + + private get privKey(): Buffer { + if (!keyExists(this.privateKey)) { + throw new Error('This is a public key only wallet') + } + return this.privateKey + } + + constructor( + private readonly privateKey?: Buffer | undefined, + private publicKey: Buffer | undefined = undefined, + ) { + if (privateKey && publicKey) { + throw new Error('Cannot supply both a private and a public key to the constructor') + } + + if (privateKey && !ethUtil.isValidPrivate(privateKey)) { + throw new Error('Private key does not satisfy the curve requirements (ie. it is invalid)') + } + + if (publicKey && !ethUtil.isValidPublic(publicKey)) { + throw new Error('Invalid public key') + } + } + + // public instance methods + + public getPrivateKey(): Buffer { + return this.privKey + } + + public getPrivateKeyString(): string { + return ethUtil.bufferToHex(this.privKey) + } + + public getPublicKey(): Buffer { + return this.pubKey + } + + public getPublicKeyString(): string { + return ethUtil.bufferToHex(this.getPublicKey()) + } + + public getAddress(): Buffer { + return ethUtil.publicToAddress(this.pubKey) + } + + public getAddressString(): string { + return ethUtil.bufferToHex(this.getAddress()) + } + + public getChecksumAddressString(): string { + return ethUtil.toChecksumAddress(this.getAddressString()) + } + + public toV3(password: string, opts?: Partial) { + if (!keyExists(this.privateKey)) { + throw new Error('This is a public key only wallet') + } + + const params = mergeToV3ParamsWithDefaults(opts); + const kdfParams = mergeKDFParamsWithDefaults(params) + + let derivedKey: Buffer, finalKDFParams: Partial + if (params.kdf === 'pbkdf2') { + derivedKey = crypto.pbkdf2Sync( + Buffer.from(password), + params.salt, + kdfParams.c, + kdfParams.dklen, + 'sha256', + ) + finalKDFParams = stripUnusedKDFParamsForPBKDF2(kdfParams) + } else if (params.kdf === 'scrypt') { + // FIXME: support progress reporting callback + derivedKey = scryptsy( + Buffer.from(password), + params.salt, + kdfParams.n, + kdfParams.r, + kdfParams.p, + kdfParams.dklen, + ) + finalKDFParams = stripUnusedKDFParamsForScrypt(kdfParams) + } else { + throw new Error('Unsupported kdf') + } + + const cipher: crypto.Cipher = crypto.createCipheriv( + params.cipher, + derivedKey.slice(0, 16), + params.iv, + ) + if (!cipher) { + throw new Error('Unsupported cipher') + } + + const ciphertext = runCipherBuffer(cipher, this.privKey) + const mac = ethUtil.keccak256( + Buffer.concat([derivedKey.slice(16, 32), Buffer.from(ciphertext)]), + ) + + return { + version: 3, + id: uuidv4({ random: params.uuid }), + address: this.getAddress().toString('hex'), + crypto: { + ciphertext: ciphertext.toString('hex'), + cipherparams: { iv: params.iv.toString('hex') }, + cipher: params.cipher, + kdf: params.kdf, + kdfparams: finalKDFParams, + mac: mac.toString('hex'), + }, + } + } + + public getV3Filename(timestamp?: number): string { + /* + * We want a timestamp like 2016-03-15T17-11-33.007598288Z. Date formatting + * is a pain in Javascript, everbody knows that. We could use moment.js, + * but decide to do it manually in order to save space. + * + * toJSON() returns a pretty close version, so let's use it. It is not UTC though, + * but does it really matter? + * + * Alternative manual way with padding and Date fields: http://stackoverflow.com/a/7244288/4964819 + * + */ + const ts = timestamp ? new Date(timestamp) : new Date() + return ['UTC--', ts.toJSON().replace(/:/g, '-'), '--', this.getAddress().toString('hex')].join( + '', + ) + } + + public toV3String(password: string, opts?: Partial): string { + return JSON.stringify(this.toV3(password, opts)) + } +} + +// helpers + +function runCipherBuffer(cipher: crypto.Cipher | crypto.Decipher, data: Buffer): Buffer { + return Buffer.concat([cipher.update(data), cipher.final()]) +} + +function keyExists(k: Buffer | undefined): k is Buffer { + return k !== undefined +} diff --git a/src/provider-engine.js b/src/provider-engine.js deleted file mode 100644 index d990279..0000000 --- a/src/provider-engine.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const inherits = require('util').inherits -const HookedWalletEthTxSubprovider = require('web3-provider-engine/subproviders/hooked-wallet-ethtx') - -module.exports = WalletSubprovider - -inherits(WalletSubprovider, HookedWalletEthTxSubprovider) - -function WalletSubprovider (wallet, opts) { - opts = opts || {} - - opts.getAccounts = function (cb) { - cb(null, [ wallet.getAddressString() ]) - } - - opts.getPrivateKey = function (address, cb) { - if (address !== wallet.getAddressString()) { - cb(new Error('Account not found')) - } else { - cb(null, wallet.getPrivateKey()) - } - } - - WalletSubprovider.super_.call(this, opts) -} diff --git a/src/provider-engine.ts b/src/provider-engine.ts new file mode 100644 index 0000000..f3e31b6 --- /dev/null +++ b/src/provider-engine.ts @@ -0,0 +1,22 @@ +import { Wallet } from './index' + +const HookedWalletEthTxSubprovider = require('web3-provider-engine/subproviders/hooked-wallet-ethtx') + +export class WalletSubprovider extends HookedWalletEthTxSubprovider { + constructor(wallet: Wallet, opts?: any) { + if (!opts) { + opts = {} + } + + opts.getAccounts = (cb: any) => cb(null, [wallet.getAddressString()]) + + opts.getPrivateKey = (address: string, cb: any) => { + if (address !== wallet.getAddressString()) { + cb(new Error('Account not found')) + } else { + cb(null, wallet.getPrivateKey()) + } + } + super(opts) + } +} diff --git a/src/test/hdkey.js b/src/test/hdkey.js deleted file mode 100644 index 0ef7edf..0000000 --- a/src/test/hdkey.js +++ /dev/null @@ -1,75 +0,0 @@ -var assert = require('assert') -var HDKey = require('../hdkey.js') -var Buffer = require('safe-buffer').Buffer - -// from BIP39 mnemonic: awake book subject inch gentle blur grant damage process float month clown -var fixtureseed = Buffer.from('747f302d9c916698912d5f70be53a6cf53bc495803a5523d3a7c3afa2afba94ec3803f838b3e1929ab5481f9da35441372283690fdcf27372c38f40ba134fe03', 'hex') -var fixturehd = HDKey.fromMasterSeed(fixtureseed) - -describe('.fromMasterSeed()', function () { - it('should work', function () { - assert.doesNotThrow(function () { - HDKey.fromMasterSeed(fixtureseed) - }) - }) -}) - -describe('.privateExtendedKey()', function () { - it('should work', function () { - assert.strictEqual(fixturehd.privateExtendedKey(), 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY') - }) -}) - -describe('.publicExtendedKey()', function () { - it('should work', function () { - assert.strictEqual(fixturehd.publicExtendedKey(), 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') - }) -}) - -describe('.fromExtendedKey()', function () { - it('should work with public', function () { - var hdnode = HDKey.fromExtendedKey('xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') - assert.strictEqual(hdnode.publicExtendedKey(), 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') - assert.throws(function () { - hdnode.privateExtendedKey() - }, /^Error: This is a public key only wallet$/) - }) - it('should work with private', function () { - var hdnode = HDKey.fromExtendedKey('xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY') - assert.strictEqual(hdnode.publicExtendedKey(), 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') - assert.strictEqual(hdnode.privateExtendedKey(), 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY') - }) -}) - -describe('.deriveChild()', function () { - it('should work', function () { - var hdnode = fixturehd.deriveChild(1) - assert.strictEqual(hdnode.privateExtendedKey(), 'xprv9vYSvrg3eR5FaKbQE4Ao2vHdyvfFL27aWMyH6X818mKWMsqqQZAN6HmRqYDGDPLArzaqbLExRsxFwtx2B2X2QKkC9uoKsiBNi22tLPKZHNS') - }) -}) - -describe('.derivePath()', function () { - it('should work with m', function () { - var hdnode = fixturehd.derivePath('m') - assert.strictEqual(hdnode.privateExtendedKey(), 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY') - }) - it('should work with m/44\'/0\'/0/1', function () { - var hdnode = fixturehd.derivePath('m/44\'/0\'/0/1') - assert.strictEqual(hdnode.privateExtendedKey(), 'xprvA1ErCzsuXhpB8iDTsbmgpkA2P8ggu97hMZbAXTZCdGYeaUrDhyR8fEw47BNEgLExsWCVzFYuGyeDZJLiFJ9kwBzGojQ6NB718tjVJrVBSrG') - }) -}) - -describe('.getWallet()', function () { - it('should work', function () { - assert.strictEqual(fixturehd.getWallet().getPrivateKeyString(), '0x26cc9417b89cd77c4acdbe2e3cd286070a015d8e380f9cd1244ae103b7d89d81') - assert.strictEqual(fixturehd.getWallet().getPublicKeyString(), - '0x0639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973defa5cb69df462bcc6d73c31e1c663c225650e80ef14a507b203f2a12aea55bc1') - }) - it('should work with public nodes', function () { - var hdnode = HDKey.fromExtendedKey('xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') - assert.throws(function () { - hdnode.getWallet().getPrivateKeyString() - }, /^Error: This is a public key only wallet$/) - assert.strictEqual(hdnode.getWallet().getPublicKeyString(), '0x0639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973defa5cb69df462bcc6d73c31e1c663c225650e80ef14a507b203f2a12aea55bc1') - }) -}) diff --git a/src/test/index.js b/src/test/index.js deleted file mode 100644 index 4ab057c..0000000 --- a/src/test/index.js +++ /dev/null @@ -1,301 +0,0 @@ -var assert = require('assert') -var Buffer = require('safe-buffer').Buffer -var Wallet = require('../') -var Thirdparty = require('../thirdparty.js') -var ethUtil = require('ethereumjs-util') - -var fixturePrivateKey = 'efca4cdd31923b50f4214af5d2ae10e7ac45a5019e9431cc195482d707485378' -var fixturePrivateKeyStr = '0x' + fixturePrivateKey -var fixturePrivateKeyBuffer = Buffer.from(fixturePrivateKey, 'hex') - -var fixturePublicKey = '5d4392f450262b276652c1fc037606abac500f3160830ce9df53aa70d95ce7cfb8b06010b2f3691c78c65c21eb4cf3dfdbfc0745d89b664ee10435bb3a0f906c' -var fixturePublicKeyStr = '0x' + fixturePublicKey -var fixturePublicKeyBuffer = Buffer.from(fixturePublicKey, 'hex') - -var fixtureWallet = Wallet.fromPrivateKey(fixturePrivateKeyBuffer) - -describe('.getPrivateKey()', function () { - it('should work', function () { - assert.strictEqual(fixtureWallet.getPrivateKey().toString('hex'), fixturePrivateKey) - }) - it('should fail', function () { - assert.throws(function () { - Wallet.fromPrivateKey(Buffer.from('001122', 'hex')) - }, /^Error: Private key does not satisfy the curve requirements \(ie. it is invalid\)$/) - }) -}) - -describe('.getPrivateKeyString()', function () { - it('should work', function () { - assert.strictEqual(fixtureWallet.getPrivateKeyString(), fixturePrivateKeyStr) - }) -}) - -describe('.getPublicKey()', function () { - it('should work', function () { - assert.strictEqual(fixtureWallet.getPublicKey().toString('hex'), fixturePublicKey) - }) -}) - -describe('.getPublicKeyString()', function () { - it('should work', function () { - assert.strictEqual(fixtureWallet.getPublicKeyString(), fixturePublicKeyStr) - }) -}) - -describe('.getAddress()', function () { - it('should work', function () { - assert.strictEqual(fixtureWallet.getAddress().toString('hex'), 'b14ab53e38da1c172f877dbc6d65e4a1b0474c3c') - }) -}) - -describe('.getAddressString()', function () { - it('should work', function () { - assert.strictEqual(fixtureWallet.getAddressString(), '0xb14ab53e38da1c172f877dbc6d65e4a1b0474c3c') - }) -}) - -describe('.getChecksumAddressString()', function () { - it('should work', function () { - assert.strictEqual(fixtureWallet.getChecksumAddressString(), '0xB14Ab53E38DA1C172f877DBC6d65e4a1B0474C3c') - }) -}) - -describe('public key only wallet', function () { - var pubKey = Buffer.from(fixturePublicKey, 'hex') - it('.fromPublicKey() should work', function () { - assert.strictEqual(Wallet.fromPublicKey(pubKey).getPublicKey().toString('hex'), - fixturePublicKey) - }) - it('.fromPublicKey() should not accept compressed keys in strict mode', function () { - assert.throws(function () { - Wallet.fromPublicKey(Buffer.from('030639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973d', 'hex')) - }, /^Error: Invalid public key$/) - }) - it('.fromPublicKey() should accept compressed keys in non-strict mode', function () { - var tmp = Buffer.from('030639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973d', 'hex') - assert.strictEqual(Wallet.fromPublicKey(tmp, true).getPublicKey().toString('hex'), - '0639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973defa5cb69df462bcc6d73c31e1c663c225650e80ef14a507b203f2a12aea55bc1') - }) - it('.getAddress() should work', function () { - assert.strictEqual(Wallet.fromPublicKey(pubKey).getAddress().toString('hex'), 'b14ab53e38da1c172f877dbc6d65e4a1b0474c3c') - }) - it('.getPrivateKey() should fail', function () { - assert.throws(function () { - Wallet.fromPublicKey(pubKey).getPrivateKey() - }, /^Error: This is a public key only wallet$/) - }) - it('.toV3() should fail', function () { - assert.throws(function () { - Wallet.fromPublicKey(pubKey).toV3() - }, /^Error: This is a public key only wallet$/) - }) -}) - -describe('.fromExtendedPrivateKey()', function () { - it('should work', function () { - var xprv = 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY' - assert.strictEqual(Wallet.fromExtendedPrivateKey(xprv).getAddressString(), '0xb800bf5435f67c7ee7d83c3a863269969a57c57c') - }) -}) - -describe('.fromExtendedPublicKey()', function () { - it('should work', function () { - var xpub = 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ' - assert.strictEqual(Wallet.fromExtendedPublicKey(xpub).getAddressString(), '0xb800bf5435f67c7ee7d83c3a863269969a57c57c') - }) -}) - -describe('.generate()', function () { - it('should generate an account', function () { - assert.strictEqual(Wallet.generate().getPrivateKey().length, 32) - }) - it('should generate an account compatible with ICAP Direct', function () { - var max = new ethUtil.BN('088f924eeceeda7fe92e1f5b0fffffffffffffff', 16) - var wallet = Wallet.generate(true) - assert.strictEqual(wallet.getPrivateKey().length, 32) - assert.strictEqual(new ethUtil.BN(wallet.getAddress()).lte(max), true) - }) -}) - -describe('.generateVanityAddress()', function () { - it('should generate an account with 000 prefix (object)', function () { - this.timeout(180000) // 3minutes - var wallet = Wallet.generateVanityAddress(/^000/) - assert.strictEqual(wallet.getPrivateKey().length, 32) - assert.strictEqual(wallet.getAddress()[0], 0) - assert.strictEqual(wallet.getAddress()[1] >>> 4, 0) - }) - it('should generate an account with 000 prefix (string)', function () { - this.timeout(180000) // 3minutes - var wallet = Wallet.generateVanityAddress('^000') - assert.strictEqual(wallet.getPrivateKey().length, 32) - assert.strictEqual(wallet.getAddress()[0], 0) - assert.strictEqual(wallet.getAddress()[1] >>> 4, 0) - }) -}) - -describe('.getV3Filename()', function () { - it('should work', function () { - assert.strictEqual(fixtureWallet.getV3Filename(1457917509265), 'UTC--2016-03-14T01-05-09.265Z--b14ab53e38da1c172f877dbc6d65e4a1b0474c3c') - }) -}) - -describe('.toV3()', function () { - var salt = Buffer.from('dc9e4a98886738bd8aae134a1f89aaa5a502c3fbd10e336136d4d5fe47448ad6', 'hex') - var iv = Buffer.from('cecacd85e9cb89788b5aab2f93361233', 'hex') - var uuid = Buffer.from('7e59dc028d42d09db29aa8a0f862cc81', 'hex') - - it('should work with PBKDF2', function () { - var w = '{"version":3,"id":"7e59dc02-8d42-409d-b29a-a8a0f862cc81","address":"b14ab53e38da1c172f877dbc6d65e4a1b0474c3c","crypto":{"ciphertext":"01ee7f1a3c8d187ea244c92eea9e332ab0bb2b4c902d89bdd71f80dc384da1be","cipherparams":{"iv":"cecacd85e9cb89788b5aab2f93361233"},"cipher":"aes-128-ctr","kdf":"pbkdf2","kdfparams":{"dklen":32,"salt":"dc9e4a98886738bd8aae134a1f89aaa5a502c3fbd10e336136d4d5fe47448ad6","c":262144,"prf":"hmac-sha256"},"mac":"0c02cd0badfebd5e783e0cf41448f84086a96365fc3456716c33641a86ebc7cc"}}' - // FIXME: just test for ciphertext and mac? - assert.strictEqual(fixtureWallet.toV3String('testtest', { kdf: 'pbkdf2', uuid: uuid, salt: salt, iv: iv }), w) - }) - it('should work with Scrypt', function () { - var w = '{"version":3,"id":"7e59dc02-8d42-409d-b29a-a8a0f862cc81","address":"b14ab53e38da1c172f877dbc6d65e4a1b0474c3c","crypto":{"ciphertext":"c52682025b1e5d5c06b816791921dbf439afe7a053abb9fac19f38a57499652c","cipherparams":{"iv":"cecacd85e9cb89788b5aab2f93361233"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"dc9e4a98886738bd8aae134a1f89aaa5a502c3fbd10e336136d4d5fe47448ad6","n":262144,"r":8,"p":1},"mac":"27b98c8676dc6619d077453b38db645a4c7c17a3e686ee5adaf53c11ac1b890e"}}' - this.timeout(180000) // 3minutes - // FIXME: just test for ciphertext and mac? - assert.strictEqual(fixtureWallet.toV3String('testtest', { kdf: 'scrypt', uuid: uuid, salt: salt, iv: iv }), w) - }) - it('should work without providing options', function () { - this.timeout(180000) // 3minutes - assert.strictEqual(fixtureWallet.toV3('testtest')['version'], 3) - }) - it('should fail for unsupported kdf', function () { - this.timeout(180000) // 3minutes - assert.throws(function () { - fixtureWallet.toV3('testtest', { kdf: 'superkey' }) - }, /^Error: Unsupported kdf$/) - }) -}) - -/* -describe('.fromV1()', function () { - it('should work', function () { - var sample = '{"Address":"d4584b5f6229b7be90727b0fc8c6b91bb427821f","Crypto":{"CipherText":"07533e172414bfa50e99dba4a0ce603f654ebfa1ff46277c3e0c577fdc87f6bb4e4fe16c5a94ce6ce14cfa069821ef9b","IV":"16d67ba0ce5a339ff2f07951253e6ba8","KeyHeader":{"Kdf":"scrypt","KdfParams":{"DkLen":32,"N":262144,"P":1,"R":8,"SaltLen":32},"Version":"1"},"MAC":"8ccded24da2e99a11d48cda146f9cc8213eb423e2ea0d8427f41c3be414424dd","Salt":"06870e5e6a24e183a5c807bd1c43afd86d573f7db303ff4853d135cd0fd3fe91"},"Id":"0498f19a-59db-4d54-ac95-33901b4f1870","Version":"1"}' - var wallet = Wallet.fromV1(sample, 'foo') - assert.strictEqual(wallet.getAddressString(), '0xd4584b5f6229b7be90727b0fc8c6b91bb427821f') - }) -}) -*/ - -describe('.fromV3()', function () { - it('should work with PBKDF2', function () { - var w = '{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"6087dab2f9fdbbfaddc31a909735c1e6"},"ciphertext":"5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46","kdf":"pbkdf2","kdfparams":{"c":262144,"dklen":32,"prf":"hmac-sha256","salt":"ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"},"mac":"517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"},"id":"3198bc9c-6672-5ab3-d995-4942343ae5b6","version":3}' - var wallet = Wallet.fromV3(w, 'testpassword') - assert.strictEqual(wallet.getAddressString(), '0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b') - }) - it('should work with Scrypt', function () { - var sample = '{"address":"2f91eb73a6cd5620d7abb50889f24eea7a6a4feb","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"a2bc4f71e8445d64ceebd1247079fbd8"},"ciphertext":"6b9ab7954c9066fa1e54e04e2c527c7d78a77611d5f84fede1bd61ab13c51e3e","kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"r":1,"p":8,"salt":"caf551e2b7ec12d93007e528093697a4c68e8a50e663b2a929754a8085d9ede4"},"mac":"506cace9c5c32544d39558025cb3bf23ed94ba2626e5338c82e50726917e1a15"},"id":"1b3cad9b-fa7b-4817-9022-d5e598eb5fe3","version":3}' - var wallet = Wallet.fromV3(sample, 'testtest') - this.timeout(180000) // 3minutes - assert.strictEqual(wallet.getAddressString(), '0x2f91eb73a6cd5620d7abb50889f24eea7a6a4feb') - }) - it('should work with \'unencrypted\' wallets', function () { - var w = '{"address":"a9886ac7489ecbcbd79268a79ef00d940e5fe1f2","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"c542cf883299b5b0a29155091054028d"},"ciphertext":"0a83c77235840cffcfcc5afe5908f2d7f89d7d54c4a796dfe2f193e90413ee9d","kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"r":1,"p":8,"salt":"699f7bf5f6985068dfaaff9db3b06aea8fe3dd3140b3addb4e60620ee97a0316"},"mac":"613fed2605240a2ff08b8d93ccc48c5b3d5023b7088189515d70df41d65f44de"},"id":"0edf817a-ee0e-4e25-8314-1f9e88a60811","version":3}' - var wallet = Wallet.fromV3(w, '') - this.timeout(180000) // 3minutes - assert.strictEqual(wallet.getAddressString(), '0xa9886ac7489ecbcbd79268a79ef00d940e5fe1f2') - }) - it('should fail with invalid password', function () { - var w = '{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"6087dab2f9fdbbfaddc31a909735c1e6"},"ciphertext":"5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46","kdf":"pbkdf2","kdfparams":{"c":262144,"dklen":32,"prf":"hmac-sha256","salt":"ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"},"mac":"517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"},"id":"3198bc9c-6672-5ab3-d995-4942343ae5b6","version":3}' - assert.throws(function () { - Wallet.fromV3(w, 'wrongtestpassword') - }, /^Error: Key derivation failed - possibly wrong passphrase$/) - }) - it('should work with (broken) mixed-case input files', function () { - var w = '{"Crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"6087dab2f9fdbbfaddc31a909735c1e6"},"ciphertext":"5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46","kdf":"pbkdf2","kdfparams":{"c":262144,"dklen":32,"prf":"hmac-sha256","salt":"ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"},"mac":"517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"},"id":"3198bc9c-6672-5ab3-d995-4942343ae5b6","version":3}' - var wallet = Wallet.fromV3(w, 'testpassword', true) - assert.strictEqual(wallet.getAddressString(), '0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b') - }) - it('shouldn\'t work with (broken) mixed-case input files in strict mode', function () { - var w = '{"Crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"6087dab2f9fdbbfaddc31a909735c1e6"},"ciphertext":"5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46","kdf":"pbkdf2","kdfparams":{"c":262144,"dklen":32,"prf":"hmac-sha256","salt":"ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"},"mac":"517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"},"id":"3198bc9c-6672-5ab3-d995-4942343ae5b6","version":3}' - assert.throws(function () { - Wallet.fromV3(w, 'testpassword') - }) // FIXME: check for assert message(s) - }) - it('should fail for wrong version', function () { - var w = '{"version":2}' - assert.throws(function () { - Wallet.fromV3(w, 'testpassword') - }, /^Error: Not a V3 wallet$/) - }) - it('should fail for wrong kdf', function () { - var w = '{"crypto":{"kdf":"superkey"},"version":3}' - assert.throws(function () { - Wallet.fromV3(w, 'testpassword') - }, /^Error: Unsupported key derivation scheme$/) - }) - it('should fail for wrong prf in pbkdf2', function () { - var w = '{"crypto":{"kdf":"pbkdf2","kdfparams":{"prf":"invalid"}},"version":3}' - assert.throws(function () { - Wallet.fromV3(w, 'testpassword') - }, /^Error: Unsupported parameters to PBKDF2$/) - }) -}) - -describe('.fromEthSale()', function () { - // Generated using https://github.com/ethereum/pyethsaletool/ [4afd19ad60cee8d09b645555180bc3a7c8a25b67] - it('should work with short password (8 characters)', function () { - var json = '{"encseed": "81ffdfaf2736310ce87df268b53169783e8420b98f3405fb9364b96ac0feebfb62f4cf31e0d25f1ded61f083514dd98c3ce1a14a24d7618fd513b6d97044725c7d2e08a7d9c2061f2c8a05af01f06755c252f04cab20fee2a4778130440a9344", "ethaddr": "22f8c5dd4a0a9d59d580667868df2da9592ab292", "email": "hello@ethereum.org", "btcaddr": "1DHW32MFwHxU2nk2SLAQq55eqFotT9jWcq"}' - var wallet = Wallet.fromEthSale(json, 'testtest') - assert.strictEqual(wallet.getAddressString(), '0x22f8c5dd4a0a9d59d580667868df2da9592ab292') - }) - it('should work with long password (19 characters)', function () { - var json = '{"encseed": "0c7e462bd67c6840ed2fa291090b2f46511b798d34492e146d6de148abbccba45d8fcfc06bea2e5b9d6c5d17b51a9a046c1054a032f24d96a56614a14dcd02e3539685d7f09b93180067160f3a9db648ccca610fc2f983fc65bf973304cbf5b6", "ethaddr": "c90b232231c83b462723f473b35cb8b1db868108", "email": "thisisalongpassword@test.com", "btcaddr": "1Cy2fN2ov5BrMkzgrzE34YadCH2yLMNkTE"}' - var wallet = Wallet.fromEthSale(json, 'thisisalongpassword') - assert.strictEqual(wallet.getAddressString(), '0xc90b232231c83b462723f473b35cb8b1db868108') - }) - // From https://github.com/ryepdx/pyethrecover/blob/master/test_wallets/ico.json - it('should work with pyethrecover\'s wallet', function () { - var json = '{"encseed": "8b4001bf61a10760d8e0876fb791e4ebeb85962f565c71697c789c23d1ade4d1285d80b2383ae5fc419ecf5319317cd94200b65df0cc50d659cbbc4365fc08e8", "ethaddr": "83b6371ba6bd9a47f82a7c4920835ef4be08f47b", "bkp": "9f566775e56486f69413c59f7ef923bc", "btcaddr": "1Nzg5v6uRCAa6Fk3CUU5qahWxEDZdZ1pBm"}' - var wallet = Wallet.fromEthSale(json, 'password123') - assert.strictEqual(wallet.getAddressString(), '0x83b6371ba6bd9a47f82a7c4920835ef4be08f47b') - }) -}) - -describe('.fromEtherWallet()', function () { - it('should work with unencrypted input', function () { - var etherWalletUnencrypted = '{"address":"0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c","encrypted":true,"locked":false,"hash":"b7a6621e8b125a17234d3e5c35522696a84134d98d07eab2479d020a8613c4bd","private":"a2c6222146ca2269086351fda9f8d2dfc8a50331e8a05f0f400c13653a521862","public":"2ed129b50b1a4dbbc53346bf711df6893265ad0c700fd11431b0bc3a66bd383a87b10ad835804a6cbe092e0375a0cc3524acf06b1ec7bb978bf25d2d6c35d120"}' - var wallet = Thirdparty.fromEtherWallet(etherWalletUnencrypted) - assert.strictEqual(wallet.getAddressString(), '0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c') - }) - it('should work with encrypted input', function () { - var etherWalletEncrypted = '{"address":"0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c","encrypted":true,"locked":true,"hash":"b7a6621e8b125a17234d3e5c35522696a84134d98d07eab2479d020a8613c4bd","private":"U2FsdGVkX1/hGPYlTZYGhzdwvtkoZfkeII4Ga4pSd/Ak373ORnwZE4nf/FFZZFcDTSH1X1+AmewadrW7dqvwr76QMYQVlihpPaFV307hWgKckkG0Mf/X4gJIQQbDPiKdcff9","public":"U2FsdGVkX1/awUDAekZQbEiXx2ct4ugXwgBllY0Hz+IwYkHiEhhxH+obu7AF7PCU2Vq5c0lpCzBUSvk2EvFyt46bw1OYIijw0iOr7fWMJEkz3bfN5mt9pYJIiPzN0gxM8u4mrmqLPUG2SkoZhWz4NOlqRUHZq7Ep6aWKz7KlEpzP9IrvDYwGubci4h+9wsspqtY1BdUJUN59EaWZSuOw1g=="}' - var wallet = Thirdparty.fromEtherWallet(etherWalletEncrypted, 'testtest') - assert.strictEqual(wallet.getAddressString(), '0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c') - }) -}) - -describe('.fromEtherCamp()', function () { - it('should work with seed text', function () { - var wallet = Thirdparty.fromEtherCamp('ethercamp123') - assert.strictEqual(wallet.getAddressString(), '0x182b6ca390224c455f11b6337d74119305014ed4') - }) -}) - -describe('.fromKryptoKit()', function () { - it('should work with basic input (d-type)', function () { - var wallet = Thirdparty.fromKryptoKit('dBWfH8QZSGbg1sAYHLBhqE5R8VGAoM7') - assert.strictEqual(wallet.getAddressString(), '0x3611981ad2d6fc1d7579d6ce4c6bc37e272c369c') - }) - it('should work with encrypted input (q-type)', function () { - var wallet = Thirdparty.fromKryptoKit('qhah1VeT0RgTvff1UKrUrxtFViiQuki16dd353d59888c25', 'testtest') - assert.strictEqual(wallet.getAddressString(), '0x3c753e27834db67329d1ec1fab67970ec1e27112') - }) -}) - -describe('.fromQuorumWallet()', function () { - it('should work', function () { - var wallet = Thirdparty.fromQuorumWallet('testtesttest', 'ethereumjs-wallet') - assert.strictEqual(wallet.getAddressString(), '0x1b86ccc22e8f137f204a41a23033541242a48815') - }) -}) - -describe('raw new Wallet() init', function () { - it('should fail when both priv and pub key provided', function () { - assert.throws(function () { - new Wallet(fixturePrivateKeyBuffer, fixturePublicKeyBuffer) // eslint-disable-line - }, /^Error: Cannot supply both a private and a public key to the constructor$/) - }) -}) diff --git a/src/thirdparty.js b/src/thirdparty.js deleted file mode 100644 index 41a9d85..0000000 --- a/src/thirdparty.js +++ /dev/null @@ -1,234 +0,0 @@ -var Wallet = require('./index.js') -var ethUtil = require('ethereumjs-util') -var crypto = require('crypto') -var scryptsy = require('scrypt.js') -var utf8 = require('utf8') -var aesjs = require('aes-js') -var Buffer = require('safe-buffer').Buffer - -function assert (val, msg) { - if (!val) { - throw new Error(msg || 'Assertion failed') - } -} - -function runCipherBuffer (cipher, data) { - return Buffer.concat([ cipher.update(data), cipher.final() ]) -} - -var Thirdparty = {} - -/* - * opts: - * - digest - digest algorithm, defaults to md5 - * - count - hash iterations - * - keysize - desired key size - * - ivsize - desired IV size - * - * Algorithm form https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html - * - * FIXME: not optimised at all - */ -function evp_kdf (data, salt, opts) { // eslint-disable-line - // A single EVP iteration, returns `D_i`, where block equlas to `D_(i-1)` - function iter (block) { - var hash = crypto.createHash(opts.digest || 'md5') - hash.update(block) - hash.update(data) - hash.update(salt) - block = hash.digest() - - for (var i = 1; i < (opts.count || 1); i++) { - hash = crypto.createHash(opts.digest || 'md5') - hash.update(block) - block = hash.digest() - } - - return block - } - - var keysize = opts.keysize || 16 - var ivsize = opts.ivsize || 16 - - var ret = [] - - var i = 0 - while (Buffer.concat(ret).length < (keysize + ivsize)) { - ret[i] = iter((i === 0) ? Buffer.alloc(0) : ret[i - 1]) - i++ - } - - var tmp = Buffer.concat(ret) - - return { - key: tmp.slice(0, keysize), - iv: tmp.slice(keysize, keysize + ivsize) - } -} - -// http://stackoverflow.com/questions/25288311/cryptojs-aes-pattern-always-ends-with -function decodeCryptojsSalt (input) { - var ciphertext = Buffer.from(input, 'base64') - if (ciphertext.slice(0, 8).toString() === 'Salted__') { - return { - salt: ciphertext.slice(8, 16), - ciphertext: ciphertext.slice(16) - } - } else { - return { - ciphertext: ciphertext - } - } -} - -/* - * This wallet format is created by https://github.com/SilentCicero/ethereumjs-accounts - * and used on https://www.myetherwallet.com/ - */ -Thirdparty.fromEtherWallet = function (input, password) { - var json = (typeof input === 'object') ? input : JSON.parse(input) - - var privKey - if (!json.locked) { - if (json.private.length !== 64) { - throw new Error('Invalid private key length') - } - - privKey = Buffer.from(json.private, 'hex') - } else { - if (typeof password !== 'string') { - throw new Error('Password required') - } - if (password.length < 7) { - throw new Error('Password must be at least 7 characters') - } - - // the "encrypted" version has the low 4 bytes - // of the hash of the address appended - var cipher = json.encrypted ? json.private.slice(0, 128) : json.private - - // decode openssl ciphertext + salt encoding - cipher = decodeCryptojsSalt(cipher) - - if (!cipher.salt) { - throw new Error('Unsupported EtherWallet key format') - } - - // derive key/iv using OpenSSL EVP as implemented in CryptoJS - var evp = evp_kdf(Buffer.from(password), cipher.salt, { keysize: 32, ivsize: 16 }) - - var decipher = crypto.createDecipheriv('aes-256-cbc', evp.key, evp.iv) - privKey = runCipherBuffer(decipher, Buffer.from(cipher.ciphertext)) - - // NOTE: yes, they've run it through UTF8 - privKey = Buffer.from(utf8.decode(privKey.toString()), 'hex') - } - - var wallet = new Wallet(privKey) - - if (wallet.getAddressString() !== json.address) { - throw new Error('Invalid private key or address') - } - - return wallet -} - -Thirdparty.fromEtherCamp = function (passphrase) { - return new Wallet(ethUtil.keccak256(Buffer.from(passphrase))) -} - -Thirdparty.fromKryptoKit = function (entropy, password) { - function kryptoKitBrokenScryptSeed (buf) { - // js-scrypt calls `Buffer.from(String(salt), 'utf8')` on the seed even though it is a buffer - // - // The `buffer`` implementation used does the below transformation (doesn't matches the current version): - // https://github.com/feross/buffer/blob/67c61181b938b17d10dbfc0a545f713b8bd59de8/index.js - - function decodeUtf8Char (str) { - try { - return decodeURIComponent(str) - } catch (err) { - return String.fromCharCode(0xFFFD) // UTF 8 invalid char - } - } - - var res = '' - var tmp = '' - - for (var i = 0; i < buf.length; i++) { - if (buf[i] <= 0x7F) { - res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]) - tmp = '' - } else { - tmp += '%' + buf[i].toString(16) - } - } - - return Buffer.from(res + decodeUtf8Char(tmp)) - } - - if (entropy[0] === '#') { - entropy = entropy.slice(1) - } - - var type = entropy[0] - entropy = entropy.slice(1) - - var privKey - if (type === 'd') { - privKey = ethUtil.sha256(entropy) - } else if (type === 'q') { - if (typeof password !== 'string') { - throw new Error('Password required') - } - - var encryptedSeed = ethUtil.sha256(Buffer.from(entropy.slice(0, 30))) - var checksum = entropy.slice(30, 46) - - var salt = kryptoKitBrokenScryptSeed(encryptedSeed) - var aesKey = scryptsy(Buffer.from(password, 'utf8'), salt, 16384, 8, 1, 32) - - /* FIXME: try to use `crypto` instead of `aesjs` - - // NOTE: ECB doesn't use the IV, so it can be anything - var decipher = crypto.createDecipheriv("aes-256-ecb", aesKey, Buffer.from(0)) - - // FIXME: this is a clear abuse, but seems to match how ECB in aesjs works - privKey = Buffer.concat([ - decipher.update(encryptedSeed).slice(0, 16), - decipher.update(encryptedSeed).slice(0, 16), - ]) - */ - - /* eslint-disable new-cap */ - var decipher = new aesjs.ModeOfOperation.ecb(aesKey) - /* eslint-enable new-cap */ - /* decrypt returns an Uint8Array, perhaps there is a better way to concatenate */ - privKey = Buffer.concat([ - Buffer.from(decipher.decrypt(encryptedSeed.slice(0, 16))), - Buffer.from(decipher.decrypt(encryptedSeed.slice(16, 32))) - ]) - - if (checksum.length > 0) { - if (checksum !== ethUtil.sha256(ethUtil.sha256(privKey)).slice(0, 8).toString('hex')) { - throw new Error('Failed to decrypt input - possibly invalid passphrase') - } - } - } else { - throw new Error('Unsupported or invalid entropy type') - } - - return new Wallet(privKey) -} - -Thirdparty.fromQuorumWallet = function (passphrase, userid) { - assert(passphrase.length >= 10) - assert(userid.length >= 10) - - var seed = passphrase + userid - seed = crypto.pbkdf2Sync(seed, seed, 2000, 32, 'sha256') - - return new Wallet(seed) -} - -module.exports = Thirdparty diff --git a/src/thirdparty.ts b/src/thirdparty.ts new file mode 100644 index 0000000..dd9977b --- /dev/null +++ b/src/thirdparty.ts @@ -0,0 +1,247 @@ +import * as crypto from 'crypto' +import * as ethUtil from 'ethereumjs-util' + +import { Wallet } from './index' + +const scryptsy = require('scrypt.js') +const utf8 = require('utf8') +const aesjs = require('aes-js') + +function runCipherBuffer(cipher: crypto.Cipher | crypto.Decipher, data: Buffer): Buffer { + return Buffer.concat([cipher.update(data), cipher.final()]) +} + +// evp_kdf + +interface EvpKdfOpts { + count: number + keysize: number + ivsize: number + digest: string +} + +const evpKdfDefaults: EvpKdfOpts = { + count: 1, + keysize: 16, + ivsize: 16, + digest: 'md5', +} + +function mergeEvpKdfOptsWithDefaults(opts?: Partial): EvpKdfOpts { + if (!opts) { + return evpKdfDefaults + } + return { + count: opts.count || evpKdfDefaults.count, + keysize: opts.keysize || evpKdfDefaults.keysize, + ivsize: opts.ivsize || evpKdfDefaults.ivsize, + digest: opts.digest || evpKdfDefaults.digest, + } +} + +/* + * opts: + * - digest - digest algorithm, defaults to md5 + * - count - hash iterations + * - keysize - desired key size + * - ivsize - desired IV size + * + * Algorithm form https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html + * + * FIXME: not optimised at all + */ +function evp_kdf(data: Buffer, salt: Buffer, opts?: Partial) { + const params = mergeEvpKdfOptsWithDefaults(opts) + + // A single EVP iteration, returns `D_i`, where block equlas to `D_(i-1)` + function iter(block: Buffer) { + let hash = crypto.createHash(params.digest) + hash.update(block) + hash.update(data) + hash.update(salt) + block = hash.digest() + + for (let i = 1, len = params.count; i < len; i++) { + hash = crypto.createHash(params.digest) + hash.update(block) + block = hash.digest() + } + return block + } + + const ret: Buffer[] = [] + let i = 0 + while (Buffer.concat(ret).length < params.keysize + params.ivsize) { + ret[i] = iter(i === 0 ? Buffer.alloc(0) : ret[i - 1]) + i++ + } + const tmp = Buffer.concat(ret) + + return { + key: tmp.slice(0, params.keysize), + iv: tmp.slice(params.keysize, params.keysize + params.ivsize), + } +} + +// http://stackoverflow.com/questions/25288311/cryptojs-aes-pattern-always-ends-with +function decodeCryptojsSalt(input: string): { ciphertext: Buffer; salt?: Buffer } { + const ciphertext = Buffer.from(input, 'base64') + if (ciphertext.slice(0, 8).toString() === 'Salted__') { + return { + salt: ciphertext.slice(8, 16), + ciphertext: ciphertext.slice(16), + } + } + return { ciphertext } +} + +/* + * This wallet format is created by https://github.com/SilentCicero/ethereumjs-accounts + * and used on https://www.myetherwallet.com/ + */ +function fromEtherWallet(input: string | Object, password: string): Wallet { + const json = typeof input === 'object' ? input : JSON.parse(input) + + let privateKey: Buffer + if (!json.locked) { + if (json.private.length !== 64) { + throw new Error('Invalid private key length') + } + privateKey = Buffer.from(json.private, 'hex') + } else { + if (password.length < 7) { + throw new Error('Password must be at least 7 characters') + } + + // the "encrypted" version has the low 4 bytes + // of the hash of the address appended + let cipher = json.encrypted ? json.private.slice(0, 128) : json.private + + // decode openssl ciphertext + salt encoding + cipher = decodeCryptojsSalt(cipher) + if (!cipher.salt) { + throw new Error('Unsupported EtherWallet key format') + } + + // derive key/iv using OpenSSL EVP as implemented in CryptoJS + const evp = evp_kdf(Buffer.from(password), cipher.salt, { keysize: 32, ivsize: 16 }) + + const decipher = crypto.createDecipheriv('aes-256-cbc', evp.key, evp.iv) + privateKey = runCipherBuffer(decipher, Buffer.from(cipher.ciphertext)) + + // NOTE: yes, they've run it through UTF8 + privateKey = Buffer.from(utf8.decode(privateKey.toString()), 'hex') + } + + const wallet = new Wallet(privateKey) + if (wallet.getAddressString() !== json.address) { + throw new Error('Invalid private key or address') + } + return wallet +} + +function fromEtherCamp(passphrase: string): Wallet { + return new Wallet(ethUtil.keccak256(Buffer.from(passphrase))) +} + +function fromKryptoKit(entropy: string, password: string): Wallet { + function kryptoKitBrokenScryptSeed(buf: Buffer) { + // js-scrypt calls `Buffer.from(String(salt), 'utf8')` on the seed even though it is a buffer + // + // The `buffer`` implementation used does the below transformation (doesn't matches the current version): + // https://github.com/feross/buffer/blob/67c61181b938b17d10dbfc0a545f713b8bd59de8/index.js + + function decodeUtf8Char(str: string) { + try { + return decodeURIComponent(str) + } catch (err) { + return String.fromCharCode(0xfffd) // UTF 8 invalid char + } + } + + let res = '', + tmp = '' + for (let i = 0; i < buf.length; i++) { + if (buf[i] <= 0x7f) { + res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]) + tmp = '' + } else { + tmp += '%' + buf[i].toString(16) + } + } + return Buffer.from(res + decodeUtf8Char(tmp)) + } + + if (entropy[0] === '#') { + entropy = entropy.slice(1) + } + + const type = entropy[0] + entropy = entropy.slice(1) + + let privateKey: Buffer + if (type === 'd') { + privateKey = ethUtil.sha256(entropy) + } else if (type === 'q') { + const encryptedSeed = ethUtil.sha256(Buffer.from(entropy.slice(0, 30))) + const checksum = entropy.slice(30, 46) + + const salt = kryptoKitBrokenScryptSeed(encryptedSeed) + const aesKey = scryptsy(Buffer.from(password, 'utf8'), salt, 16384, 8, 1, 32) + + /* FIXME: try to use `crypto` instead of `aesjs` + + // NOTE: ECB doesn't use the IV, so it can be anything + var decipher = crypto.createDecipheriv("aes-256-ecb", aesKey, Buffer.from(0)) + + // FIXME: this is a clear abuse, but seems to match how ECB in aesjs works + privKey = Buffer.concat([ + decipher.update(encryptedSeed).slice(0, 16), + decipher.update(encryptedSeed).slice(0, 16), + ]) + */ + + const decipher = new aesjs.ModeOfOperation.ecb(aesKey) + /* decrypt returns an Uint8Array, perhaps there is a better way to concatenate */ + privateKey = Buffer.concat([ + Buffer.from(decipher.decrypt(encryptedSeed.slice(0, 16))), + Buffer.from(decipher.decrypt(encryptedSeed.slice(16, 32))), + ]) + + if (checksum.length > 0) { + if ( + checksum !== + ethUtil + .sha256(ethUtil.sha256(privateKey)) + .slice(0, 8) + .toString('hex') + ) { + throw new Error('Failed to decrypt input - possibly invalid passphrase') + } + } + } else { + throw new Error('Unsupported or invalid entropy type') + } + + return new Wallet(privateKey) +} + +function fromQuorumWallet(passphrase: string, userid: string): Wallet { + if (passphrase.length < 10) { + throw new Error('Passphrase must be at least 10 characters') + } + if (userid.length < 10) { + throw new Error('User id must be at least 10 characters') + } + + const merged = passphrase + userid + const seed = crypto.pbkdf2Sync(merged, merged, 2000, 32, 'sha256') + return new Wallet(seed) +} + +export const Thirdparty = { + fromEtherWallet, + fromEtherCamp, + fromKryptoKit, + fromQuorumWallet, +} diff --git a/test/hdkey.ts b/test/hdkey.ts new file mode 100644 index 0000000..93a805c --- /dev/null +++ b/test/hdkey.ts @@ -0,0 +1,115 @@ +import * as assert from 'assert' +import { EthereumHDKey } from '../src/hdkey' + +// from BIP39 mnemonic: awake book subject inch gentle blur grant damage process float month clown +const fixtureseed = Buffer.from( + '747f302d9c916698912d5f70be53a6cf53bc495803a5523d3a7c3afa2afba94ec3803f838b3e1929ab5481f9da35441372283690fdcf27372c38f40ba134fe03', + 'hex', +) +const fixturehd = EthereumHDKey.fromMasterSeed(fixtureseed) + +describe('.fromMasterSeed()', function() { + it('should work', function() { + assert.doesNotThrow(function() { + EthereumHDKey.fromMasterSeed(fixtureseed) + }) + }) +}) + +describe('.privateExtendedKey()', function() { + it('should work', function() { + assert.strictEqual( + fixturehd.privateExtendedKey(), + 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY', + ) + }) +}) + +describe('.publicExtendedKey()', function() { + it('should work', function() { + assert.strictEqual( + fixturehd.publicExtendedKey(), + 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ', + ) + }) +}) + +describe('.fromExtendedKey()', function() { + it('should work with public', function() { + const hdnode = EthereumHDKey.fromExtendedKey( + 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ', + ) + assert.strictEqual( + hdnode.publicExtendedKey(), + 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ', + ) + assert.throws(function() { + hdnode.privateExtendedKey() + }, /^Error: This is a public key only wallet$/) + }) + it('should work with private', function() { + const hdnode = EthereumHDKey.fromExtendedKey( + 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY', + ) + assert.strictEqual( + hdnode.publicExtendedKey(), + 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ', + ) + assert.strictEqual( + hdnode.privateExtendedKey(), + 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY', + ) + }) +}) + +describe('.deriveChild()', function() { + it('should work', function() { + const hdnode = fixturehd.deriveChild(1) + assert.strictEqual( + hdnode.privateExtendedKey(), + 'xprv9vYSvrg3eR5FaKbQE4Ao2vHdyvfFL27aWMyH6X818mKWMsqqQZAN6HmRqYDGDPLArzaqbLExRsxFwtx2B2X2QKkC9uoKsiBNi22tLPKZHNS', + ) + }) +}) + +describe('.derivePath()', function() { + it('should work with m', function() { + const hdnode = fixturehd.derivePath('m') + assert.strictEqual( + hdnode.privateExtendedKey(), + 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY', + ) + }) + it("should work with m/44'/0'/0/1", function() { + const hdnode = fixturehd.derivePath("m/44'/0'/0/1") + assert.strictEqual( + hdnode.privateExtendedKey(), + 'xprvA1ErCzsuXhpB8iDTsbmgpkA2P8ggu97hMZbAXTZCdGYeaUrDhyR8fEw47BNEgLExsWCVzFYuGyeDZJLiFJ9kwBzGojQ6NB718tjVJrVBSrG', + ) + }) +}) + +describe('.getWallet()', function() { + it('should work', function() { + assert.strictEqual( + fixturehd.getWallet().getPrivateKeyString(), + '0x26cc9417b89cd77c4acdbe2e3cd286070a015d8e380f9cd1244ae103b7d89d81', + ) + assert.strictEqual( + fixturehd.getWallet().getPublicKeyString(), + '0x0639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973defa5cb69df462bcc6d73c31e1c663c225650e80ef14a507b203f2a12aea55bc1', + ) + }) + it('should work with public nodes', function() { + const hdnode = EthereumHDKey.fromExtendedKey( + 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ', + ) + assert.throws(function() { + hdnode.getWallet().getPrivateKeyString() + }, /^Error: This is a public key only wallet$/) + assert.strictEqual( + hdnode.getWallet().getPublicKeyString(), + '0x0639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973defa5cb69df462bcc6d73c31e1c663c225650e80ef14a507b203f2a12aea55bc1', + ) + }) +}) diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..fae020c --- /dev/null +++ b/test/index.ts @@ -0,0 +1,365 @@ +/* tslint:disable no-invalid-this */ +import * as assert from 'assert' +import * as ethUtil from 'ethereumjs-util' + +import { Wallet } from '../src' +import { Thirdparty } from '../src/thirdparty' + +const fixturePrivateKey = 'efca4cdd31923b50f4214af5d2ae10e7ac45a5019e9431cc195482d707485378' +const fixturePrivateKeyStr = '0x' + fixturePrivateKey +const fixturePrivateKeyBuffer = Buffer.from(fixturePrivateKey, 'hex') + +const fixturePublicKey = + '5d4392f450262b276652c1fc037606abac500f3160830ce9df53aa70d95ce7cfb8b06010b2f3691c78c65c21eb4cf3dfdbfc0745d89b664ee10435bb3a0f906c' +const fixturePublicKeyStr = '0x' + fixturePublicKey +const fixturePublicKeyBuffer = Buffer.from(fixturePublicKey, 'hex') + +const fixtureWallet = Wallet.fromPrivateKey(fixturePrivateKeyBuffer) + +describe('.getPrivateKey()', function() { + it('should work', function() { + assert.strictEqual(fixtureWallet.getPrivateKey().toString('hex'), fixturePrivateKey) + }) + it('should fail', function() { + assert.throws(function() { + Wallet.fromPrivateKey(Buffer.from('001122', 'hex')) + }, /^Error: Private key does not satisfy the curve requirements \(ie. it is invalid\)$/) + }) +}) + +describe('.getPrivateKeyString()', function() { + it('should work', function() { + assert.strictEqual(fixtureWallet.getPrivateKeyString(), fixturePrivateKeyStr) + }) +}) + +describe('.getPublicKey()', function() { + it('should work', function() { + assert.strictEqual(fixtureWallet.getPublicKey().toString('hex'), fixturePublicKey) + }) +}) + +describe('.getPublicKeyString()', function() { + it('should work', function() { + assert.strictEqual(fixtureWallet.getPublicKeyString(), fixturePublicKeyStr) + }) +}) + +describe('.getAddress()', function() { + it('should work', function() { + assert.strictEqual( + fixtureWallet.getAddress().toString('hex'), + 'b14ab53e38da1c172f877dbc6d65e4a1b0474c3c', + ) + }) +}) + +describe('.getAddressString()', function() { + it('should work', function() { + assert.strictEqual( + fixtureWallet.getAddressString(), + '0xb14ab53e38da1c172f877dbc6d65e4a1b0474c3c', + ) + }) +}) + +describe('.getChecksumAddressString()', function() { + it('should work', function() { + assert.strictEqual( + fixtureWallet.getChecksumAddressString(), + '0xB14Ab53E38DA1C172f877DBC6d65e4a1B0474C3c', + ) + }) +}) + +describe('public key only wallet', function() { + const pubKey = Buffer.from(fixturePublicKey, 'hex') + it('.fromPublicKey() should work', function() { + assert.strictEqual( + Wallet.fromPublicKey(pubKey) + .getPublicKey() + .toString('hex'), + fixturePublicKey, + ) + }) + it('.fromPublicKey() should not accept compressed keys in strict mode', function() { + assert.throws(function() { + Wallet.fromPublicKey( + Buffer.from('030639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973d', 'hex'), + ) + }, /^Error: Invalid public key$/) + }) + it('.fromPublicKey() should accept compressed keys in non-strict mode', function() { + const tmp = Buffer.from( + '030639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973d', + 'hex', + ) + assert.strictEqual( + Wallet.fromPublicKey(tmp, true) + .getPublicKey() + .toString('hex'), + '0639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973defa5cb69df462bcc6d73c31e1c663c225650e80ef14a507b203f2a12aea55bc1', + ) + }) + it('.getAddress() should work', function() { + assert.strictEqual( + Wallet.fromPublicKey(pubKey) + .getAddress() + .toString('hex'), + 'b14ab53e38da1c172f877dbc6d65e4a1b0474c3c', + ) + }) + it('.getPrivateKey() should fail', function() { + assert.throws(function() { + Wallet.fromPublicKey(pubKey).getPrivateKey() + }, /^Error: This is a public key only wallet$/) + }) + // it('.toV3() should fail', function () { + // assert.throws(function () { + // Wallet.fromPublicKey(pubKey).toV3() + // }, /^Error: This is a public key only wallet$/) + // }) +}) + +describe('.fromExtendedPrivateKey()', function() { + it('should work', function() { + const xprv = + 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY' + assert.strictEqual( + Wallet.fromExtendedPrivateKey(xprv).getAddressString(), + '0xb800bf5435f67c7ee7d83c3a863269969a57c57c', + ) + }) +}) + +describe('.fromExtendedPublicKey()', function() { + it('should work', function() { + const xpub = + 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ' + assert.strictEqual( + Wallet.fromExtendedPublicKey(xpub).getAddressString(), + '0xb800bf5435f67c7ee7d83c3a863269969a57c57c', + ) + }) +}) + +describe('.generate()', function() { + it('should generate an account', function() { + assert.strictEqual(Wallet.generate().getPrivateKey().length, 32) + }) + it('should generate an account compatible with ICAP Direct', function() { + const max = new ethUtil.BN('088f924eeceeda7fe92e1f5b0fffffffffffffff', 16) + const wallet = Wallet.generate(true) + assert.strictEqual(wallet.getPrivateKey().length, 32) + assert.strictEqual(new ethUtil.BN(wallet.getAddress()).lte(max), true) + }) +}) + +describe('.generateVanityAddress()', function() { + it('should generate an account with 000 prefix (object)', function() { + this.timeout(180000) // 3minutes + const wallet = Wallet.generateVanityAddress(/^000/) + assert.strictEqual(wallet.getPrivateKey().length, 32) + assert.strictEqual(wallet.getAddress()[0], 0) + assert.strictEqual(wallet.getAddress()[1] >>> 4, 0) + }) + it('should generate an account with 000 prefix (string)', function() { + this.timeout(180000) // 3minutes + const wallet = Wallet.generateVanityAddress('^000') + assert.strictEqual(wallet.getPrivateKey().length, 32) + assert.strictEqual(wallet.getAddress()[0], 0) + assert.strictEqual(wallet.getAddress()[1] >>> 4, 0) + }) +}) + +describe('.getV3Filename()', function() { + it('should work', function() { + assert.strictEqual( + fixtureWallet.getV3Filename(1457917509265), + 'UTC--2016-03-14T01-05-09.265Z--b14ab53e38da1c172f877dbc6d65e4a1b0474c3c', + ) + }) +}) + +describe('.toV3()', function() { + const salt = Buffer.from( + 'dc9e4a98886738bd8aae134a1f89aaa5a502c3fbd10e336136d4d5fe47448ad6', + 'hex', + ) + const iv = Buffer.from('cecacd85e9cb89788b5aab2f93361233', 'hex') + const uuid = Buffer.from('7e59dc028d42d09db29aa8a0f862cc81', 'hex') + + it('should work with PBKDF2', function() { + const w = + '{"version":3,"id":"7e59dc02-8d42-409d-b29a-a8a0f862cc81","address":"b14ab53e38da1c172f877dbc6d65e4a1b0474c3c","crypto":{"ciphertext":"01ee7f1a3c8d187ea244c92eea9e332ab0bb2b4c902d89bdd71f80dc384da1be","cipherparams":{"iv":"cecacd85e9cb89788b5aab2f93361233"},"cipher":"aes-128-ctr","kdf":"pbkdf2","kdfparams":{"dklen":32,"salt":"dc9e4a98886738bd8aae134a1f89aaa5a502c3fbd10e336136d4d5fe47448ad6","c":262144,"prf":"hmac-sha256"},"mac":"0c02cd0badfebd5e783e0cf41448f84086a96365fc3456716c33641a86ebc7cc"}}' + // FIXME: just test for ciphertext and mac? + assert.strictEqual( + fixtureWallet.toV3String('testtest', { kdf: 'pbkdf2', uuid: uuid, salt: salt, iv: iv }), + w, + ) + }) + it('should work with Scrypt', function() { + const w = + '{"version":3,"id":"7e59dc02-8d42-409d-b29a-a8a0f862cc81","address":"b14ab53e38da1c172f877dbc6d65e4a1b0474c3c","crypto":{"ciphertext":"c52682025b1e5d5c06b816791921dbf439afe7a053abb9fac19f38a57499652c","cipherparams":{"iv":"cecacd85e9cb89788b5aab2f93361233"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"dc9e4a98886738bd8aae134a1f89aaa5a502c3fbd10e336136d4d5fe47448ad6","n":262144,"r":8,"p":1},"mac":"27b98c8676dc6619d077453b38db645a4c7c17a3e686ee5adaf53c11ac1b890e"}}' + this.timeout(180000) // 3minutes + // FIXME: just test for ciphertext and mac? + assert.strictEqual( + fixtureWallet.toV3String('testtest', { kdf: 'scrypt', uuid: uuid, salt: salt, iv: iv }), + w, + ) + }) + it('should work without providing options', function() { + this.timeout(180000) // 3minutes + assert.strictEqual(fixtureWallet.toV3('testtest')['version'], 3) + }) + it('should fail for unsupported kdf', function() { + this.timeout(180000) // 3minutes + assert.throws(function() { + fixtureWallet.toV3('testtest', { kdf: 'superkey' }) + }, /^Error: Unsupported kdf$/) + }) +}) + +/* +describe('.fromV1()', function () { + it('should work', function () { + const sample = '{"Address":"d4584b5f6229b7be90727b0fc8c6b91bb427821f","Crypto":{"CipherText":"07533e172414bfa50e99dba4a0ce603f654ebfa1ff46277c3e0c577fdc87f6bb4e4fe16c5a94ce6ce14cfa069821ef9b","IV":"16d67ba0ce5a339ff2f07951253e6ba8","KeyHeader":{"Kdf":"scrypt","KdfParams":{"DkLen":32,"N":262144,"P":1,"R":8,"SaltLen":32},"Version":"1"},"MAC":"8ccded24da2e99a11d48cda146f9cc8213eb423e2ea0d8427f41c3be414424dd","Salt":"06870e5e6a24e183a5c807bd1c43afd86d573f7db303ff4853d135cd0fd3fe91"},"Id":"0498f19a-59db-4d54-ac95-33901b4f1870","Version":"1"}' + const wallet = Wallet.fromV1(sample, 'foo') + assert.strictEqual(wallet.getAddressString(), '0xd4584b5f6229b7be90727b0fc8c6b91bb427821f') + }) +}) +*/ + +describe('.fromV3()', function() { + it('should work with PBKDF2', function() { + const w = + '{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"6087dab2f9fdbbfaddc31a909735c1e6"},"ciphertext":"5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46","kdf":"pbkdf2","kdfparams":{"c":262144,"dklen":32,"prf":"hmac-sha256","salt":"ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"},"mac":"517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"},"id":"3198bc9c-6672-5ab3-d995-4942343ae5b6","version":3}' + const wallet = Wallet.fromV3(w, 'testpassword') + assert.strictEqual(wallet.getAddressString(), '0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b') + }) + it('should work with Scrypt', function() { + const sample = + '{"address":"2f91eb73a6cd5620d7abb50889f24eea7a6a4feb","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"a2bc4f71e8445d64ceebd1247079fbd8"},"ciphertext":"6b9ab7954c9066fa1e54e04e2c527c7d78a77611d5f84fede1bd61ab13c51e3e","kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"r":1,"p":8,"salt":"caf551e2b7ec12d93007e528093697a4c68e8a50e663b2a929754a8085d9ede4"},"mac":"506cace9c5c32544d39558025cb3bf23ed94ba2626e5338c82e50726917e1a15"},"id":"1b3cad9b-fa7b-4817-9022-d5e598eb5fe3","version":3}' + const wallet = Wallet.fromV3(sample, 'testtest') + this.timeout(180000) // 3minutes + assert.strictEqual(wallet.getAddressString(), '0x2f91eb73a6cd5620d7abb50889f24eea7a6a4feb') + }) + it("should work with 'unencrypted' wallets", function() { + const w = + '{"address":"a9886ac7489ecbcbd79268a79ef00d940e5fe1f2","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"c542cf883299b5b0a29155091054028d"},"ciphertext":"0a83c77235840cffcfcc5afe5908f2d7f89d7d54c4a796dfe2f193e90413ee9d","kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"r":1,"p":8,"salt":"699f7bf5f6985068dfaaff9db3b06aea8fe3dd3140b3addb4e60620ee97a0316"},"mac":"613fed2605240a2ff08b8d93ccc48c5b3d5023b7088189515d70df41d65f44de"},"id":"0edf817a-ee0e-4e25-8314-1f9e88a60811","version":3}' + const wallet = Wallet.fromV3(w, '') + this.timeout(180000) // 3minutes + assert.strictEqual(wallet.getAddressString(), '0xa9886ac7489ecbcbd79268a79ef00d940e5fe1f2') + }) + it('should fail with invalid password', function() { + const w = + '{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"6087dab2f9fdbbfaddc31a909735c1e6"},"ciphertext":"5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46","kdf":"pbkdf2","kdfparams":{"c":262144,"dklen":32,"prf":"hmac-sha256","salt":"ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"},"mac":"517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"},"id":"3198bc9c-6672-5ab3-d995-4942343ae5b6","version":3}' + assert.throws(function() { + Wallet.fromV3(w, 'wrongtestpassword') + }, /^Error: Key derivation failed - possibly wrong passphrase$/) + }) + it('should work with (broken) mixed-case input files', function() { + const w = + '{"Crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"6087dab2f9fdbbfaddc31a909735c1e6"},"ciphertext":"5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46","kdf":"pbkdf2","kdfparams":{"c":262144,"dklen":32,"prf":"hmac-sha256","salt":"ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"},"mac":"517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"},"id":"3198bc9c-6672-5ab3-d995-4942343ae5b6","version":3}' + const wallet = Wallet.fromV3(w, 'testpassword', true) + assert.strictEqual(wallet.getAddressString(), '0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b') + }) + it("shouldn't work with (broken) mixed-case input files in strict mode", function() { + const w = + '{"Crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"6087dab2f9fdbbfaddc31a909735c1e6"},"ciphertext":"5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46","kdf":"pbkdf2","kdfparams":{"c":262144,"dklen":32,"prf":"hmac-sha256","salt":"ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"},"mac":"517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"},"id":"3198bc9c-6672-5ab3-d995-4942343ae5b6","version":3}' + assert.throws(function() { + Wallet.fromV3(w, 'testpassword') + }) // FIXME: check for assert message(s) + }) + it('should fail for wrong version', function() { + const w = '{"version":2}' + assert.throws(function() { + Wallet.fromV3(w, 'testpassword') + }, /^Error: Not a V3 wallet$/) + }) + it('should fail for wrong kdf', function() { + const w = '{"crypto":{"kdf":"superkey"},"version":3}' + assert.throws(function() { + Wallet.fromV3(w, 'testpassword') + }, /^Error: Unsupported key derivation scheme$/) + }) + it('should fail for wrong prf in pbkdf2', function() { + const w = '{"crypto":{"kdf":"pbkdf2","kdfparams":{"prf":"invalid"}},"version":3}' + assert.throws(function() { + Wallet.fromV3(w, 'testpassword') + }, /^Error: Unsupported parameters to PBKDF2$/) + }) +}) + +describe('.fromEthSale()', function() { + // Generated using https://github.com/ethereum/pyethsaletool/ [4afd19ad60cee8d09b645555180bc3a7c8a25b67] + it('should work with short password (8 characters)', function() { + const json = + '{"encseed": "81ffdfaf2736310ce87df268b53169783e8420b98f3405fb9364b96ac0feebfb62f4cf31e0d25f1ded61f083514dd98c3ce1a14a24d7618fd513b6d97044725c7d2e08a7d9c2061f2c8a05af01f06755c252f04cab20fee2a4778130440a9344", "ethaddr": "22f8c5dd4a0a9d59d580667868df2da9592ab292", "email": "hello@ethereum.org", "btcaddr": "1DHW32MFwHxU2nk2SLAQq55eqFotT9jWcq"}' + const wallet = Wallet.fromEthSale(json, 'testtest') + assert.strictEqual(wallet.getAddressString(), '0x22f8c5dd4a0a9d59d580667868df2da9592ab292') + }) + it('should work with long password (19 characters)', function() { + const json = + '{"encseed": "0c7e462bd67c6840ed2fa291090b2f46511b798d34492e146d6de148abbccba45d8fcfc06bea2e5b9d6c5d17b51a9a046c1054a032f24d96a56614a14dcd02e3539685d7f09b93180067160f3a9db648ccca610fc2f983fc65bf973304cbf5b6", "ethaddr": "c90b232231c83b462723f473b35cb8b1db868108", "email": "thisisalongpassword@test.com", "btcaddr": "1Cy2fN2ov5BrMkzgrzE34YadCH2yLMNkTE"}' + const wallet = Wallet.fromEthSale(json, 'thisisalongpassword') + assert.strictEqual(wallet.getAddressString(), '0xc90b232231c83b462723f473b35cb8b1db868108') + }) + // From https://github.com/ryepdx/pyethrecover/blob/master/test_wallets/ico.json + it("should work with pyethrecover's wallet", function() { + const json = + '{"encseed": "8b4001bf61a10760d8e0876fb791e4ebeb85962f565c71697c789c23d1ade4d1285d80b2383ae5fc419ecf5319317cd94200b65df0cc50d659cbbc4365fc08e8", "ethaddr": "83b6371ba6bd9a47f82a7c4920835ef4be08f47b", "bkp": "9f566775e56486f69413c59f7ef923bc", "btcaddr": "1Nzg5v6uRCAa6Fk3CUU5qahWxEDZdZ1pBm"}' + const wallet = Wallet.fromEthSale(json, 'password123') + assert.strictEqual(wallet.getAddressString(), '0x83b6371ba6bd9a47f82a7c4920835ef4be08f47b') + }) +}) + +describe('.fromEtherWallet()', function() { + // it('should work with unencrypted input', function () { + // const etherWalletUnencrypted = '{"address":"0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c","encrypted":true,"locked":false,"hash":"b7a6621e8b125a17234d3e5c35522696a84134d98d07eab2479d020a8613c4bd","private":"a2c6222146ca2269086351fda9f8d2dfc8a50331e8a05f0f400c13653a521862","public":"2ed129b50b1a4dbbc53346bf711df6893265ad0c700fd11431b0bc3a66bd383a87b10ad835804a6cbe092e0375a0cc3524acf06b1ec7bb978bf25d2d6c35d120"}' + // const wallet = Thirdparty.fromEtherWallet(etherWalletUnencrypted) + // assert.strictEqual(wallet.getAddressString(), '0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c') + // }) + it('should work with encrypted input', function() { + const etherWalletEncrypted = + '{"address":"0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c","encrypted":true,"locked":true,"hash":"b7a6621e8b125a17234d3e5c35522696a84134d98d07eab2479d020a8613c4bd","private":"U2FsdGVkX1/hGPYlTZYGhzdwvtkoZfkeII4Ga4pSd/Ak373ORnwZE4nf/FFZZFcDTSH1X1+AmewadrW7dqvwr76QMYQVlihpPaFV307hWgKckkG0Mf/X4gJIQQbDPiKdcff9","public":"U2FsdGVkX1/awUDAekZQbEiXx2ct4ugXwgBllY0Hz+IwYkHiEhhxH+obu7AF7PCU2Vq5c0lpCzBUSvk2EvFyt46bw1OYIijw0iOr7fWMJEkz3bfN5mt9pYJIiPzN0gxM8u4mrmqLPUG2SkoZhWz4NOlqRUHZq7Ep6aWKz7KlEpzP9IrvDYwGubci4h+9wsspqtY1BdUJUN59EaWZSuOw1g=="}' + const wallet = Thirdparty.fromEtherWallet(etherWalletEncrypted, 'testtest') + assert.strictEqual(wallet.getAddressString(), '0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c') + }) +}) + +describe('.fromEtherCamp()', function() { + it('should work with seed text', function() { + const wallet = Thirdparty.fromEtherCamp('ethercamp123') + assert.strictEqual(wallet.getAddressString(), '0x182b6ca390224c455f11b6337d74119305014ed4') + }) +}) + +describe('.fromKryptoKit()', function() { + // it('should work with basic input (d-type)', function () { + // const wallet = Thirdparty.fromKryptoKit('dBWfH8QZSGbg1sAYHLBhqE5R8VGAoM7') + // assert.strictEqual(wallet.getAddressString(), '0x3611981ad2d6fc1d7579d6ce4c6bc37e272c369c') + // }) + it('should work with encrypted input (q-type)', function() { + const wallet = Thirdparty.fromKryptoKit( + 'qhah1VeT0RgTvff1UKrUrxtFViiQuki16dd353d59888c25', + 'testtest', + ) + assert.strictEqual(wallet.getAddressString(), '0x3c753e27834db67329d1ec1fab67970ec1e27112') + }) +}) + +describe('.fromQuorumWallet()', function() { + it('should work', function() { + const wallet = Thirdparty.fromQuorumWallet('testtesttest', 'ethereumjs-wallet') + assert.strictEqual(wallet.getAddressString(), '0x1b86ccc22e8f137f204a41a23033541242a48815') + }) +}) + +describe('raw new Wallet() init', function() { + it('should fail when both priv and pub key provided', function() { + assert.throws(function() { + new Wallet(fixturePrivateKeyBuffer, fixturePublicKeyBuffer) // eslint-disable-line + }, /^Error: Cannot supply both a private and a public key to the constructor$/) + }) +}) diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..e015271 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,4 @@ +--require ./node_modules/ts-node/register +--require ./node_modules/source-map-support/register +--recursive +--exit diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ddbd8a9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@ethereumjs/config-tsc", + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/tsconfig.prod.json b/tsconfig.prod.json new file mode 100644 index 0000000..184d95b --- /dev/null +++ b/tsconfig.prod.json @@ -0,0 +1,7 @@ +{ + "extends": "@ethereumjs/config-tsc", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..2ba21c4 --- /dev/null +++ b/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "@ethereumjs/config-tslint" +} From 96f5f5af4c6ca6792a0b1ec3e8933f67f359ff28 Mon Sep 17 00:00:00 2001 From: Jack Cook Date: Thu, 27 Jun 2019 19:19:03 -0700 Subject: [PATCH 2/7] adding lint commands per ethereumjs config guidelines --- package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0b5a1ba..8359186 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "coveralls": "ethereumjs-config-coveralls", "format": "ethereumjs-config-format", "format-fix": "ethereumjs-config-format-fix", + "lint": "ethereumjs-config-lint", + "lint-fix": "ethereumjs-config-lint-fix", "pretest": "npm run build", "test": "nyc mocha ./test/**/*.ts", "tsc": "ethereumjs-config-tsc", @@ -62,11 +64,11 @@ "husky": "^2.1.0", "mocha": "^5.2.0", "nyc": "^14.1.1", - "prettier": "^1.18.2", + "prettier": "^1.15.3", "source-map-support": "^0.5.12", "ts-node": "^8.3.0", - "tslint": "^5.18.0", - "typescript": "^3.5.2", + "tslint": "^5.12.0", + "typescript": "^3.2.2", "typestrict": "^1.0.2" } } From 384860ea6b870d12ab1faf947ea5325c0b4e21b8 Mon Sep 17 00:00:00 2001 From: Jack Cook Date: Thu, 27 Jun 2019 19:26:49 -0700 Subject: [PATCH 3/7] linting fixes --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b1ff798..a17e3d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -353,7 +353,7 @@ export class Wallet { throw new Error('This is a public key only wallet') } - const params = mergeToV3ParamsWithDefaults(opts); + const params = mergeToV3ParamsWithDefaults(opts) const kdfParams = mergeKDFParamsWithDefaults(params) let derivedKey: Buffer, finalKDFParams: Partial From 733e49b7e98c3496cec302e9b0b3d1e00f7b0895 Mon Sep 17 00:00:00 2001 From: Jack Cook Date: Fri, 28 Jun 2019 15:16:13 -0700 Subject: [PATCH 4/7] use module style exports to maintain the way JS imports the compiled code --- .gitignore | 5 ----- prettier.config.js | 1 + src/hdkey.ts | 6 ++++-- src/index.ts | 7 ++++++- src/provider-engine.ts | 6 ++++-- src/thirdparty.ts | 6 ++++-- test/hdkey.ts | 2 +- test/index.ts | 4 ++-- 8 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 prettier.config.js diff --git a/.gitignore b/.gitignore index 6a61354..612fb85 100644 --- a/.gitignore +++ b/.gitignore @@ -35,11 +35,6 @@ package-lock.json # Optional REPL history .node_repl_history -# Build folder -# Update 2018-08-07: currently build is done to / (before: dist/) due to -# backwards compatibility reasons, JS files from root and root test/ folder -# are excluded -/*.js # IDE and text editor config files .idea diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..0f2e5b7 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1 @@ +module.exports = require('@ethereumjs/config-prettier') diff --git a/src/hdkey.ts b/src/hdkey.ts index 8b12243..b4d51bd 100644 --- a/src/hdkey.ts +++ b/src/hdkey.ts @@ -1,8 +1,8 @@ -import { Wallet } from './index' +import Wallet = require('./index') const HDKey = require('hdkey') -export class EthereumHDKey { +class EthereumHDKey { public static fromMasterSeed(seedBuffer: Buffer): EthereumHDKey { return new EthereumHDKey(HDKey.fromMasterSeed(seedBuffer)) } @@ -39,3 +39,5 @@ export class EthereumHDKey { return Wallet.fromPublicKey(this._hdkey._publicKey, true) } } + +export = EthereumHDKey diff --git a/src/index.ts b/src/index.ts index a17e3d6..0f50b3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,7 +101,7 @@ function stripUnusedKDFParamsForScrypt(params: KDFParams): Partial { return params } -export class Wallet { +class Wallet { // static methods public static generate(icapDirect: boolean = false): Wallet { @@ -309,6 +309,9 @@ export class Wallet { throw new Error('Cannot supply both a private and a public key to the constructor') } + if (!privateKey && !publicKey) { + } + if (privateKey && !ethUtil.isValidPrivate(privateKey)) { throw new Error('Private key does not satisfy the curve requirements (ie. it is invalid)') } @@ -442,3 +445,5 @@ function runCipherBuffer(cipher: crypto.Cipher | crypto.Decipher, data: Buffer): function keyExists(k: Buffer | undefined): k is Buffer { return k !== undefined } + +export = Wallet diff --git a/src/provider-engine.ts b/src/provider-engine.ts index f3e31b6..9f44684 100644 --- a/src/provider-engine.ts +++ b/src/provider-engine.ts @@ -1,8 +1,8 @@ -import { Wallet } from './index' +import Wallet = require('./index') const HookedWalletEthTxSubprovider = require('web3-provider-engine/subproviders/hooked-wallet-ethtx') -export class WalletSubprovider extends HookedWalletEthTxSubprovider { +class WalletSubprovider extends HookedWalletEthTxSubprovider { constructor(wallet: Wallet, opts?: any) { if (!opts) { opts = {} @@ -20,3 +20,5 @@ export class WalletSubprovider extends HookedWalletEthTxSubprovider { super(opts) } } + +export = WalletSubprovider diff --git a/src/thirdparty.ts b/src/thirdparty.ts index dd9977b..091ffb1 100644 --- a/src/thirdparty.ts +++ b/src/thirdparty.ts @@ -1,7 +1,7 @@ import * as crypto from 'crypto' import * as ethUtil from 'ethereumjs-util' -import { Wallet } from './index' +import Wallet = require('./index') const scryptsy = require('scrypt.js') const utf8 = require('utf8') @@ -239,9 +239,11 @@ function fromQuorumWallet(passphrase: string, userid: string): Wallet { return new Wallet(seed) } -export const Thirdparty = { +const Thirdparty = { fromEtherWallet, fromEtherCamp, fromKryptoKit, fromQuorumWallet, } + +export = Thirdparty diff --git a/test/hdkey.ts b/test/hdkey.ts index 93a805c..fdebfab 100644 --- a/test/hdkey.ts +++ b/test/hdkey.ts @@ -1,5 +1,5 @@ import * as assert from 'assert' -import { EthereumHDKey } from '../src/hdkey' +import EthereumHDKey = require('../src/hdkey') // from BIP39 mnemonic: awake book subject inch gentle blur grant damage process float month clown const fixtureseed = Buffer.from( diff --git a/test/index.ts b/test/index.ts index fae020c..a825430 100644 --- a/test/index.ts +++ b/test/index.ts @@ -2,8 +2,8 @@ import * as assert from 'assert' import * as ethUtil from 'ethereumjs-util' -import { Wallet } from '../src' -import { Thirdparty } from '../src/thirdparty' +import Wallet = require('../src') +import Thirdparty = require('../src/thirdparty') const fixturePrivateKey = 'efca4cdd31923b50f4214af5d2ae10e7ac45a5019e9431cc195482d707485378' const fixturePrivateKeyStr = '0x' + fixturePrivateKey From bca01ea69ff5bd27a40e63812009434557442d79 Mon Sep 17 00:00:00 2001 From: Jack Cook Date: Fri, 28 Jun 2019 15:18:46 -0700 Subject: [PATCH 5/7] deduped travis setting --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 58d74a8..e9ecd50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ node_js: - "6" - "8" - "10" -env: - - CXX=g++-4.8 addons: apt: sources: From 288442924eb2ee2ae17bb8a757fb9901de5cc1d7 Mon Sep 17 00:00:00 2001 From: Jack Cook Date: Tue, 2 Jul 2019 11:56:56 -0700 Subject: [PATCH 6/7] PR review fixes --- src/hdkey.ts | 6 +- src/index.ts | 166 +++++++++++++++++++++++++++-------------- src/provider-engine.ts | 6 +- src/thirdparty.ts | 38 ++++++++-- test/hdkey.ts | 2 +- test/index.ts | 4 +- 6 files changed, 151 insertions(+), 71 deletions(-) diff --git a/src/hdkey.ts b/src/hdkey.ts index b4d51bd..2eafb27 100644 --- a/src/hdkey.ts +++ b/src/hdkey.ts @@ -1,8 +1,8 @@ -import Wallet = require('./index') +import Wallet from './index' const HDKey = require('hdkey') -class EthereumHDKey { +export default class EthereumHDKey { public static fromMasterSeed(seedBuffer: Buffer): EthereumHDKey { return new EthereumHDKey(HDKey.fromMasterSeed(seedBuffer)) } @@ -39,5 +39,3 @@ class EthereumHDKey { return Wallet.fromPublicKey(this._hdkey._publicKey, true) } } - -export = EthereumHDKey diff --git a/src/index.ts b/src/index.ts index 0f50b3b..d3cd8e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,22 +52,48 @@ function mergeToV3ParamsWithDefaults(params?: Partial): V3Params { } } -interface KDFParams { - c: number - prf: string +// KDF params + +interface ScryptKDFParams { dklen: number n: number - r: number p: number + r: number + salt: string +} + +interface PBKDFParams { + c: number + dklen: number + prf: string salt: string } +// union of both the PBKDF2 and Scrypt KDF parameters representing all possible +// parameters the user could supply +type AllKDFParams = ScryptKDFParams & PBKDFParams + +type KDFParams = ScryptKDFParams | PBKDFParams + +function kdfParamsForPBKDF(params: AllKDFParams): PBKDFParams { + delete params.n + delete params.p + delete params.r + return params +} + +function kdfParamsForScrypt(params: AllKDFParams): ScryptKDFParams { + delete params.c + delete params.prf + return params +} + /** * Based on the parameter list passed to the Wallet.prototype.toV3() method this * returns a list of parameters for running the key derivation function. * @param params params passed into the .toV3() method */ -function mergeKDFParamsWithDefaults(params: V3Params): KDFParams { +function mergeKDFParamsWithDefaults(params: V3Params): AllKDFParams { const kdfDefaults = { c: 262144, prf: 'hmac-sha256', @@ -84,24 +110,79 @@ function mergeKDFParamsWithDefaults(params: V3Params): KDFParams { prf: kdfDefaults.prf, n: params.n || kdfDefaults.n, r: params.r || kdfDefaults.r, - p: params.p || kdfDefaults.c, + p: params.p || kdfDefaults.p, } } -function stripUnusedKDFParamsForPBKDF2(params: KDFParams): Partial { - delete params.n - delete params.r - delete params.p - return params +// JSON keystore types + +// https://github.com/ethereum/homestead-guide/blob/master/old-docs-for-reference/go-ethereum-wiki.rst/Passphrase-protected-key-store-spec.rst +interface V1Keystore { + Address: string + Crypto: { + CipherText: string + IV: string + KeyHeader: { + Kdf: string + KdfParams: { + DkLen: number + N: number + P: number + R: number + SaltLen: number + } + Version: string + } + MAC: string + Salt: string + } + Id: string + Version: string } -function stripUnusedKDFParamsForScrypt(params: KDFParams): Partial { - delete params.c - delete params.prf - return params +// https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition +interface V3Keystore { + crypto: { + cipher: string + cipherparams: { + iv: string + } + ciphertext: string + kdf: string + kdfparams: KDFParams + mac: string + } + id: string + version: number +} + +interface EthSaleKeystore { + encseed: string + ethaddr: string + btcaddr: string + email: string } -class Wallet { +// wallet implementation + +export default class Wallet { + constructor( + private readonly privateKey?: Buffer | undefined, + private publicKey: Buffer | undefined = undefined, + ) { + if (privateKey && publicKey) { + throw new Error('Cannot supply both a private and a public key to the constructor') + } + + if (privateKey && !ethUtil.isValidPrivate(privateKey)) { + throw new Error('Private key does not satisfy the curve requirements (ie. it is invalid)') + } + + if (publicKey && !ethUtil.isValidPublic(publicKey)) { + throw new Error('Invalid public key') + } + } + // static methods public static generate(icapDirect: boolean = false): Wallet { @@ -164,10 +245,8 @@ class Wallet { return Wallet.fromPrivateKey(tmp.slice(46)) } - // https://github.com/ethereum/go-ethereum/wiki/Passphrase-protected-key-store-spec - public static fromV1(input: string | Object, password: string): Wallet { - const json = typeof input === 'object' ? input : JSON.parse(input) - + public static fromV1(input: string | V1Keystore, password: string): Wallet { + const json: V1Keystore = typeof input === 'object' ? input : JSON.parse(input) if (json.Version !== '1') { throw new Error('Not a V1 Wallet') } @@ -187,7 +266,6 @@ class Wallet { const ciphertext = Buffer.from(json.Crypto.CipherText, 'hex') const mac = ethUtil.keccak256(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) - if (mac.toString('hex') !== json.Crypto.MAC) { throw new Error('Key derivation failed - possibly wrong passphrase') } @@ -198,16 +276,15 @@ class Wallet { Buffer.from(json.Crypto.IV, 'hex'), ) const seed = runCipherBuffer(decipher, ciphertext) - return new Wallet(seed) } public static fromV3( - input: string | Object, + input: string | V3Keystore, password: string, nonStrict: boolean = false, ): Wallet { - const json = + const json: V3Keystore = typeof input === 'object' ? input : JSON.parse(nonStrict ? input.toLowerCase() : input) if (json.version !== 3) { @@ -264,8 +341,8 @@ class Wallet { * Based on https://github.com/ethereum/pyethsaletool/blob/master/pyethsaletool.py * JSON fields: encseed, ethaddr, btcaddr, email */ - public static fromEthSale(input: string | Object, password: string): Wallet { - const json = typeof input === 'object' ? input : JSON.parse(input) + public static fromEthSale(input: string | EthSaleKeystore, password: string): Wallet { + const json: EthSaleKeystore = typeof input === 'object' ? input : JSON.parse(input) const encseed = Buffer.from(json.encseed, 'hex') @@ -301,26 +378,6 @@ class Wallet { return this.privateKey } - constructor( - private readonly privateKey?: Buffer | undefined, - private publicKey: Buffer | undefined = undefined, - ) { - if (privateKey && publicKey) { - throw new Error('Cannot supply both a private and a public key to the constructor') - } - - if (!privateKey && !publicKey) { - } - - if (privateKey && !ethUtil.isValidPrivate(privateKey)) { - throw new Error('Private key does not satisfy the curve requirements (ie. it is invalid)') - } - - if (publicKey && !ethUtil.isValidPublic(publicKey)) { - throw new Error('Invalid public key') - } - } - // public instance methods public getPrivateKey(): Buffer { @@ -351,7 +408,7 @@ class Wallet { return ethUtil.toChecksumAddress(this.getAddressString()) } - public toV3(password: string, opts?: Partial) { + public toV3(password: string, opts?: Partial): V3Keystore { if (!keyExists(this.privateKey)) { throw new Error('This is a public key only wallet') } @@ -359,7 +416,9 @@ class Wallet { const params = mergeToV3ParamsWithDefaults(opts) const kdfParams = mergeKDFParamsWithDefaults(params) - let derivedKey: Buffer, finalKDFParams: Partial + let derivedKey: Buffer + let finalKDFParams: KDFParams + if (params.kdf === 'pbkdf2') { derivedKey = crypto.pbkdf2Sync( Buffer.from(password), @@ -368,7 +427,7 @@ class Wallet { kdfParams.dklen, 'sha256', ) - finalKDFParams = stripUnusedKDFParamsForPBKDF2(kdfParams) + finalKDFParams = kdfParamsForPBKDF(kdfParams) } else if (params.kdf === 'scrypt') { // FIXME: support progress reporting callback derivedKey = scryptsy( @@ -379,7 +438,7 @@ class Wallet { kdfParams.p, kdfParams.dklen, ) - finalKDFParams = stripUnusedKDFParamsForScrypt(kdfParams) + finalKDFParams = kdfParamsForScrypt(kdfParams) } else { throw new Error('Unsupported kdf') } @@ -401,6 +460,7 @@ class Wallet { return { version: 3, id: uuidv4({ random: params.uuid }), + // @ts-ignore FIXME: official V3 keystore spec omits the address key address: this.getAddress().toString('hex'), crypto: { ciphertext: ciphertext.toString('hex'), @@ -442,8 +502,6 @@ function runCipherBuffer(cipher: crypto.Cipher | crypto.Decipher, data: Buffer): return Buffer.concat([cipher.update(data), cipher.final()]) } -function keyExists(k: Buffer | undefined): k is Buffer { - return k !== undefined +function keyExists(k: Buffer | undefined | null): k is Buffer { + return k !== undefined && k !== null } - -export = Wallet diff --git a/src/provider-engine.ts b/src/provider-engine.ts index 9f44684..f27ed96 100644 --- a/src/provider-engine.ts +++ b/src/provider-engine.ts @@ -1,8 +1,8 @@ -import Wallet = require('./index') +import Wallet from './index' const HookedWalletEthTxSubprovider = require('web3-provider-engine/subproviders/hooked-wallet-ethtx') -class WalletSubprovider extends HookedWalletEthTxSubprovider { +export default class WalletSubprovider extends HookedWalletEthTxSubprovider { constructor(wallet: Wallet, opts?: any) { if (!opts) { opts = {} @@ -20,5 +20,3 @@ class WalletSubprovider extends HookedWalletEthTxSubprovider { super(opts) } } - -export = WalletSubprovider diff --git a/src/thirdparty.ts b/src/thirdparty.ts index 091ffb1..bbb2179 100644 --- a/src/thirdparty.ts +++ b/src/thirdparty.ts @@ -1,7 +1,7 @@ import * as crypto from 'crypto' import * as ethUtil from 'ethereumjs-util' -import Wallet = require('./index') +import Wallet from './index' const scryptsy = require('scrypt.js') const utf8 = require('utf8') @@ -95,12 +95,30 @@ function decodeCryptojsSalt(input: string): { ciphertext: Buffer; salt?: Buffer return { ciphertext } } +// { +// "address": "0x169aab499b549eac087035e640d3f7d882ef5e2d", +// "encrypted": true, +// "locked": true, +// "hash": "342f636d174cc1caa49ce16e5b257877191b663e0af0271d2ea03ac7e139317d", +// "private": "U2FsdGVkX19ZrornRBIfl1IDdcj6S9YywY8EgOeOtLj2DHybM/CHL4Jl0jcwjT+36kDnjj+qEfUBu6J1mGQF/fNcD/TsAUgGUTEUEOsP1CKDvNHfLmWLIfxqnYHhHsG5", +// "public": "U2FsdGVkX19EaDNK52q7LEz3hL/VR3dYW5VcoP04tcVKNS0Q3JINpM4XzttRJCBtq4g22hNDrOR8RWyHuh3nPo0pRSe9r5AUfEiCLaMBAhI16kf2KqCA8ah4brkya9ZLECdIl0HDTMYfDASBnyNXd87qodt46U0vdRT3PppK+9hsyqP8yqm9kFcWqMHktqubBE937LIU0W22Rfw6cJRwIw==" +// } + +interface EtherWalletOptions { + address: string + encrypted: boolean + locked: boolean + hash: string + private: string + public: string +} + /* * This wallet format is created by https://github.com/SilentCicero/ethereumjs-accounts * and used on https://www.myetherwallet.com/ */ -function fromEtherWallet(input: string | Object, password: string): Wallet { - const json = typeof input === 'object' ? input : JSON.parse(input) +function fromEtherWallet(input: string | EtherWalletOptions, password: string): Wallet { + const json: EtherWalletOptions = typeof input === 'object' ? input : JSON.parse(input) let privateKey: Buffer if (!json.locked) { @@ -109,16 +127,20 @@ function fromEtherWallet(input: string | Object, password: string): Wallet { } privateKey = Buffer.from(json.private, 'hex') } else { + if (typeof password !== 'string') { + throw new Error('Password required') + } + if (password.length < 7) { throw new Error('Password must be at least 7 characters') } // the "encrypted" version has the low 4 bytes // of the hash of the address appended - let cipher = json.encrypted ? json.private.slice(0, 128) : json.private + const hash = json.encrypted ? json.private.slice(0, 128) : json.private // decode openssl ciphertext + salt encoding - cipher = decodeCryptojsSalt(cipher) + const cipher = decodeCryptojsSalt(hash) if (!cipher.salt) { throw new Error('Unsupported EtherWallet key format') } @@ -183,6 +205,10 @@ function fromKryptoKit(entropy: string, password: string): Wallet { if (type === 'd') { privateKey = ethUtil.sha256(entropy) } else if (type === 'q') { + if (typeof password !== 'string') { + throw new Error('Password required') + } + const encryptedSeed = ethUtil.sha256(Buffer.from(entropy.slice(0, 30))) const checksum = entropy.slice(30, 46) @@ -246,4 +272,4 @@ const Thirdparty = { fromQuorumWallet, } -export = Thirdparty +export default Thirdparty diff --git a/test/hdkey.ts b/test/hdkey.ts index fdebfab..7b869a0 100644 --- a/test/hdkey.ts +++ b/test/hdkey.ts @@ -1,5 +1,5 @@ import * as assert from 'assert' -import EthereumHDKey = require('../src/hdkey') +import EthereumHDKey from '../src/hdkey' // from BIP39 mnemonic: awake book subject inch gentle blur grant damage process float month clown const fixtureseed = Buffer.from( diff --git a/test/index.ts b/test/index.ts index a825430..79dc152 100644 --- a/test/index.ts +++ b/test/index.ts @@ -2,8 +2,8 @@ import * as assert from 'assert' import * as ethUtil from 'ethereumjs-util' -import Wallet = require('../src') -import Thirdparty = require('../src/thirdparty') +import Wallet from '../src' +import Thirdparty from '../src/thirdparty' const fixturePrivateKey = 'efca4cdd31923b50f4214af5d2ae10e7ac45a5019e9431cc195482d707485378' const fixturePrivateKeyStr = '0x' + fixturePrivateKey From 807b98003d45b28c59c213894440dcae42ae7ae1 Mon Sep 17 00:00:00 2001 From: Jack Cook Date: Tue, 16 Jul 2019 20:32:06 -0700 Subject: [PATCH 7/7] cleaner flow around the .toV3 KDF parameters --- src/index.ts | 124 ++++++++++++++++++++++----------------------------- 1 file changed, 53 insertions(+), 71 deletions(-) diff --git a/src/index.ts b/src/index.ts index d3cd8e4..7852ece 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,7 +52,12 @@ function mergeToV3ParamsWithDefaults(params?: Partial): V3Params { } } -// KDF params +// KDF + +const enum KDFFunctions { + PBKDF = 'pbkdf2', + Scrypt = 'scrypt', +} interface ScryptKDFParams { dklen: number @@ -69,48 +74,24 @@ interface PBKDFParams { salt: string } -// union of both the PBKDF2 and Scrypt KDF parameters representing all possible -// parameters the user could supply -type AllKDFParams = ScryptKDFParams & PBKDFParams - type KDFParams = ScryptKDFParams | PBKDFParams -function kdfParamsForPBKDF(params: AllKDFParams): PBKDFParams { - delete params.n - delete params.p - delete params.r - return params -} - -function kdfParamsForScrypt(params: AllKDFParams): ScryptKDFParams { - delete params.c - delete params.prf - return params -} - -/** - * Based on the parameter list passed to the Wallet.prototype.toV3() method this - * returns a list of parameters for running the key derivation function. - * @param params params passed into the .toV3() method - */ -function mergeKDFParamsWithDefaults(params: V3Params): AllKDFParams { - const kdfDefaults = { - c: 262144, +function kdfParamsForPBKDF(opts: V3Params): PBKDFParams { + return { + dklen: opts.dklen, + salt: opts.salt.toString('hex'), + c: opts.c, prf: 'hmac-sha256', - n: 262144, - r: 8, - p: 1, - salt: params.salt.toString('hex'), } +} +function kdfParamsForScrypt(opts: V3Params): ScryptKDFParams { return { - dklen: params.dklen, - salt: kdfDefaults.salt, - c: params.c || kdfDefaults.c, - prf: kdfDefaults.prf, - n: params.n || kdfDefaults.n, - r: params.r || kdfDefaults.r, - p: params.p || kdfDefaults.p, + dklen: opts.dklen, + salt: opts.salt.toString('hex'), + n: opts.n, + r: opts.r, + p: opts.p, } } @@ -413,40 +394,41 @@ export default class Wallet { throw new Error('This is a public key only wallet') } - const params = mergeToV3ParamsWithDefaults(opts) - const kdfParams = mergeKDFParamsWithDefaults(params) + const v3Params: V3Params = mergeToV3ParamsWithDefaults(opts) + let kdfParams: PBKDFParams | ScryptKDFParams let derivedKey: Buffer - let finalKDFParams: KDFParams - - if (params.kdf === 'pbkdf2') { - derivedKey = crypto.pbkdf2Sync( - Buffer.from(password), - params.salt, - kdfParams.c, - kdfParams.dklen, - 'sha256', - ) - finalKDFParams = kdfParamsForPBKDF(kdfParams) - } else if (params.kdf === 'scrypt') { - // FIXME: support progress reporting callback - derivedKey = scryptsy( - Buffer.from(password), - params.salt, - kdfParams.n, - kdfParams.r, - kdfParams.p, - kdfParams.dklen, - ) - finalKDFParams = kdfParamsForScrypt(kdfParams) - } else { - throw new Error('Unsupported kdf') + switch (v3Params.kdf) { + case KDFFunctions.PBKDF: + kdfParams = kdfParamsForPBKDF(v3Params) + derivedKey = crypto.pbkdf2Sync( + Buffer.from(password), + v3Params.salt, + kdfParams.c, + kdfParams.dklen, + 'sha256', + ) + break + case KDFFunctions.Scrypt: + kdfParams = kdfParamsForScrypt(v3Params) + // FIXME: support progress reporting callback + derivedKey = scryptsy( + Buffer.from(password), + v3Params.salt, + kdfParams.n, + kdfParams.r, + kdfParams.p, + kdfParams.dklen, + ) + break + default: + throw new Error('Unsupported kdf') } const cipher: crypto.Cipher = crypto.createCipheriv( - params.cipher, + v3Params.cipher, derivedKey.slice(0, 16), - params.iv, + v3Params.iv, ) if (!cipher) { throw new Error('Unsupported cipher') @@ -459,15 +441,15 @@ export default class Wallet { return { version: 3, - id: uuidv4({ random: params.uuid }), - // @ts-ignore FIXME: official V3 keystore spec omits the address key + id: uuidv4({ random: v3Params.uuid }), + // @ts-ignore - the official V3 keystore spec omits the address key address: this.getAddress().toString('hex'), crypto: { ciphertext: ciphertext.toString('hex'), - cipherparams: { iv: params.iv.toString('hex') }, - cipher: params.cipher, - kdf: params.kdf, - kdfparams: finalKDFParams, + cipherparams: { iv: v3Params.iv.toString('hex') }, + cipher: v3Params.cipher, + kdf: v3Params.kdf, + kdfparams: kdfParams, mac: mac.toString('hex'), }, }