Added x-ethers compatible encrypted mnemonic to JSON wallets.
This commit is contained in:
parent
bb24fce859
commit
4ee9a4d191
@ -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)],
|
||||
|
@ -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"}}
|
@ -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"}
|
@ -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.
@ -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",
|
||||
|
@ -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;
|
||||
@ -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));
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user