From c4be1ac0da1202bf5299cbafbee8cc6d113128bc Mon Sep 17 00:00:00 2001 From: Overtorment Date: Tue, 7 Apr 2020 15:25:56 +0100 Subject: [PATCH 1/3] FIX: BIP38 import support --- {bip38 => blue_modules/bip38}/.npmignore | 0 {bip38 => blue_modules/bip38}/CHANGELOG.md | 0 {bip38 => blue_modules/bip38}/LICENSE | 0 {bip38 => blue_modules/bip38}/README.md | 0 {bip38 => blue_modules/bip38}/index.js | 21 +++++++++++++++---- {bip38 => blue_modules/bip38}/package.json | 0 .../bip38}/scryptsy/.npmignore | 0 .../bip38}/scryptsy/CHANGELOG.md | 0 .../bip38}/scryptsy/README.md | 0 .../bip38}/scryptsy/lib/scrypt.js | 0 .../bip38}/scryptsy/package.json | 0 class/walletImport.js | 20 ++++++++++++++++++ package-lock.json | 4 ++++ package.json | 2 ++ screen/selftest.js | 9 ++++++++ tests/unit/Bip38.test.js | 10 ++++++--- 16 files changed, 59 insertions(+), 7 deletions(-) rename {bip38 => blue_modules/bip38}/.npmignore (100%) rename {bip38 => blue_modules/bip38}/CHANGELOG.md (100%) rename {bip38 => blue_modules/bip38}/LICENSE (100%) rename {bip38 => blue_modules/bip38}/README.md (100%) rename {bip38 => blue_modules/bip38}/index.js (88%) rename {bip38 => blue_modules/bip38}/package.json (100%) rename {bip38 => blue_modules/bip38}/scryptsy/.npmignore (100%) rename {bip38 => blue_modules/bip38}/scryptsy/CHANGELOG.md (100%) rename {bip38 => blue_modules/bip38}/scryptsy/README.md (100%) rename {bip38 => blue_modules/bip38}/scryptsy/lib/scrypt.js (100%) rename {bip38 => blue_modules/bip38}/scryptsy/package.json (100%) 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 88% rename from bip38/index.js rename to blue_modules/bip38/index.js index 2253d5db..a1b65dd8 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) @@ -161,7 +174,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 +187,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) 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 378aae77..d61206bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11448,6 +11448,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#30a0a054c634033cb3d0247a0a18a95fd616e45e", + "from": "git+https://github.com/Overtorment/react-native-blue-crypto.git" + }, "react-native-camera": { "version": "3.17.0", "resolved": "https://registry.npmjs.org/react-native-camera/-/react-native-camera-3.17.0.tgz", diff --git a/package.json b/package.json index bcf1ce11..afbc06c3 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", "react-native-camera": "3.17.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..375dd587 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 !== '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..bf87eb7d 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,11 +25,15 @@ 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', () => { + callbackWasCalled = true; + }); + assert.ok(callbackWasCalled); assert.strictEqual( wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed), From 463bf9feb4f5ccdc72453c7b4b2e7f26577e8923 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Fri, 10 Apr 2020 12:15:33 +0100 Subject: [PATCH 2/3] FIX: BIP38 should reject invalid passwords --- blue_modules/bip38/index.js | 8 ++++++++ package-lock.json | 2 +- tests/unit/Bip38.test.js | 11 +++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/blue_modules/bip38/index.js b/blue_modules/bip38/index.js index a1b65dd8..7f8cf62f 100644 --- a/blue_modules/bip38/index.js +++ b/blue_modules/bip38/index.js @@ -146,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 @@ -212,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/package-lock.json b/package-lock.json index 4fafd3b1..585b80ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11562,7 +11562,7 @@ "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#30a0a054c634033cb3d0247a0a18a95fd616e45e", + "version": "git+https://github.com/Overtorment/react-native-blue-crypto.git#21d4d914a0170adb79e009f36539b90bd25f7669", "from": "git+https://github.com/Overtorment/react-native-blue-crypto.git" }, "react-native-camera": { diff --git a/tests/unit/Bip38.test.js b/tests/unit/Bip38.test.js index bf87eb7d..6c886812 100644 --- a/tests/unit/Bip38.test.js +++ b/tests/unit/Bip38.test.js @@ -31,6 +31,8 @@ it('bip38 decodes slow', async () => { let encryptedKey = '6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN'; 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); @@ -39,4 +41,13 @@ it('bip38 decodes slow', async () => { wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed), 'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc', ); + + let wasError = false; + try { + await bip38.decrypt(encryptedKey, 'a'); + } catch (_) { + wasError = true; + } + + assert.ok(wasError); }); From a6c2cdaf4a45772b09a9365493609ad26fb4ad9f Mon Sep 17 00:00:00 2001 From: Overtorment Date: Fri, 10 Apr 2020 17:06:37 +0100 Subject: [PATCH 3/3] FIX: bip38 fixes --- package-lock.json | 2 +- package.json | 2 +- screen/selftest.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 585b80ad..48fd9256 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11562,7 +11562,7 @@ "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#21d4d914a0170adb79e009f36539b90bd25f7669", + "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": { diff --git a/package.json b/package.json index 90698e94..d6b064cf 100644 --- a/package.json +++ b/package.json @@ -97,7 +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", + "react-native-blue-crypto": "git+https://github.com/Overtorment/react-native-blue-crypto.git", "react-native-camera": "3.19.1", "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 375dd587..bf6b28a2 100644 --- a/screen/selftest.js +++ b/screen/selftest.js @@ -258,7 +258,7 @@ 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 !== 'F36AB2DC12377C788D61E6770126D8A01028C8F6D8FE01871CE0489A1F696A90') + if (hex.toUpperCase() !== 'F36AB2DC12377C788D61E6770126D8A01028C8F6D8FE01871CE0489A1F696A90') throw new Error('react-native-blue-crypto is not ok'); }