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..612fb85 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 @@ -34,9 +35,10 @@ 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 -/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/.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: 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..8359186 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,25 @@ "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, + } +} + +// KDF + +const enum KDFFunctions { + PBKDF = 'pbkdf2', + Scrypt = 'scrypt', +} + +interface ScryptKDFParams { + dklen: number + n: number + p: number + r: number + salt: string +} + +interface PBKDFParams { + c: number + dklen: number + prf: string + salt: string +} + +type KDFParams = ScryptKDFParams | PBKDFParams + +function kdfParamsForPBKDF(opts: V3Params): PBKDFParams { + return { + dklen: opts.dklen, + salt: opts.salt.toString('hex'), + c: opts.c, + prf: 'hmac-sha256', + } +} + +function kdfParamsForScrypt(opts: V3Params): ScryptKDFParams { + return { + dklen: opts.dklen, + salt: opts.salt.toString('hex'), + n: opts.n, + r: opts.r, + p: opts.p, + } +} + +// 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 +} + +// 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 +} + +// 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 { + 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)) + } + + 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') + } + 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 | V3Keystore, + password: string, + nonStrict: boolean = false, + ): Wallet { + const json: V3Keystore = + 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 | EthSaleKeystore, password: string): Wallet { + const json: EthSaleKeystore = 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 + } + + // 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): V3Keystore { + if (!keyExists(this.privateKey)) { + throw new Error('This is a public key only wallet') + } + + const v3Params: V3Params = mergeToV3ParamsWithDefaults(opts) + + let kdfParams: PBKDFParams | ScryptKDFParams + let derivedKey: Buffer + 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( + v3Params.cipher, + derivedKey.slice(0, 16), + v3Params.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: 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: v3Params.iv.toString('hex') }, + cipher: v3Params.cipher, + kdf: v3Params.kdf, + kdfparams: kdfParams, + 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 | null): k is Buffer { + return k !== undefined && k !== null +} 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..f27ed96 --- /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 default 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..bbb2179 --- /dev/null +++ b/src/thirdparty.ts @@ -0,0 +1,275 @@ +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 } +} + +// { +// "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 | EtherWalletOptions, password: string): Wallet { + const json: EtherWalletOptions = 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 (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 + const hash = json.encrypted ? json.private.slice(0, 128) : json.private + + // decode openssl ciphertext + salt encoding + const cipher = decodeCryptojsSalt(hash) + 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') { + 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) + + 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) +} + +const Thirdparty = { + fromEtherWallet, + fromEtherCamp, + fromKryptoKit, + fromQuorumWallet, +} + +export default Thirdparty diff --git a/test/hdkey.ts b/test/hdkey.ts new file mode 100644 index 0000000..7b869a0 --- /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..79dc152 --- /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" +}