Browse Source

Merge pull request #955 from BlueWallet/fix-bip38

FIX: BIP38 import support
receivehooks
Overtorment 5 years ago
committed by GitHub
parent
commit
85b4534266
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 0
      blue_modules/bip38/.npmignore
  2. 0
      blue_modules/bip38/CHANGELOG.md
  3. 0
      blue_modules/bip38/LICENSE
  4. 0
      blue_modules/bip38/README.md
  5. 29
      blue_modules/bip38/index.js
  6. 0
      blue_modules/bip38/package.json
  7. 0
      blue_modules/bip38/scryptsy/.npmignore
  8. 0
      blue_modules/bip38/scryptsy/CHANGELOG.md
  9. 0
      blue_modules/bip38/scryptsy/README.md
  10. 0
      blue_modules/bip38/scryptsy/lib/scrypt.js
  11. 0
      blue_modules/bip38/scryptsy/package.json
  12. 20
      class/walletImport.js
  13. 4
      package-lock.json
  14. 2
      package.json
  15. 9
      screen/selftest.js
  16. 21
      tests/unit/Bip38.test.js

0
bip38/.npmignore → blue_modules/bip38/.npmignore

0
bip38/CHANGELOG.md → blue_modules/bip38/CHANGELOG.md

0
bip38/LICENSE → blue_modules/bip38/LICENSE

0
bip38/README.md → blue_modules/bip38/README.md

29
bip38/index.js → blue_modules/bip38/index.js

