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',
|
'0x4a9cf99357f5789251a8d7fad5b86d0f31eeb938': '0xa016182717223d01f776149ec0b4a217d0e9930cad263f205427c6d3cd5560e7',
|
||||||
'0x88a5c2d9919e46f883eb62f7b8dd9d0cc45bc290': '0xf03e581353c794928373fb0893bc731aefc4c4e234e643f3a46998b03cd4d7c5',
|
'0x88a5c2d9919e46f883eb62f7b8dd9d0cc45bc290': '0xf03e581353c794928373fb0893bc731aefc4c4e234e643f3a46998b03cd4d7c5',
|
||||||
'0x17c5185167401ed00cf5f5b2fc97d9bbfdb7d025': '0x4242424242424242424242424242424242424242424242424242424242424242',
|
'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');
|
var walletPath = path.join(__dirname, 'test-wallets');
|
||||||
fs.readdirSync(walletPath).forEach(function(filename) {
|
fs.readdirSync(walletPath).forEach(function(filename) {
|
||||||
var data = require(path.join(walletPath, 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('-');
|
var password = filename.substring(0, filename.length - 5).split('-');
|
||||||
password = password[password.length - 1];
|
password = password[password.length - 1];
|
||||||
|
|
||||||
if (password === 'life') { password = 'foo'; }
|
|
||||||
|
|
||||||
if (data.ethaddr) {
|
if (data.ethaddr) {
|
||||||
Output.push({
|
Output.push({
|
||||||
type: 'crowdsale',
|
type: 'crowdsale',
|
||||||
@ -61,6 +66,7 @@ fs.readdirSync(walletPath).forEach(function(filename) {
|
|||||||
type: 'secret-storage',
|
type: 'secret-storage',
|
||||||
address: prefixAddress(data.address),
|
address: prefixAddress(data.address),
|
||||||
json: JSON.stringify(data),
|
json: JSON.stringify(data),
|
||||||
|
mnemonic: mnemonics[prefixAddress(data.address)] || '',
|
||||||
name: name,
|
name: name,
|
||||||
password: password,
|
password: password,
|
||||||
privateKey: privateKeys[prefixAddress(data.address)],
|
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);
|
'generated correct private key - ' + wallet.privateKey);
|
||||||
assert.equal(wallet.address.toLowerCase(), test.address,
|
assert.equal(wallet.address.toLowerCase(), test.address,
|
||||||
'generate correct address - ' + wallet.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",
|
"name": "ethers-wallet",
|
||||||
"version": "2.1.7",
|
"version": "2.1.8",
|
||||||
"description": "Wallet and signing library for Ethereum.",
|
"description": "Wallet and signing library for Ethereum.",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "http://github.com/ethers-io/ethers.js/issues",
|
"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 pbkdf2 = require('ethers-utils/pbkdf2');
|
||||||
var utils = require('ethers-utils');
|
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) {
|
function arrayify(hexString) {
|
||||||
if (typeof(hexString) === 'string' && hexString.substring(0, 2) !== '0x') {
|
if (typeof(hexString) === 'string' && hexString.substring(0, 2) !== '0x') {
|
||||||
@ -17,6 +21,12 @@ function arrayify(hexString) {
|
|||||||
return utils.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) {
|
function getPassword(password) {
|
||||||
if (typeof(password) === 'string') {
|
if (typeof(password) === 'string') {
|
||||||
return utils.toUtf8Bytes(password, 'NFKC');
|
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);
|
var aesCtr = new aes.ModeOfOperation.ctr(key, counter);
|
||||||
|
|
||||||
return new arrayify(aesCtr.decrypt(ciphertext));
|
return arrayify(aesCtr.decrypt(ciphertext));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -153,6 +163,7 @@ utils.defineProperty(secretStorage, 'decrypt', function(json, password, progress
|
|||||||
}
|
}
|
||||||
|
|
||||||
var privateKey = decrypt(key.slice(0, 16), ciphertext);
|
var privateKey = decrypt(key.slice(0, 16), ciphertext);
|
||||||
|
var mnemonicKey = key.slice(32, 64);
|
||||||
|
|
||||||
if (!privateKey) {
|
if (!privateKey) {
|
||||||
reject(new Error('unsupported cipher'));
|
reject(new Error('unsupported cipher'));
|
||||||
@ -165,6 +176,28 @@ utils.defineProperty(secretStorage, 'decrypt', function(json, password, progress
|
|||||||
return null;
|
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;
|
return signingKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +227,7 @@ utils.defineProperty(secretStorage, 'decrypt', function(json, password, progress
|
|||||||
return;
|
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) {
|
if (error) {
|
||||||
error.progress = progress;
|
error.progress = progress;
|
||||||
reject(error);
|
reject(error);
|
||||||
@ -265,11 +298,33 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op
|
|||||||
if (privateKey instanceof SigningKey) {
|
if (privateKey instanceof SigningKey) {
|
||||||
privateKey = privateKey.privateKey;
|
privateKey = privateKey.privateKey;
|
||||||
}
|
}
|
||||||
privateKey = utils.arrayify(privateKey, 'private key');
|
privateKey = arrayify(privateKey, 'private key');
|
||||||
if (privateKey.length !== 32) { throw new Error('invalid private key'); }
|
if (privateKey.length !== 32) { throw new Error('invalid private key'); }
|
||||||
|
|
||||||
password = getPassword(password);
|
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
|
// Check/generate the salt
|
||||||
var salt = options.salt;
|
var salt = options.salt;
|
||||||
if (salt) {
|
if (salt) {
|
||||||
@ -283,13 +338,17 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op
|
|||||||
if (options.iv) {
|
if (options.iv) {
|
||||||
iv = arrayify(options.iv, 'iv');
|
iv = arrayify(options.iv, 'iv');
|
||||||
if (iv.length !== 16) { throw new Error('invalid iv'); }
|
if (iv.length !== 16) { throw new Error('invalid iv'); }
|
||||||
|
} else {
|
||||||
|
iv = utils.randomBytes(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override the uuid
|
// Override the uuid
|
||||||
var uuidRandom = options.uuid;
|
var uuidRandom = options.uuid;
|
||||||
if (uuidRandom) {
|
if (uuidRandom) {
|
||||||
uuidRandom = utils.arrayify(uuidRandom, 'uuid');
|
uuidRandom = arrayify(uuidRandom, 'uuid');
|
||||||
if (uuidRandom.length !== 16) { throw new Error('invalid uuid'); }
|
if (uuidRandom.length !== 16) { throw new Error('invalid uuid'); }
|
||||||
|
} else {
|
||||||
|
uuidRandom = utils.randomBytes(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override the scrypt password-based key derivation function parameters
|
// 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:
|
// We take 64 bytes:
|
||||||
// - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix)
|
// - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix)
|
||||||
// - 16 bytes The initialization vector
|
// - 32 bytes AES key to encrypt mnemonic with (required here to be Ethers Wallet)
|
||||||
// - 16 bytes The UUID random bytes
|
|
||||||
scrypt(password, salt, N, r, p, 64, function(error, progress, key) {
|
scrypt(password, salt, N, r, p, 64, function(error, progress, key) {
|
||||||
if (error) {
|
if (error) {
|
||||||
error.progress = progress;
|
error.progress = progress;
|
||||||
@ -314,15 +372,12 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op
|
|||||||
} else if (key) {
|
} else if (key) {
|
||||||
key = arrayify(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 derivedKey = key.slice(0, 16);
|
||||||
var macPrefix = key.slice(16, 32);
|
var macPrefix = key.slice(16, 32);
|
||||||
|
|
||||||
// Get the initialization vector
|
// This will be used to encrypt the mnemonic phrase (if any)
|
||||||
if (!iv) { iv = key.slice(32, 48); }
|
var mnemonicKey = key.slice(32, 64);
|
||||||
|
|
||||||
// Get the UUID random data
|
|
||||||
if (!uuidRandom) { uuidRandom = key.slice(48, 64); }
|
|
||||||
|
|
||||||
// Get the address for this private key
|
// Get the address for this private key
|
||||||
var address = (new SigningKey(privateKey)).address;
|
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
|
// See: https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
|
||||||
var data = {
|
var data = {
|
||||||
address: address.substring(2).toLowerCase(),
|
address: address.substring(2).toLowerCase(),
|
||||||
id: uuid.v4({random: uuidRandom}),
|
id: uuid.v4({ random: uuidRandom }),
|
||||||
version: 3,
|
version: 3,
|
||||||
Crypto: {
|
Crypto: {
|
||||||
cipher: 'aes-128-ctr',
|
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); }
|
if (progressCallback) { progressCallback(1); }
|
||||||
resolve(JSON.stringify(data));
|
resolve(JSON.stringify(data));
|
||||||
|
|
||||||
|
@ -331,6 +331,17 @@ utils.defineProperty(Wallet.prototype, 'encrypt', function(password, options, pr
|
|||||||
|
|
||||||
if (!options) { options = {}; }
|
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);
|
return secretStorage.encrypt(this.privateKey, password, options, progressCallback);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -349,6 +360,7 @@ utils.defineProperty(Wallet, 'createRandom', function(options) {
|
|||||||
if (options.extraEntropy) {
|
if (options.extraEntropy) {
|
||||||
entropy = utils.keccak256(utils.concat([entropy, options.extraEntropy])).substring(0, 34);
|
entropy = utils.keccak256(utils.concat([entropy, options.extraEntropy])).substring(0, 34);
|
||||||
}
|
}
|
||||||
|
|
||||||
var mnemonic = HDNode.entropyToMnemonic(entropy);
|
var mnemonic = HDNode.entropyToMnemonic(entropy);
|
||||||
return Wallet.fromMnemonic(mnemonic, options.path);
|
return Wallet.fromMnemonic(mnemonic, options.path);
|
||||||
});
|
});
|
||||||
@ -372,7 +384,12 @@ utils.defineProperty(Wallet, 'fromEncryptedWallet', function(json, password, pro
|
|||||||
} else if (secretStorage.isValidWallet(json)) {
|
} else if (secretStorage.isValidWallet(json)) {
|
||||||
|
|
||||||
secretStorage.decrypt(json, password, progressCallback).then(function(signingKey) {
|
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) {
|
}, function(error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user