Added x-ethers compatible encrypted mnemonic to JSON wallets.

This commit is contained in:
Richard Moore 2018-01-26 21:56:20 -05:00
parent bb24fce859
commit 4ee9a4d191
No known key found for this signature in database
GPG Key ID: 525F70A6FCABC295
13 changed files with 125 additions and 18 deletions

View File

@ -32,8 +32,15 @@ var privateKeys = {
'0x4a9cf99357f5789251a8d7fad5b86d0f31eeb938': '0xa016182717223d01f776149ec0b4a217d0e9930cad263f205427c6d3cd5560e7',
'0x88a5c2d9919e46f883eb62f7b8dd9d0cc45bc290': '0xf03e581353c794928373fb0893bc731aefc4c4e234e643f3a46998b03cd4d7c5',
'0x17c5185167401ed00cf5f5b2fc97d9bbfdb7d025': '0x4242424242424242424242424242424242424242424242424242424242424242',
'0x012363d61bdc53d0290a0f25e9c89f8257550fb8': '0x4c94faa2c558a998d10ee8b2b9b8eb1fbcb8a6ac5fd085c6f95535604fc1bffb',
'0x15db397ed5f682acb22b0afc6c8de4cdfbda7cbc': '0xcdf3c34a2ea0ff181f462856168f5851e68c37b583eb158403e43aeab4964fee'
}
var mnemonics = {
'0x15db397ed5f682acb22b0afc6c8de4cdfbda7cbc': 'debris glass rich exotic window other film slow expose flight either wealth',
'0x012363d61bdc53d0290a0f25e9c89f8257550fb8': 'service basket parent alcohol fault similar survey twelve hockey cloud walk panel'
};
var walletPath = path.join(__dirname, 'test-wallets');
fs.readdirSync(walletPath).forEach(function(filename) {
var data = require(path.join(walletPath, filename));
@ -44,8 +51,6 @@ fs.readdirSync(walletPath).forEach(function(filename) {
var password = filename.substring(0, filename.length - 5).split('-');
password = password[password.length - 1];
if (password === 'life') { password = 'foo'; }
if (data.ethaddr) {
Output.push({
type: 'crowdsale',
@ -61,6 +66,7 @@ fs.readdirSync(walletPath).forEach(function(filename) {
type: 'secret-storage',
address: prefixAddress(data.address),
json: JSON.stringify(data),
mnemonic: mnemonics[prefixAddress(data.address)] || '',
name: name,
password: password,
privateKey: privateKeys[prefixAddress(data.address)],

View File

@ -0,0 +1 @@
{"address":"012363d61bdc53d0290a0f25e9c89f8257550fb8","id":"5ba8719b-faf9-49ec-8bca-21522e3d56dc","version":3,"Crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"bc0473d60284d2d6994bb6793e916d06"},"ciphertext":"e73ed0b0c53bcaea4516a15faba3f6d76dbe71b9b46a460ed7e04a68e0867dd7","kdf":"scrypt","kdfparams":{"salt":"97f0b6e17c392f76a726ceea02bac98f17265f1aa5cf8f9ad1c2b56025bc4714","n":131072,"dklen":32,"p":1,"r":8},"mac":"ff4f2db7e7588f8dd41374d7b98dfd7746b554c0099a6c0765be7b1c7913e1f3"},"x-ethers":{"client":"ethers.js","gethFilename":"UTC--2018-01-27T01-52-22.0Z--012363d61bdc53d0290a0f25e9c89f8257550fb8","mnemonicCounter":"70224accc00e35328a010a19fef51121","mnemonicCiphertext":"cf835e13e4f90b190052263dbd24b020","version":"0.1"}}

View File

@ -0,0 +1 @@
{"version":3,"id":"05D302EE-23DC-48C4-B89C-CAAAC1C780C4","x-ethers":{"gethFilename":"UTC--2018-01-26T20-25-02.0Z--15db397ed5f682acb22b0afc6c8de4cdfbda7cbc","mnemonicCiphertext":"b92c7c3da540ae7beee55365fb330c33","mnemonicCounter":"a65d689a73c096025f2acae3f7068510","client":"ethers/iOS","version":"0.1"},"Crypto":{"ciphertext":"fa6ff2374087a089ec9fcba3bc21389f5e26ec2932c95b608ebe0218efab83c6","cipherparams":{"iv":"5ddca45b254406516ca2c97d18d5dfd9"},"kdf":"scrypt","kdfparams":{"r":8,"p":1,"n":262144,"dklen":32,"salt":"864a474f8586ab0fa97ed9240ae6227b2b22b48bdf298137c355e4a07eb19831"},"mac":"eaa89325acedbf88c0893e53c8c8d426b590655746c66da9cd76e4f72d84084f","cipher":"aes-128-ctr"},"address":"15db397ed5f682acb22b0afc6c8de4cdfbda7cbc"}

View File

@ -41,6 +41,10 @@ describe('Test JSON Wallets', function() {
'generated correct private key - ' + wallet.privateKey);
assert.equal(wallet.address.toLowerCase(), test.address,
'generate correct address - ' + wallet.address);
if (test.mnemonic) {
assert.equal(wallet.mnemonic, test.mnemonic,
'mnemonic enabled encrypted wallet has a mnemonic');
}
});
});
});

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "ethers-wallet",
"version": "2.1.7",
"version": "2.1.8",
"description": "Wallet and signing library for Ethereum.",
"bugs": {
"url": "http://github.com/ethers-io/ethers.js/issues",

View File

@ -8,7 +8,11 @@ var hmac = require('ethers-utils/hmac');
var pbkdf2 = require('ethers-utils/pbkdf2');
var utils = require('ethers-utils');
var SigningKey = require('./signing-key.js');
var SigningKey = require('./signing-key');
var HDNode = require('./hdnode');
// @TODO: Maybe move this to HDNode?
var defaultPath = "m/44'/60'/0'/0/0";
function arrayify(hexString) {
if (typeof(hexString) === 'string' && hexString.substring(0, 2) !== '0x') {
@ -17,6 +21,12 @@ function arrayify(hexString) {
return utils.arrayify(hexString);
}
function zpad(value, length) {
value = String(value);
while (value.length < length) { value = '0' + value; }
return value;
}
function getPassword(password) {
if (typeof(password) === 'string') {
return utils.toUtf8Bytes(password, 'NFKC');
@ -133,7 +143,7 @@ utils.defineProperty(secretStorage, 'decrypt', function(json, password, progress
var aesCtr = new aes.ModeOfOperation.ctr(key, counter);
return new arrayify(aesCtr.decrypt(ciphertext));
return arrayify(aesCtr.decrypt(ciphertext));
}
return null;
@ -153,6 +163,7 @@ utils.defineProperty(secretStorage, 'decrypt', function(json, password, progress
}
var privateKey = decrypt(key.slice(0, 16), ciphertext);
var mnemonicKey = key.slice(32, 64);
if (!privateKey) {
reject(new Error('unsupported cipher'));
@ -165,6 +176,28 @@ utils.defineProperty(secretStorage, 'decrypt', function(json, password, progress
return null;
}
// Version 0.1 x-ethers metadata must contain an encrypted mnemonic phrase
if (searchPath(data, 'x-ethers/version') === '0.1') {
var mnemonicCiphertext = arrayify(searchPath(data, 'x-ethers/mnemonicCiphertext'), 'x-ethers/mnemonicCiphertext');
var mnemonicIv = arrayify(searchPath(data, 'x-ethers/mnemonicCounter'), 'x-ethers/mnemonicCounter');
var mnemonicCounter = new aes.Counter(mnemonicIv);
var mnemonicAesCtr = new aes.ModeOfOperation.ctr(mnemonicKey, mnemonicCounter);
var path = searchPath(data, 'x-ethers/path') || defaultPath;
var entropy = arrayify(mnemonicAesCtr.decrypt(mnemonicCiphertext));
var mnemonic = HDNode.entropyToMnemonic(entropy);
if (HDNode.fromMnemonic(mnemonic).derivePath(path).privateKey != utils.hexlify(privateKey)) {
reject(new Error('mnemonic mismatch'));
return null;
}
signingKey.mnemonic = mnemonic;
signingKey.path = path;
}
return signingKey;
}
@ -194,7 +227,7 @@ utils.defineProperty(secretStorage, 'decrypt', function(json, password, progress
return;
}
scrypt(password, salt, N, r, p, dkLen, function(error, progress, key) {
scrypt(password, salt, N, r, p, 64, function(error, progress, key) {
if (error) {
error.progress = progress;
reject(error);
@ -265,11 +298,33 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op
if (privateKey instanceof SigningKey) {
privateKey = privateKey.privateKey;
}
privateKey = utils.arrayify(privateKey, 'private key');
privateKey = arrayify(privateKey, 'private key');
if (privateKey.length !== 32) { throw new Error('invalid private key'); }
password = getPassword(password);
var entropy = options.entropy;
if (options.mnemonic) {
if (entropy) {
if (HDNode.entropyToMnemonic(entropy) !== options.mnemonic) {
throw new Error('entropy and mnemonic mismatch');
}
} else {
entropy = HDNode.mnemonicToEntropy(options.mnemonic);
}
}
if (entropy) {
entropy = arrayify(entropy, 'entropy');
}
var path = options.path;
if (entropy && !path) {
path = defaultPath;
}
var client = options.client;
if (!client) { client = "ethers.js"; }
// Check/generate the salt
var salt = options.salt;
if (salt) {
@ -283,13 +338,17 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op
if (options.iv) {
iv = arrayify(options.iv, 'iv');
if (iv.length !== 16) { throw new Error('invalid iv'); }
} else {
iv = utils.randomBytes(16);
}
// Override the uuid
var uuidRandom = options.uuid;
if (uuidRandom) {
uuidRandom = utils.arrayify(uuidRandom, 'uuid');
uuidRandom = arrayify(uuidRandom, 'uuid');
if (uuidRandom.length !== 16) { throw new Error('invalid uuid'); }
} else {
uuidRandom = utils.randomBytes(16);
}
// Override the scrypt password-based key derivation function parameters
@ -304,8 +363,7 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op
// We take 64 bytes:
// - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix)
// - 16 bytes The initialization vector
// - 16 bytes The UUID random bytes
// - 32 bytes AES key to encrypt mnemonic with (required here to be Ethers Wallet)
scrypt(password, salt, N, r, p, 64, function(error, progress, key) {
if (error) {
error.progress = progress;
@ -314,15 +372,12 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op
} else if (key) {
key = arrayify(key);
// These will be used to encrypt the wallet (as per Web3 secret storage)
// This will be used to encrypt the wallet (as per Web3 secret storage)
var derivedKey = key.slice(0, 16);
var macPrefix = key.slice(16, 32);
// Get the initialization vector
if (!iv) { iv = key.slice(32, 48); }
// Get the UUID random data
if (!uuidRandom) { uuidRandom = key.slice(48, 64); }
// This will be used to encrypt the mnemonic phrase (if any)
var mnemonicKey = key.slice(32, 64);
// Get the address for this private key
var address = (new SigningKey(privateKey)).address;
@ -338,7 +393,7 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op
// See: https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
var data = {
address: address.substring(2).toLowerCase(),
id: uuid.v4({random: uuidRandom}),
id: uuid.v4({ random: uuidRandom }),
version: 3,
Crypto: {
cipher: 'aes-128-ctr',
@ -358,6 +413,29 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op
}
};
// If we have a mnemonic, encrypt it into the JSON wallet
if (entropy) {
var mnemonicIv = utils.randomBytes(16);
var mnemonicCounter = new aes.Counter(mnemonicIv);
var mnemonicAesCtr = new aes.ModeOfOperation.ctr(mnemonicKey, mnemonicCounter);
var mnemonicCiphertext = utils.arrayify(mnemonicAesCtr.encrypt(entropy));
var now = new Date();
var timestamp = (now.getUTCFullYear() + '-' +
zpad(now.getUTCMonth() + 1, 2) + '-' +
zpad(now.getUTCDate(), 2) + 'T' +
zpad(now.getUTCHours(), 2) + '-' +
zpad(now.getUTCMinutes(), 2) + '-' +
zpad(now.getUTCSeconds(), 2) + '.0Z'
);
data['x-ethers'] = {
client: client,
gethFilename: ('UTC--' + timestamp + '--' + data.address),
mnemonicCounter: utils.hexlify(mnemonicIv).substring(2),
mnemonicCiphertext: utils.hexlify(mnemonicCiphertext).substring(2),
version: "0.1"
};
}
if (progressCallback) { progressCallback(1); }
resolve(JSON.stringify(data));

View File

@ -331,6 +331,17 @@ utils.defineProperty(Wallet.prototype, 'encrypt', function(password, options, pr
if (!options) { options = {}; }
if (this.mnemonic) {
// Make sure we don't accidentally bubble the mnemonic up the call-stack
var safeOptions = {};
for (var key in options) { safeOptions[key] = options[key]; }
options = safeOptions;
// Set the mnemonic and path
options.mnemonic = this.mnemonic;
options.path = this.path
}
return secretStorage.encrypt(this.privateKey, password, options, progressCallback);
});
@ -349,6 +360,7 @@ utils.defineProperty(Wallet, 'createRandom', function(options) {
if (options.extraEntropy) {
entropy = utils.keccak256(utils.concat([entropy, options.extraEntropy])).substring(0, 34);
}
var mnemonic = HDNode.entropyToMnemonic(entropy);
return Wallet.fromMnemonic(mnemonic, options.path);
});
@ -372,7 +384,12 @@ utils.defineProperty(Wallet, 'fromEncryptedWallet', function(json, password, pro
} else if (secretStorage.isValidWallet(json)) {
secretStorage.decrypt(json, password, progressCallback).then(function(signingKey) {
resolve(new Wallet(signingKey));
var wallet = new Wallet(signingKey);
if (signingKey.mnemonic && signingKey.path) {
utils.defineProperty(wallet, 'mnemonic', signingKey.mnemonic);
utils.defineProperty(wallet, 'path', signingKey.path);
}
resolve(wallet);
}, function(error) {
reject(error);
});