diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c1ca392 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock = false diff --git a/.travis.yml b/.travis.yml index e9ecd50..62cc53f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: node_js node_js: - - "6" - "8" - "10" addons: diff --git a/package.json b/package.json index 8359186..17c8014 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,11 @@ "@types/bn.js": "^4.11.5", "@types/mocha": "^5.2.7", "@types/node": "^12.0.10", + "@types/lodash.zip": "^4.2.6", "coveralls": "^3.0.0", + "ethers": "^4.0.33", "husky": "^2.1.0", + "lodash.zip": "^4.2.0", "mocha": "^5.2.0", "nyc": "^14.1.1", "prettier": "^1.15.3", diff --git a/src/index.ts b/src/index.ts index 7852ece..8845f61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,19 @@ const uuidv4 = require('uuid/v4') // parameters for the toV3() method interface V3Params { + kdf: string + cipher: string + salt: string | Buffer + iv: string | Buffer + uuid: string | Buffer + dklen: number + c: number + n: number + r: number + p: number +} + +interface V3ParamsStrict { kdf: string cipher: string salt: Buffer @@ -21,8 +34,43 @@ interface V3Params { p: number } -function mergeToV3ParamsWithDefaults(params?: Partial): V3Params { - const v3Defaults: V3Params = { +function validateHexString(paramName: string, str: string, length?: number) { + if (str.toLowerCase().startsWith('0x')) { + str = str.slice(2) + } + if (!str && !length) { + return str + } + if ((length as number) % 2) { + throw new Error(`Invalid length argument, must be an even number`) + } + if (typeof length === 'number' && str.length !== length) { + throw new Error(`Invalid ${paramName}, string must be ${length} hex characters`) + } + if (!/^([0-9a-f]{2})+$/i.test(str)) { + const howMany = typeof length === 'number' ? length : 'empty or a non-zero even number of' + throw new Error(`Invalid ${paramName}, string must be ${howMany} hex characters`) + } + return str +} + +function validateBuffer(paramName: string, buff: Buffer, length?: number) { + if (!Buffer.isBuffer(buff)) { + const howManyHex = + typeof length === 'number' ? `${length * 2}` : 'empty or a non-zero even number of' + const howManyBytes = typeof length === 'number' ? ` (${length} bytes)` : '' + throw new Error( + `Invalid ${paramName}, must be a string (${howManyHex} hex characters) or buffer${howManyBytes}`, + ) + } + if (typeof length === 'number' && buff.length !== length) { + throw new Error(`Invalid ${paramName}, buffer must be ${length} bytes`) + } + return buff +} + +function mergeToV3ParamsWithDefaults(params?: Partial): V3ParamsStrict { + const v3Defaults: V3ParamsStrict = { cipher: 'aes-128-ctr', kdf: 'scrypt', salt: randomBytes(32), @@ -38,17 +86,30 @@ function mergeToV3ParamsWithDefaults(params?: Partial): V3Params { if (!params) { return v3Defaults } + + if (typeof params.salt === 'string') { + params.salt = Buffer.from(validateHexString('salt', params.salt), 'hex') + } + if (typeof params.iv === 'string') { + params.iv = Buffer.from(validateHexString('iv', params.iv, 32), 'hex') + } + if (typeof params.uuid === 'string') { + params.uuid = Buffer.from(validateHexString('uuid', params.uuid, 32), 'hex') + } + + if (params.salt) { + validateBuffer('salt', params.salt) + } + if (params.iv) { + validateBuffer('iv', params.iv, 16) + } + if (params.uuid) { + validateBuffer('uuid', params.uuid, 16) + } + 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, + ...v3Defaults, + ...(params as V3ParamsStrict), } } @@ -60,6 +121,14 @@ const enum KDFFunctions { } interface ScryptKDFParams { + dklen: number + n: number + p: number + r: number + salt: Buffer +} + +interface ScryptKDFParamsOut { dklen: number n: number p: number @@ -68,6 +137,13 @@ interface ScryptKDFParams { } interface PBKDFParams { + c: number + dklen: number + prf: string + salt: Buffer +} + +interface PBKDFParamsOut { c: number dklen: number prf: string @@ -75,20 +151,21 @@ interface PBKDFParams { } type KDFParams = ScryptKDFParams | PBKDFParams +type KDFParamsOut = ScryptKDFParamsOut | PBKDFParamsOut -function kdfParamsForPBKDF(opts: V3Params): PBKDFParams { +function kdfParamsForPBKDF(opts: V3ParamsStrict): PBKDFParams { return { dklen: opts.dklen, - salt: opts.salt.toString('hex'), + salt: opts.salt, c: opts.c, prf: 'hmac-sha256', } } -function kdfParamsForScrypt(opts: V3Params): ScryptKDFParams { +function kdfParamsForScrypt(opts: V3ParamsStrict): ScryptKDFParams { return { dklen: opts.dklen, - salt: opts.salt.toString('hex'), + salt: opts.salt, n: opts.n, r: opts.r, p: opts.p, @@ -130,7 +207,7 @@ interface V3Keystore { } ciphertext: string kdf: string - kdfparams: KDFParams + kdfparams: KDFParamsOut mac: string } id: string @@ -361,6 +438,7 @@ export default class Wallet { // public instance methods + // tslint:disable-next-line public getPrivateKey(): Buffer { return this.privKey } @@ -369,6 +447,7 @@ export default class Wallet { return ethUtil.bufferToHex(this.privKey) } + // tslint:disable-next-line public getPublicKey(): Buffer { return this.pubKey } @@ -394,16 +473,16 @@ export default class Wallet { throw new Error('This is a public key only wallet') } - const v3Params: V3Params = mergeToV3ParamsWithDefaults(opts) + const v3Params: V3ParamsStrict = mergeToV3ParamsWithDefaults(opts) - let kdfParams: PBKDFParams | ScryptKDFParams + let kdfParams: KDFParams let derivedKey: Buffer switch (v3Params.kdf) { case KDFFunctions.PBKDF: kdfParams = kdfParamsForPBKDF(v3Params) derivedKey = crypto.pbkdf2Sync( Buffer.from(password), - v3Params.salt, + kdfParams.salt, kdfParams.c, kdfParams.dklen, 'sha256', @@ -414,7 +493,7 @@ export default class Wallet { // FIXME: support progress reporting callback derivedKey = scryptsy( Buffer.from(password), - v3Params.salt, + kdfParams.salt, kdfParams.n, kdfParams.r, kdfParams.p, @@ -449,7 +528,10 @@ export default class Wallet { cipherparams: { iv: v3Params.iv.toString('hex') }, cipher: v3Params.cipher, kdf: v3Params.kdf, - kdfparams: kdfParams, + kdfparams: { + ...kdfParams, + salt: kdfParams.salt.toString('hex'), + }, mac: mac.toString('hex'), }, } diff --git a/test/index.ts b/test/index.ts index 79dc152..ab0cf4a 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,10 +1,17 @@ /* tslint:disable no-invalid-this */ import * as assert from 'assert' import * as ethUtil from 'ethereumjs-util' +import { Wallet as ethersWallet } from 'ethers' + +const zip = require('lodash.zip') import Wallet from '../src' import Thirdparty from '../src/thirdparty' +const n = 262144 +const r = 8 +const p = 1 + const fixturePrivateKey = 'efca4cdd31923b50f4214af5d2ae10e7ac45a5019e9431cc195482d707485378' const fixturePrivateKeyStr = '0x' + fixturePrivateKey const fixturePrivateKeyBuffer = Buffer.from(fixturePrivateKey, 'hex') @@ -15,6 +22,7 @@ const fixturePublicKeyStr = '0x' + fixturePublicKey const fixturePublicKeyBuffer = Buffer.from(fixturePublicKey, 'hex') const fixtureWallet = Wallet.fromPrivateKey(fixturePrivateKeyBuffer) +const fixtureEthersWallet = new ethersWallet(fixtureWallet.getPrivateKeyString()) describe('.getPrivateKey()', function() { it('should work', function() { @@ -157,14 +165,14 @@ describe('.generate()', function() { describe('.generateVanityAddress()', function() { it('should generate an account with 000 prefix (object)', function() { - this.timeout(180000) // 3minutes + this.timeout(0) // never 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 + this.timeout(0) // never const wallet = Wallet.generateVanityAddress('^000') assert.strictEqual(wallet.getPrivateKey().length, 32) assert.strictEqual(wallet.getAddress()[0], 0) @@ -182,42 +190,416 @@ describe('.getV3Filename()', function() { }) 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 pw = 'testtest' + const salt = 'dc9e4a98886738bd8aae134a1f89aaa5a502c3fbd10e336136d4d5fe47448ad6' + const iv = 'cecacd85e9cb89788b5aab2f93361233' + const uuid = '7e59dc028d42d09db29aa8a0f862cc81' + + const strKdfOptions = { iv, salt, uuid } + const buffKdfOptions = { + salt: Buffer.from(salt, 'hex'), + iv: Buffer.from(iv, 'hex'), + uuid: Buffer.from(uuid, 'hex'), + } + + // generate all possible combinations of salt, iv, uuid properties, e.g. + // {salt: [string], iv: [buffer], uuid: [string]} + // the number of objects is naturally a radix for selecting one of the + // input values for a given property; example, three objects and two keys: + // [{a: 0, b: 0}, + // {a: 1, b: 1}, + // {a: 2, b: 2}] + const makePermutations = (...objs: Array): Array => { + const permus = [] + const keys = Array.from( + objs.reduce((acc: Set, curr: object) => { + Object.keys(curr).forEach(key => { + acc.add(key) + }) + return acc + }, new Set()), + ) + const radix = objs.length + const numPermus = radix ** keys.length + for (let permuIdx = 0; permuIdx < numPermus; permuIdx++) { + const selectors = permuIdx + .toString(radix) + .padStart(keys.length, '0') + .split('') + .map(v => parseInt(v, 10)) + const obj: any = {} + zip(selectors, keys).forEach(([sel, k]: [number, string]) => { + if ((objs as any)[sel].hasOwnProperty(k)) { + obj[k] = (objs as any)[sel][k] + } + }) + permus.push(obj) + } + return permus + } + + const makeEthersOptions = (opts: object) => { + const obj: any = {} + Object.entries(opts).forEach(([key, val]: [string, string | Buffer]) => { + obj[key] = typeof val === 'string' ? '0x' + val : val + }) + return obj + } + + const permutations = makePermutations(strKdfOptions, buffKdfOptions) + + it('should work with PBKDF2', async function() { + this.timeout(0) // never 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, + + await Promise.all( + (permutations as Array<{ + salt: string | Buffer + iv: string | Buffer + uuid: string | Buffer + }>).map(async function({ salt, iv, uuid }) { + const encFixtureWallet = fixtureWallet.toV3String(pw, { + kdf: 'pbkdf2', + c: n, + uuid: uuid, + salt: salt, + iv: iv, + }) + + assert.deepStrictEqual(JSON.parse(w), JSON.parse(encFixtureWallet)) + // ethers doesn't support encrypting with PBKDF2 + }), ) }) - it('should work with Scrypt', function() { - const w = + it('should work with Scrypt', async function() { + this.timeout(0) // never + const wStatic = '{"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, + const wRandom = Wallet.generate() + const wEthers = new ethersWallet(wRandom.getPrivateKeyString()) + + await Promise.all( + (permutations as Array<{ + salt: string | Buffer + iv: string | Buffer + uuid: string | Buffer + }>).map(async function({ salt, iv, uuid }) { + const ethersOpts = makeEthersOptions({ salt, iv, uuid }) + + const encFixtureWallet = fixtureWallet.toV3String(pw, { + kdf: 'scrypt', + uuid: uuid, + salt: salt, + iv: iv, + n: n, + r: r, + p: p, + }) + + const encFixtureEthersWallet = (await fixtureEthersWallet.encrypt(pw, { + scrypt: { N: n, r: r, p: p }, + salt: ethersOpts.salt, + iv: ethersOpts.iv, + uuid: ethersOpts.uuid, + })).toLowerCase() + + const encRandomWallet = wRandom.toV3String(pw, { + kdf: 'scrypt', + uuid: uuid, + salt: salt, + iv: iv, + n: n, + r: r, + p: p, + }) + + const encEthersWallet = (await wEthers.encrypt(pw, { + scrypt: { N: n, r: r, p: p }, + salt: ethersOpts.salt, + iv: ethersOpts.iv, + uuid: ethersOpts.uuid, + })).toLowerCase() + + assert.deepStrictEqual(JSON.parse(wStatic), JSON.parse(encFixtureWallet)) + assert.deepStrictEqual(JSON.parse(wStatic), JSON.parse(encFixtureEthersWallet)) + assert.deepStrictEqual(JSON.parse(encRandomWallet), JSON.parse(encEthersWallet)) + }), ) }) it('should work without providing options', function() { - this.timeout(180000) // 3minutes + this.timeout(0) // never assert.strictEqual(fixtureWallet.toV3('testtest')['version'], 3) }) it('should fail for unsupported kdf', function() { - this.timeout(180000) // 3minutes + this.timeout(0) // never assert.throws(function() { fixtureWallet.toV3('testtest', { kdf: 'superkey' }) }, /^Error: Unsupported kdf$/) }) + it('should fail for bad salt', function() { + const pw = 'test' + const errStr = /^Error: Invalid salt, string must be empty or a non-zero even number of hex characters$/ + + assert.throws(function() { + fixtureWallet.toV3(pw, { salt: 'f' }) + }, errStr) + assert.throws(function() { + fixtureWallet.toV3(pw, { salt: 'fff' }) + }, errStr) + assert.throws(function() { + fixtureWallet.toV3(pw, { salt: 'xfff' }) + }, errStr) + assert.throws(function() { + fixtureWallet.toV3(pw, { salt: 'fffx' }) + }, errStr) + assert.throws(function() { + fixtureWallet.toV3(pw, { salt: 'fffxff' }) + }, errStr) + assert.throws(function() { + // @ts-ignore + fixtureWallet.toV3(pw, { salt: {} }) + }, /^Error: Invalid salt, must be a string \(empty or a non-zero even number of hex characters\) or buffer$/) + }) + it('should work with empty salt', async function() { + this.timeout(0) // never + const pw = 'test' + let salt: any = '' + let w = fixtureWallet.toV3(pw, { salt: salt, kdf: 'pbkdf2' }) + + assert.strictEqual(salt, w.crypto.kdfparams.salt) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(w, pw).getPrivateKeyString(), + ) + + salt = '0x' + w = fixtureWallet.toV3(pw, { salt: salt, kdf: 'pbkdf2' }) + + assert.strictEqual('', w.crypto.kdfparams.salt) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(w, pw).getPrivateKeyString(), + ) + + salt = Buffer.from('', 'hex') + w = fixtureWallet.toV3(pw, { salt: salt, kdf: 'pbkdf2' }) + + assert.strictEqual('', w.crypto.kdfparams.salt) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(w, pw).getPrivateKeyString(), + ) + + salt = '' + let iv = 'ffffffffffffffffffffffffffffffff' + let uuid = 'ffffffffffffffffffffffffffffffff' + let wStr = fixtureWallet.toV3String(pw, { + salt: salt, + iv: iv, + uuid: uuid, + kdf: 'scrypt', + n: n, + r: r, + p: p, + }) + let wEthersStr = await new ethersWallet(fixtureWallet.getPrivateKeyString()).encrypt(pw, { + scrypt: { N: n, r: r, p: p }, + salt: '0x' + (salt as string), + iv: '0x' + iv, + uuid: '0x' + uuid, + }) + + assert.strictEqual(salt, JSON.parse(wStr).crypto.kdfparams.salt) + assert.deepStrictEqual(JSON.parse(wStr), JSON.parse(wEthersStr.toLowerCase())) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(JSON.parse(wStr), pw).getPrivateKeyString(), + ) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + (await ethersWallet.fromEncryptedJson(wEthersStr, pw)).privateKey, + ) + + salt = '0x' + iv = '0x' + iv + uuid = '0x' + uuid + wStr = fixtureWallet.toV3String(pw, { + salt: salt, + iv: iv, + uuid: uuid, + kdf: 'scrypt', + n: n, + r: r, + p: p, + }) + wEthersStr = await new ethersWallet(fixtureWallet.getPrivateKeyString()).encrypt(pw, { + scrypt: { N: n, r: r, p: p }, + salt: salt, + iv: iv, + uuid: uuid, + }) + + assert.strictEqual('', JSON.parse(wStr).crypto.kdfparams.salt) + assert.deepStrictEqual(JSON.parse(wStr), JSON.parse(wEthersStr.toLowerCase())) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(JSON.parse(wStr), pw).getPrivateKeyString(), + ) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + (await ethersWallet.fromEncryptedJson(wEthersStr, pw)).privateKey, + ) + + salt = Buffer.from('', 'hex') + wStr = fixtureWallet.toV3String(pw, { + salt: salt, + iv: iv, + uuid: uuid, + kdf: 'scrypt', + n: n, + r: r, + p: p, + }) + wEthersStr = await new ethersWallet(fixtureWallet.getPrivateKeyString()).encrypt(pw, { + scrypt: { N: n, r: r, p: p }, + salt: salt, + iv: iv, + uuid: uuid, + }) + + assert.strictEqual('', JSON.parse(wStr).crypto.kdfparams.salt) + assert.deepStrictEqual(JSON.parse(wStr), JSON.parse(wEthersStr.toLowerCase())) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(JSON.parse(wStr), pw).getPrivateKeyString(), + ) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + (await ethersWallet.fromEncryptedJson(wEthersStr, pw)).privateKey, + ) + }) + it('should fail for bad iv', function() { + const pw = 'test' + const errStrLength = /^Error: Invalid iv, string must be 32 hex characters$/ + const errBuffLength = /^Error: Invalid iv, buffer must be 16 bytes$/ + + assert.throws(function() { + fixtureWallet.toV3(pw, { iv: '' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { iv: 'ff' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { iv: 'ffffffffffffffffffffffffffffffffff' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { iv: 'xfffffffffffffffffffffffffffffff' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { iv: 'fffffffffffffffffffffffffffffffx' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { iv: 'fffffffffffffffxffffffffffffffff' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { iv: Buffer.from('', 'hex') }) + }, errBuffLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { iv: Buffer.from('ff', 'hex') }) + }, errBuffLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { iv: Buffer.from('ffffffffffffffffffffffffffffffffff', 'hex') }) + }, errBuffLength) + assert.throws(function() { + // @ts-ignore + fixtureWallet.toV3(pw, { iv: {} }) + }, /^Error: Invalid iv, must be a string \(32 hex characters\) or buffer \(16 bytes\)$/) + }) + it('should fail for bad uuid', function() { + const pw = 'test' + const errStrLength = /^Error: Invalid uuid, string must be 32 hex characters$/ + const errBuffLength = /^Error: Invalid uuid, buffer must be 16 bytes$/ + + assert.throws(function() { + fixtureWallet.toV3(pw, { uuid: '' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { uuid: 'ff' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { uuid: 'ffffffffffffffffffffffffffffffffff' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { uuid: 'xfffffffffffffffffffffffffffffff' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { uuid: 'fffffffffffffffffffffffffffffffx' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { uuid: 'fffffffffffffffxffffffffffffffff' }) + }, errStrLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { uuid: Buffer.from('', 'hex') }) + }, errBuffLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { uuid: Buffer.from('ff', 'hex') }) + }, errBuffLength) + assert.throws(function() { + fixtureWallet.toV3(pw, { uuid: Buffer.from('ffffffffffffffffffffffffffffffffff', 'hex') }) + }, errBuffLength) + assert.throws(function() { + // @ts-ignore + fixtureWallet.toV3(pw, { uuid: {} }) + }, /^Error: Invalid uuid, must be a string \(32 hex characters\) or buffer \(16 bytes\)$/) + }) + it('should strip leading "0x" from salt, iv, uuid', function() { + this.timeout(0) // never + const pw = 'test' + const salt = + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const iv = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + const uuid = 'cccccccccccccccccccccccccccccccc' + let w = fixtureWallet.toV3(pw, { + salt: '0x' + salt, + iv: '0X' + iv, + uuid: '0x' + uuid, + kdf: 'pbkdf2', + }) + let w2 = fixtureWallet.toV3(pw, { salt: '0x' + salt, iv: '0X' + iv, uuid: uuid, kdf: 'pbkdf2' }) + + assert.strictEqual(salt, w.crypto.kdfparams.salt) + assert.strictEqual(iv, w.crypto.cipherparams.iv) + assert.strictEqual(w.id, w2.id) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(w, pw).getPrivateKeyString(), + ) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(w2, pw).getPrivateKeyString(), + ) + + w = fixtureWallet.toV3(pw, { + salt: '0x' + salt, + iv: '0X' + iv, + uuid: '0x' + uuid, + kdf: 'scrypt', + }) + w2 = fixtureWallet.toV3(pw, { salt: '0x' + salt, iv: '0X' + iv, uuid: uuid, kdf: 'scrypt' }) + + assert.strictEqual(salt, w.crypto.kdfparams.salt) + assert.strictEqual(iv, w.crypto.cipherparams.iv) + assert.strictEqual(w.id, w2.id) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(w, pw).getPrivateKeyString(), + ) + assert.strictEqual( + fixtureWallet.getPrivateKeyString(), + Wallet.fromV3(w2, pw).getPrivateKeyString(), + ) + }) }) /* @@ -231,24 +613,53 @@ describe('.fromV1()', function () { */ describe('.fromV3()', function() { - it('should work with PBKDF2', function() { + it('should work with PBKDF2', async 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') + let wEthersCompat = JSON.parse(w) + // see: https://github.com/ethers-io/ethers.js/issues/582 + wEthersCompat.address = '0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b' + wEthersCompat = JSON.stringify(wEthersCompat) + const pw = 'testpassword' + const wallet = Wallet.fromV3(w, pw) + const wRandom = Wallet.generate().toV3String(pw, { kdf: 'pbkdf2' }) + const walletRandom = Wallet.fromV3(wRandom, pw) + + this.timeout(0) // never assert.strictEqual(wallet.getAddressString(), '0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b') + assert.strictEqual( + wallet.getAddressString(), + (await ethersWallet.fromEncryptedJson(wEthersCompat, pw)).address.toLowerCase(), + ) + assert.strictEqual( + walletRandom.getAddressString(), + (await ethersWallet.fromEncryptedJson(wRandom, pw)).address.toLowerCase(), + ) }) - it('should work with Scrypt', function() { + it('should work with Scrypt', async 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 + const pw = 'testtest' + const wallet = Wallet.fromV3(sample, pw) + const sampleRandom = Wallet.generate().toV3String(pw) + const walletRandom = Wallet.fromV3(sampleRandom, pw) + + this.timeout(0) // never assert.strictEqual(wallet.getAddressString(), '0x2f91eb73a6cd5620d7abb50889f24eea7a6a4feb') + assert.strictEqual( + wallet.getAddressString(), + (await ethersWallet.fromEncryptedJson(sample, pw)).address.toLowerCase(), + ) + assert.strictEqual( + walletRandom.getAddressString(), + (await ethersWallet.fromEncryptedJson(sampleRandom, pw)).address.toLowerCase(), + ) }) 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 + this.timeout(0) // never assert.strictEqual(wallet.getAddressString(), '0xa9886ac7489ecbcbd79268a79ef00d940e5fe1f2') }) it('should fail with invalid password', function() { @@ -359,7 +770,7 @@ describe('.fromQuorumWallet()', function() { 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 + new Wallet(fixturePrivateKeyBuffer, fixturePublicKeyBuffer) }, /^Error: Cannot supply both a private and a public key to the constructor$/) }) })