@ -1,3 +1,4 @@
const BlueCrypto = require('react-native-blue-crypto');
var aes = require('browserify-aes') var aes = require('browserify-aes')
var assert = require('assert') var assert = require('assert')
var Buffer = require('safe-buffer').Buffer var Buffer = require('safe-buffer').Buffer
@ -41,6 +42,18 @@ function getAddress (d, compressed) {
return bs58check.encode(payload) 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) { async function encryptRaw (buffer, compressed, passphrase, progressCallback, scryptParams) {
if (buffer.length !== 32) throw new Error('Invalid private key length') if (buffer.length !== 32) throw new Error('Invalid private key length')
scryptParams = scryptParams || SCRYPT_PARAMS scryptParams = scryptParams || SCRYPT_PARAMS
@ -54,7 +67,7 @@ async function encryptRaw (buffer, compressed, passphrase, progressCallback, scr
var r = scryptParams.r var r = scryptParams.r
var p = scryptParams.p 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 derivedHalf1 = scryptBuf.slice(0, 32)
var derivedHalf2 = scryptBuf.slice(32, 64) var derivedHalf2 = scryptBuf.slice(32, 64)
@ -103,7 +116,7 @@ async function decryptRaw (buffer, passphrase, progressCallback, scryptParams) {
var p = scryptParams.p var p = scryptParams.p
var salt = buffer.slice(3, 7) 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 derivedHalf1 = scryptBuf.slice(0, 32)
var derivedHalf2 = scryptBuf.slice(32, 64) var derivedHalf2 = scryptBuf.slice(32, 64)
@ -133,6 +146,7 @@ async function decrypt (string, passphrase, progressCallback, scryptParams) {
async function decryptECMult (buffer, passphrase, progressCallback, scryptParams) { async function decryptECMult (buffer, passphrase, progressCallback, scryptParams) {
passphrase = Buffer.from(passphrase, 'utf8') passphrase = Buffer.from(passphrase, 'utf8')
const bufferOrig = buffer;
buffer = buffer.slice(1) // FIXME: we can avoid this buffer = buffer.slice(1) // FIXME: we can avoid this
scryptParams = scryptParams || SCRYPT_PARAMS scryptParams = scryptParams || SCRYPT_PARAMS
@ -161,7 +175,7 @@ async function decryptECMult (buffer, passphrase, progressCallback, scryptParams
var N = scryptParams.N var N = scryptParams.N
var r = scryptParams.r var r = scryptParams.r
var p = scryptParams.p 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 var passFactor
if (hasLotSeq) { if (hasLotSeq) {
@ -174,7 +188,7 @@ async function decryptECMult (buffer, passphrase, progressCallback, scryptParams
var passInt = BigInteger.fromBuffer(passFactor) var passInt = BigInteger.fromBuffer(passFactor)
var passPoint = curve.G.multiply(passInt).getEncoded(true) 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 derivedHalf1 = seedBPass.slice(0, 32)
var derivedHalf2 = seedBPass.slice(32, 64) var derivedHalf2 = seedBPass.slice(32, 64)
@ -199,6 +213,13 @@ async function decryptECMult (buffer, passphrase, progressCallback, scryptParams
// d = passFactor * factorB (mod n) // d = passFactor * factorB (mod n)
var d = passInt.multiply(factorB).mod(curve.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 { return {
privateKey: d.toBuffer(32), privateKey: d.toBuffer(32),
compressed: compressed compressed: compressed

0
bip38/package.json → blue_modules/bip38/package.json

0
bip38/scryptsy/.npmignore → blue_modules/bip38/scryptsy/.npmignore

0
bip38/scryptsy/CHANGELOG.md → blue_modules/bip38/scryptsy/CHANGELOG.md

0
bip38/scryptsy/README.md → blue_modules/bip38/scryptsy/README.md

0
bip38/scryptsy/lib/scrypt.js → blue_modules/bip38/scryptsy/lib/scrypt.js

0
bip38/scryptsy/package.json → blue_modules/bip38/scryptsy/package.json

20
class/walletImport.js

@ -17,6 +17,9 @@ const A = require('../analytics');
/** @type {AppStorage} */ /** @type {AppStorage} */
const BlueApp = require('../BlueApp'); const BlueApp = require('../BlueApp');
const loc = require('../loc'); const loc = require('../loc');
const bip38 = require('../blue_modules/bip38');
const wif = require('wif');
const prompt = require('../prompt');
export default class WalletImport { export default class WalletImport {
/** /**
@ -88,6 +91,8 @@ export default class WalletImport {
} }
const placeholderWallet = WalletImport.addPlaceholderWallet(importText); const placeholderWallet = WalletImport.addPlaceholderWallet(importText);
// Plan: // Plan:
// -2. check if BIP38 encrypted
// -1. check lightning custodian
// 0. check if its HDSegwitBech32Wallet (BIP84) // 0. check if its HDSegwitBech32Wallet (BIP84)
// 1. check if its HDSegwitP2SHWallet (BIP49) // 1. check if its HDSegwitP2SHWallet (BIP49)
// 2. check if its HDLegacyP2PKHWallet (BIP44) // 2. check if its HDLegacyP2PKHWallet (BIP44)
@ -99,6 +104,21 @@ export default class WalletImport {
// 7. check if its private key (legacy address) TODO // 7. check if its private key (legacy address) TODO
try { 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? // is it lightning custodian?
if (importText.indexOf('blitzhub://') !== -1 || importText.indexOf('lndhub://') !== -1) { if (importText.indexOf('blitzhub://') !== -1 || importText.indexOf('lndhub://') !== -1) {
let lnd = new LightningCustodianWallet(); let lnd = new LightningCustodianWallet();

4
package-lock.json

@ -11561,6 +11561,10 @@
"version": "git+https://github.com/BlueWallet/react-native-biometrics.git#570dedf776413b76bc414996f6a509135313a06f", "version": "git+https://github.com/BlueWallet/react-native-biometrics.git#570dedf776413b76bc414996f6a509135313a06f",
"from": "git+https://github.com/BlueWallet/react-native-biometrics.git#2.0.0" "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": { "react-native-camera": {
"version": "3.21.0", "version": "3.21.0",
"resolved": "https://registry.npmjs.org/react-native-camera/-/react-native-camera-3.21.0.tgz", "resolved": "https://registry.npmjs.org/react-native-camera/-/react-native-camera-3.21.0.tgz",

2
package.json

@ -36,6 +36,7 @@
"podinstall": "./podinstall.sh", "podinstall": "./podinstall.sh",
"start": "node node_modules/react-native/local-cli/cli.js start", "start": "node node_modules/react-native/local-cli/cli.js start",
"android": "react-native run-android", "android": "react-native run-android",
"android:clean": "cd android; ./gradlew clean ; cd .. ; npm run android",
"ios": "react-native run-ios", "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", "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", "test": "npm run unit && npm run jest && npm run lint",
@ -96,6 +97,7 @@
"react-localization": "1.0.15", "react-localization": "1.0.15",
"react-native": "0.61.5", "react-native": "0.61.5",
"react-native-biometrics": "git+https://github.com/BlueWallet/react-native-biometrics.git#2.0.0", "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-camera": "3.21.0",
"react-native-default-preference": "1.4.1", "react-native-default-preference": "1.4.1",
"react-native-device-info": "4.0.1", "react-native-device-info": "4.0.1",

9
screen/selftest.js

@ -4,6 +4,7 @@ import { BlueLoading, BlueSpacing20, SafeBlueArea, BlueCard, BlueText, BlueNavig
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SegwitP2SHWallet, LegacyWallet, HDSegwitP2SHWallet, HDSegwitBech32Wallet } from '../class'; import { SegwitP2SHWallet, LegacyWallet, HDSegwitP2SHWallet, HDSegwitBech32Wallet } from '../class';
const bitcoin = require('bitcoinjs-lib'); const bitcoin = require('bitcoinjs-lib');
const BlueCrypto = require('react-native-blue-crypto');
let BigNumber = require('bignumber.js'); let BigNumber = require('bignumber.js');
let encryption = require('../encryption'); let encryption = require('../encryption');
let BlueElectrum = require('../BlueElectrum'); 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) { } catch (Err) {
errorMessage += Err; errorMessage += Err;
isOk = false; isOk = false;

21
tests/unit/Bip38.test.js

@ -2,7 +2,7 @@
let assert = require('assert'); let assert = require('assert');
it('bip38 decodes', async () => { it('bip38 decodes', async () => {
const bip38 = require('../../bip38'); const bip38 = require('../../blue_modules/bip38');
const wif = require('wif'); const wif = require('wif');
let encryptedKey = '6PRVWUbkzq2VVjRuv58jpwVjTeN46MeNmzUHqUjQptBJUHGcBakduhrUNc'; let encryptedKey = '6PRVWUbkzq2VVjRuv58jpwVjTeN46MeNmzUHqUjQptBJUHGcBakduhrUNc';
@ -25,14 +25,29 @@ it('bip38 decodes slow', async () => {
return; return;
} }
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const bip38 = require('../../bip38'); const bip38 = require('../../blue_modules/bip38');
const wif = require('wif'); const wif = require('wif');
let encryptedKey = '6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN'; 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( assert.strictEqual(
wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed), wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed),
'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc', 'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc',
); );
let wasError = false;
try {
await bip38.decrypt(encryptedKey, 'a');
} catch (_) {
wasError = true;
}
assert.ok(wasError);
}); });

Loading…
Cancel
Save