diff --git a/bip38/.npmignore b/blue_modules/bip38/.npmignore similarity index 100% rename from bip38/.npmignore rename to blue_modules/bip38/.npmignore diff --git a/bip38/CHANGELOG.md b/blue_modules/bip38/CHANGELOG.md similarity index 100% rename from bip38/CHANGELOG.md rename to blue_modules/bip38/CHANGELOG.md diff --git a/bip38/LICENSE b/blue_modules/bip38/LICENSE similarity index 100% rename from bip38/LICENSE rename to blue_modules/bip38/LICENSE diff --git a/bip38/README.md b/blue_modules/bip38/README.md similarity index 100% rename from bip38/README.md rename to blue_modules/bip38/README.md diff --git a/bip38/index.js b/blue_modules/bip38/index.js similarity index 85% rename from bip38/index.js rename to blue_modules/bip38/index.js index 2253d5db..7f8cf62f 100644 --- a/bip38/index.js +++ b/blue_modules/bip38/index.js @@ -1,3 +1,4 @@ +const BlueCrypto = require('react-native-blue-crypto'); var aes = require('browserify-aes') var assert = require('assert') var Buffer = require('safe-buffer').Buffer @@ -41,6 +42,18 @@ function getAddress (d, compressed) { return bs58check.encode(payload) } +async function scryptWrapper(secret, salt, N, r, p, dkLen, progressCallback) { + if (BlueCrypto.isAvailable()) { + secret = Buffer.from(secret).toString('hex'); + salt = Buffer.from(salt).toString('hex'); + const hex = await BlueCrypto.scrypt(secret, salt, N, r, p, dkLen); + return Buffer.from(hex, 'hex'); + } else { + // fallback to js implementation + return scrypt(secret, salt, N, r, p, dkLen, progressCallback); + } +} + async function encryptRaw (buffer, compressed, passphrase, progressCallback, scryptParams) { if (buffer.length !== 32) throw new Error('Invalid private key length') scryptParams = scryptParams || SCRYPT_PARAMS @@ -54,7 +67,7 @@ async function encryptRaw (buffer, compressed, passphrase, progressCallback, scr var r = scryptParams.r var p = scryptParams.p - var scryptBuf = await scrypt(secret, salt, N, r, p, 64, progressCallback) + var scryptBuf = await scryptWrapper(secret, salt, N, r, p, 64, progressCallback) var derivedHalf1 = scryptBuf.slice(0, 32) var derivedHalf2 = scryptBuf.slice(32, 64) @@ -103,7 +116,7 @@ async function decryptRaw (buffer, passphrase, progressCallback, scryptParams) { var p = scryptParams.p var salt = buffer.slice(3, 7) - var scryptBuf = await scrypt(passphrase, salt, N, r, p, 64, progressCallback) + var scryptBuf = await scryptWrapper(passphrase, salt, N, r, p, 64, progressCallback) var derivedHalf1 = scryptBuf.slice(0, 32) var derivedHalf2 = scryptBuf.slice(32, 64) @@ -133,6 +146,7 @@ async function decrypt (string, passphrase, progressCallback, scryptParams) { async function decryptECMult (buffer, passphrase, progressCallback, scryptParams) { passphrase = Buffer.from(passphrase, 'utf8') + const bufferOrig = buffer; buffer = buffer.slice(1) // FIXME: we can avoid this scryptParams = scryptParams || SCRYPT_PARAMS @@ -161,7 +175,7 @@ async function decryptECMult (buffer, passphrase, progressCallback, scryptParams var N = scryptParams.N var r = scryptParams.r var p = scryptParams.p - var preFactor = await scrypt(passphrase, ownerSalt, N, r, p, 32, progressCallback) + var preFactor = await scryptWrapper(passphrase, ownerSalt, N, r, p, 32, progressCallback) var passFactor if (hasLotSeq) { @@ -174,7 +188,7 @@ async function decryptECMult (buffer, passphrase, progressCallback, scryptParams var passInt = BigInteger.fromBuffer(passFactor) var passPoint = curve.G.multiply(passInt).getEncoded(true) - var seedBPass = await scrypt(passPoint, Buffer.concat([addressHash, ownerEntropy]), 1024, 1, 1, 64) + var seedBPass = await scryptWrapper(passPoint, Buffer.concat([addressHash, ownerEntropy]), 1024, 1, 1, 64) var derivedHalf1 = seedBPass.slice(0, 32) var derivedHalf2 = seedBPass.slice(32, 64) @@ -199,6 +213,13 @@ async function decryptECMult (buffer, passphrase, progressCallback, scryptParams // d = passFactor * factorB (mod n) var d = passInt.multiply(factorB).mod(curve.n) + // added by overtorment: see https://github.com/bitcoinjs/bip38/issues/60 + // verify salt matches address + var address = getAddress(d, compressed) + var checksum = hash256(address).slice(0, 4) + var salt = bufferOrig.slice(3, 7) + assert.deepEqual(salt, checksum) + return { privateKey: d.toBuffer(32), compressed: compressed diff --git a/bip38/package.json b/blue_modules/bip38/package.json similarity index 100% rename from bip38/package.json rename to blue_modules/bip38/package.json diff --git a/bip38/scryptsy/.npmignore b/blue_modules/bip38/scryptsy/.npmignore similarity index 100% rename from bip38/scryptsy/.npmignore rename to blue_modules/bip38/scryptsy/.npmignore diff --git a/bip38/scryptsy/CHANGELOG.md b/blue_modules/bip38/scryptsy/CHANGELOG.md similarity index 100% rename from bip38/scryptsy/CHANGELOG.md rename to blue_modules/bip38/scryptsy/CHANGELOG.md diff --git a/bip38/scryptsy/README.md b/blue_modules/bip38/scryptsy/README.md similarity index 100% rename from bip38/scryptsy/README.md rename to blue_modules/bip38/scryptsy/README.md diff --git a/bip38/scryptsy/lib/scrypt.js b/blue_modules/bip38/scryptsy/lib/scrypt.js similarity index 100% rename from bip38/scryptsy/lib/scrypt.js rename to blue_modules/bip38/scryptsy/lib/scrypt.js diff --git a/bip38/scryptsy/package.json b/blue_modules/bip38/scryptsy/package.json similarity index 100% rename from bip38/scryptsy/package.json rename to blue_modules/bip38/scryptsy/package.json diff --git a/class/walletImport.js b/class/walletImport.js index 302f005c..f079b9ac 100644 --- a/class/walletImport.js +++ b/class/walletImport.js @@ -17,6 +17,9 @@ const A = require('../analytics'); /** @type {AppStorage} */ const BlueApp = require('../BlueApp'); const loc = require('../loc'); +const bip38 = require('../blue_modules/bip38'); +const wif = require('wif'); +const prompt = require('../prompt'); export default class WalletImport { /** @@ -88,6 +91,8 @@ export default class WalletImport { } const placeholderWallet = WalletImport.addPlaceholderWallet(importText); // Plan: + // -2. check if BIP38 encrypted + // -1. check lightning custodian // 0. check if its HDSegwitBech32Wallet (BIP84) // 1. check if its HDSegwitP2SHWallet (BIP49) // 2. check if its HDLegacyP2PKHWallet (BIP44) @@ -99,6 +104,21 @@ export default class WalletImport { // 7. check if its private key (legacy address) TODO try { + if (importText.startsWith('6P')) { + let password = false; + do { + password = await prompt('This looks like password-protected private key (BIP38)', 'Enter password to decrypt', false); + } while (!password); + + let decryptedKey = await bip38.decrypt(importText, password, status => { + console.warn(status.percent + '%'); + }); + + if (decryptedKey) { + importText = wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed); + } + } + // is it lightning custodian? if (importText.indexOf('blitzhub://') !== -1 || importText.indexOf('lndhub://') !== -1) { let lnd = new LightningCustodianWallet(); diff --git a/package-lock.json b/package-lock.json index 50fc9136..7e18b504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11561,6 +11561,10 @@ "version": "git+https://github.com/BlueWallet/react-native-biometrics.git#570dedf776413b76bc414996f6a509135313a06f", "from": "git+https://github.com/BlueWallet/react-native-biometrics.git#2.0.0" }, + "react-native-blue-crypto": { + "version": "git+https://github.com/Overtorment/react-native-blue-crypto.git#d2100110c57016cfda2440c33af3a46ccd7d9c7a", + "from": "git+https://github.com/Overtorment/react-native-blue-crypto.git" + }, "react-native-camera": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/react-native-camera/-/react-native-camera-3.21.0.tgz", diff --git a/package.json b/package.json index 78f27a11..a359d320 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "podinstall": "./podinstall.sh", "start": "node node_modules/react-native/local-cli/cli.js start", "android": "react-native run-android", + "android:clean": "cd android; ./gradlew clean ; cd .. ; npm run android", "ios": "react-native run-ios", "postinstall": "./node_modules/.bin/rn-nodeify --install buffer,events,process,stream,util,inherits,fs,path --hack; npm run releasenotes2json; npm run podinstall; npx jetify", "test": "npm run unit && npm run jest && npm run lint", @@ -96,6 +97,7 @@ "react-localization": "1.0.15", "react-native": "0.61.5", "react-native-biometrics": "git+https://github.com/BlueWallet/react-native-biometrics.git#2.0.0", + "react-native-blue-crypto": "git+https://github.com/Overtorment/react-native-blue-crypto.git", "react-native-camera": "3.21.0", "react-native-default-preference": "1.4.1", "react-native-device-info": "4.0.1", diff --git a/screen/selftest.js b/screen/selftest.js index 47a3b395..bf6b28a2 100644 --- a/screen/selftest.js +++ b/screen/selftest.js @@ -4,6 +4,7 @@ import { BlueLoading, BlueSpacing20, SafeBlueArea, BlueCard, BlueText, BlueNavig import PropTypes from 'prop-types'; import { SegwitP2SHWallet, LegacyWallet, HDSegwitP2SHWallet, HDSegwitBech32Wallet } from '../class'; const bitcoin = require('bitcoinjs-lib'); +const BlueCrypto = require('react-native-blue-crypto'); let BigNumber = require('bignumber.js'); let encryption = require('../encryption'); let BlueElectrum = require('../BlueElectrum'); @@ -254,6 +255,14 @@ export default class Selftest extends Component { } // + + if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') { + const hex = await BlueCrypto.scrypt('717765727479', '4749345a22b23cf3', 64, 8, 8, 32); // using non-default parameters to speed it up (not-bip38 compliant) + if (hex.toUpperCase() !== 'F36AB2DC12377C788D61E6770126D8A01028C8F6D8FE01871CE0489A1F696A90') + throw new Error('react-native-blue-crypto is not ok'); + } + + // } catch (Err) { errorMessage += Err; isOk = false; diff --git a/tests/unit/Bip38.test.js b/tests/unit/Bip38.test.js index 7abb5aaa..6c886812 100644 --- a/tests/unit/Bip38.test.js +++ b/tests/unit/Bip38.test.js @@ -2,7 +2,7 @@ let assert = require('assert'); it('bip38 decodes', async () => { - const bip38 = require('../../bip38'); + const bip38 = require('../../blue_modules/bip38'); const wif = require('wif'); let encryptedKey = '6PRVWUbkzq2VVjRuv58jpwVjTeN46MeNmzUHqUjQptBJUHGcBakduhrUNc'; @@ -25,14 +25,29 @@ it('bip38 decodes slow', async () => { return; } jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; - const bip38 = require('../../bip38'); + const bip38 = require('../../blue_modules/bip38'); const wif = require('wif'); let encryptedKey = '6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN'; - let decryptedKey = await bip38.decrypt(encryptedKey, 'qwerty', status => process.stdout.write(parseInt(status.percent) + '%\r')); + let callbackWasCalled = false; + let decryptedKey = await bip38.decrypt(encryptedKey, 'qwerty', () => { + // callbacks make sense only with pure js scrypt implementation (nodejs and browsers). + // on RN scrypt is handled by native module and takes ~4 secs + callbackWasCalled = true; + }); + assert.ok(callbackWasCalled); assert.strictEqual( wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed), 'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc', ); + + let wasError = false; + try { + await bip38.decrypt(encryptedKey, 'a'); + } catch (_) { + wasError = true; + } + + assert.ok(wasError); });