From bcefbee1f6cb0b5bbeab94d9738ff9ff9edd2472 Mon Sep 17 00:00:00 2001 From: sophietech Date: Thu, 14 Jun 2018 13:30:24 +0800 Subject: [PATCH 1/7] Update package.json add react-native-scrypt --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8ce73d22eb..b041976b16 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "inherits": "2.0.1", "js-sha3": "0.5.7", "scrypt-js": "2.0.3", + "react-native-scrypt": "1.0.0", "setimmediate": "1.0.4", "uuid": "2.0.1", "xmlhttprequest": "1.8.0" From 79609fc782c269b66fd3ac149cddf3653992de8e Mon Sep 17 00:00:00 2001 From: sophietech Date: Thu, 14 Jun 2018 13:41:49 +0800 Subject: [PATCH 2/7] add RNencrypt for react-native --- wallet/secret-storage.js | 153 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/wallet/secret-storage.js b/wallet/secret-storage.js index 27188de061..8b8ae3c194 100644 --- a/wallet/secret-storage.js +++ b/wallet/secret-storage.js @@ -2,6 +2,7 @@ var aes = require('aes-js'); var scrypt = require('scrypt-js'); +import RNscrypt from 'react-native-scrypt'; var uuid = require('uuid'); var hmac = require('../utils/hmac'); @@ -395,7 +396,7 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op address: address.substring(2).toLowerCase(), id: uuid.v4({ random: uuidRandom }), version: 3, - Crypto: { + crypto: { cipher: 'aes-128-ctr', cipherparams: { iv: utils.hexlify(iv).substring(2), @@ -446,4 +447,154 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op }); }); +utils.defineProperty(secretStorage, 'RNencrypt', function(privateKey, password, options, progressCallback) { + + // the options are optional, so adjust the call as needed + if (typeof(options) === 'function' && !progressCallback) { + progressCallback = options; + options = {}; + } + if (!options) { options = {}; } + + // Check the private key + if (privateKey instanceof SigningKey) { + privateKey = privateKey.privateKey; + } + privateKey = arrayify(privateKey, 'private key'); + if (privateKey.length !== 32) { throw new Error('invalid private key'); } + + 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) { + salt = arrayify(salt, 'salt'); + } else { + salt = utils.randomBytes(32);; + } + + // Override initialization vector + var iv = null; + 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 = 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 + var N = (1 << 17), r = 8, p = 1; + if (options.scrypt) { + if (options.scrypt.N) { N = options.scrypt.N; } + if (options.scrypt.r) { r = options.scrypt.r; } + if (options.scrypt.p) { p = options.scrypt.p; } + } + + return new Promise(function(resolve, reject) { + + // We take 64 bytes: + // - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix) + // - 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; + reject(error); + + } else if (key) { + + + } else if (progressCallback) { + return progressCallback(progress); + } + }); + + var int_array_salt = Object.values(salt); + const result = RNscrypt(password, int_array_salt, N, r, p, 64); + result.then(key => { + if (key) { + key = arrayify(key); + + // 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); + + // 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; + + // Encrypt the private key + var counter = new aes.Counter(iv); + var aesCtr = new aes.ModeOfOperation.ctr(derivedKey, counter); + var ciphertext = utils.arrayify(aesCtr.encrypt(privateKey)); + + // Compute the message authentication code, used to check the password + var mac = utils.keccak256(utils.concat([macPrefix, ciphertext])) + + // See: https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition + var data = { + address: address.substring(2).toLowerCase(), + id: uuid.v4({ random: uuidRandom }), + version: 3, + crypto: { + cipher: 'aes-128-ctr', + cipherparams: { + iv: utils.hexlify(iv).substring(2), + }, + ciphertext: utils.hexlify(ciphertext).substring(2), + kdf: 'scrypt', + kdfparams: { + salt: utils.hexlify(salt).substring(2), + n: N, + dklen: 32, + p: p, + r: r + }, + mac: mac.substring(2) + } + }; + if (progressCallback) { progressCallback(1); } + resolve(JSON.stringify(data)); + + } else if (progressCallback) { + return progressCallback(); + } + }); + result.catch (error => { + reject(error); + }); + + }); +}); + module.exports = secretStorage; From 9eefd400313182a07cc827689c81ab280a96352b Mon Sep 17 00:00:00 2001 From: sophietech Date: Thu, 14 Jun 2018 13:53:09 +0800 Subject: [PATCH 3/7] add RNdecrypt and lowwercase 'Crypto' to 'crypto' --- wallet/secret-storage.js | 165 +++++++++++++++++++++++++++++++++++---- 1 file changed, 151 insertions(+), 14 deletions(-) diff --git a/wallet/secret-storage.js b/wallet/secret-storage.js index 8b8ae3c194..641349532e 100644 --- a/wallet/secret-storage.js +++ b/wallet/secret-storage.js @@ -447,6 +447,156 @@ utils.defineProperty(secretStorage, 'encrypt', function(privateKey, password, op }); }); +utils.defineProperty(secretStorage, 'RNdecrypt', function(json, password, progressCallback) { + var data = JSON.parse(json); + + var decrypt = function(key, ciphertext) { + var cipher = searchPath(data, 'crypto/cipher'); + if (cipher === 'aes-128-ctr') { + var iv = arrayify(searchPath(data, 'crypto/cipherparams/iv'), 'crypto/cipherparams/iv') + var counter = new aes.Counter(iv); + + var aesCtr = new aes.ModeOfOperation.ctr(key, counter); + + return arrayify(aesCtr.decrypt(ciphertext)); + } + + return null; + }; + + var computeMAC = function(derivedHalf, ciphertext) { + return utils.keccak256(utils.concat([derivedHalf, ciphertext])); + } + + var getSigningKey = function(key, reject) { + var ciphertext = arrayify(searchPath(data, 'crypto/ciphertext')); + + var computedMAC = utils.hexlify(computeMAC(key.slice(16, 32), ciphertext)).substring(2); + if (computedMAC !== searchPath(data, 'crypto/mac').toLowerCase()) { + reject(new Error('invalid password')); + return null; + } + + var privateKey = decrypt(key.slice(0, 16), ciphertext); + var mnemonicKey = key.slice(32, 64); + + if (!privateKey) { + reject(new Error('unsupported cipher')); + return null; + } + + var signingKey = new SigningKey(privateKey); + if (signingKey.address !== utils.getAddress(data.address)) { + reject(new Error('address mismatch')); + 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 new Promise(function(resolve, reject) { + var kdf = searchPath(data, 'crypto/kdf'); + if (kdf && typeof(kdf) === 'string') { + if (kdf.toLowerCase() === 'scrypt') { + var salt = arrayify(searchPath(data, 'crypto/kdfparams/salt'), 'crypto/kdfparams/salt'); + var N = parseInt(searchPath(data, 'crypto/kdfparams/n')); + var r = parseInt(searchPath(data, 'crypto/kdfparams/r')); + var p = parseInt(searchPath(data, 'crypto/kdfparams/p')); + if (!N || !r || !p) { + reject(new Error('unsupported key-derivation function parameters')); + return; + } + + // Make sure N is a power of 2 + if ((N & (N - 1)) !== 0) { + reject(new Error('unsupported key-derivation function parameter value for N')); + return; + } + + var dkLen = parseInt(searchPath(data, 'crypto/kdfparams/dklen')); + if (dkLen !== 32) { + reject( new Error('unsupported key-derivation derived-key length')); + return; + } + + var RNsalt = Object.values(salt); + const result = RNscrypt(password, RNsalt, N, r, p, 64); + result.then(key => { + key = arrayify(key); + + var signingKey = getSigningKey(key, reject); + if (!signingKey) { return; } + + if (progressCallback) { progressCallback(1); } + resolve(signingKey); + }); + result.catch(error => { + reject(error); + }); + + + } else if (kdf.toLowerCase() === 'pbkdf2') { + var salt = arrayify(searchPath(data, 'crypto/kdfparams/salt'), 'crypto/kdfparams/salt'); + + var prfFunc = null; + var prf = searchPath(data, 'crypto/kdfparams/prf'); + if (prf === 'hmac-sha256') { + prfFunc = hmac.createSha256Hmac; + } else if (prf === 'hmac-sha512') { + prfFunc = hmac.createSha512Hmac; + } else { + reject(new Error('unsupported prf')); + return; + } + + var c = parseInt(searchPath(data, 'crypto/kdfparams/c')); + + var dkLen = parseInt(searchPath(data, 'crypto/kdfparams/dklen')); + if (dkLen !== 32) { + reject( new Error('unsupported key-derivation derived-key length')); + return; + } + + var key = pbkdf2(getPassword(password), salt, c, dkLen, prfFunc); + + var signingKey = getSigningKey(key, reject); + if (!signingKey) { return; } + + resolve(signingKey); + + } else { + reject(new Error('unsupported key-derivation function')); + } + + } else { + reject(new Error('unsupported key-derivation function')); + } + }); +}); + utils.defineProperty(secretStorage, 'RNencrypt', function(privateKey, password, options, progressCallback) { // the options are optional, so adjust the call as needed @@ -523,20 +673,7 @@ utils.defineProperty(secretStorage, 'RNencrypt', function(privateKey, password, // We take 64 bytes: // - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix) - // - 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; - reject(error); - - } else if (key) { - - - } else if (progressCallback) { - return progressCallback(progress); - } - }); - + // - 32 bytes AES key to encrypt mnemonic with (required here to be Ethers Wallet) var int_array_salt = Object.values(salt); const result = RNscrypt(password, int_array_salt, N, r, p, 64); result.then(key => { From cd9529448375cb1516cbc7b2e331171658178419 Mon Sep 17 00:00:00 2001 From: sophietech Date: Thu, 14 Jun 2018 14:06:44 +0800 Subject: [PATCH 4/7] add RNfromEncyptedWallet for react-native --- wallet/wallet.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/wallet/wallet.js b/wallet/wallet.js index babb1c3f46..a0307d8e91 100644 --- a/wallet/wallet.js +++ b/wallet/wallet.js @@ -426,6 +426,40 @@ utils.defineProperty(Wallet, 'fromEncryptedWallet', function(json, password, pro }); }); +utils.defineProperty(Wallet, 'RNfromEncryptedWallet', function(json, password, progressCallback) { + if (progressCallback && typeof(progressCallback) !== 'function') { + throw new Error('invalid callback'); + } + + return new Promise(function(resolve, reject) { + + if (secretStorage.isCrowdsaleWallet(json)) { + try { + var privateKey = secretStorage.decryptCrowdsale(json, password); + resolve(new Wallet(privateKey)); + } catch (error) { + reject(error); + } + + } else if (secretStorage.isValidWallet(json)) { + + secretStorage.RNdecrypt(json, password, progressCallback).then(function(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); + }); + + } else { + reject('invalid wallet JSON'); + } + }); +}); + utils.defineProperty(Wallet, 'fromMnemonic', function(mnemonic, path) { if (!path) { path = defaultPath; } From f2678542cd7002e9e2566f354cec7d236671b962 Mon Sep 17 00:00:00 2001 From: sophietech Date: Thu, 14 Jun 2018 14:24:53 +0800 Subject: [PATCH 5/7] add RNencrypt to call secretStorage.RNencrypt --- wallet/wallet.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/wallet/wallet.js b/wallet/wallet.js index a0307d8e91..1603238ed1 100644 --- a/wallet/wallet.js +++ b/wallet/wallet.js @@ -371,6 +371,32 @@ utils.defineProperty(Wallet.prototype, 'encrypt', function(password, options, pr return secretStorage.encrypt(this.privateKey, password, options, progressCallback); }); +utils.defineProperty(Wallet.prototype, 'RNencrypt', function(password, options, progressCallback) { + if (typeof(options) === 'function' && !progressCallback) { + progressCallback = options; + options = {}; + } + + if (progressCallback && typeof(progressCallback) !== 'function') { + throw new Error('invalid callback'); + } + + 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.RNencrypt(this.privateKey, password, options, progressCallback); +}); + utils.defineProperty(Wallet, 'isEncryptedWallet', function(json) { return (secretStorage.isValidWallet(json) || secretStorage.isCrowdsaleWallet(json)); From fdd5225fe40ae6709fd3416913d88e8368f32957 Mon Sep 17 00:00:00 2001 From: sophietech Date: Thu, 14 Jun 2018 15:27:39 +0800 Subject: [PATCH 6/7] convert salt to int array --- wallet/secret-storage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wallet/secret-storage.js b/wallet/secret-storage.js index 641349532e..5b6a876213 100644 --- a/wallet/secret-storage.js +++ b/wallet/secret-storage.js @@ -542,7 +542,7 @@ utils.defineProperty(secretStorage, 'RNdecrypt', function(json, password, progre return; } - var RNsalt = Object.values(salt); + var RNsalt = Object.values(salt).filter(v => Number.isInteger(v)); const result = RNscrypt(password, RNsalt, N, r, p, 64); result.then(key => { key = arrayify(key); @@ -674,8 +674,8 @@ utils.defineProperty(secretStorage, 'RNencrypt', function(privateKey, password, // We take 64 bytes: // - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix) // - 32 bytes AES key to encrypt mnemonic with (required here to be Ethers Wallet) - var int_array_salt = Object.values(salt); - const result = RNscrypt(password, int_array_salt, N, r, p, 64); + var RNsalt = Object.values(salt).filter(v => Number.isInteger(v)); + const result = RNscrypt(password, RNsalt, N, r, p, 64); result.then(key => { if (key) { key = arrayify(key); From 27a8c6269645ecbfdf5b596e0fcbf48ec4215504 Mon Sep 17 00:00:00 2001 From: sophietech Date: Fri, 15 Jun 2018 14:57:10 +0800 Subject: [PATCH 7/7] link react-native-scrypt --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 73be3a6e94..c38382b59c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ To use in [node.js](https://nodejs.org/): ``` /Users/ethers/my-app> npm install --save ethers +/Users/ethers/my-app> react-native link react-native-scrypt ```