From 5db3540e8910eed2feebc93d79813459878a3036 Mon Sep 17 00:00:00 2001 From: Ian Coleman Date: Thu, 12 Sep 2019 14:42:40 +1000 Subject: [PATCH] Add bitcoinjs-bip38 to libs directory --- libs/bitcoinjs-bip38/index.js | 244 ++++++++++++++++++++++++++++++ libs/bitcoinjs-bip38/package.json | 38 +++++ libs/bitcoinjs-bip38/readme.md | 4 + 3 files changed, 286 insertions(+) create mode 100644 libs/bitcoinjs-bip38/index.js create mode 100644 libs/bitcoinjs-bip38/package.json create mode 100644 libs/bitcoinjs-bip38/readme.md diff --git a/libs/bitcoinjs-bip38/index.js b/libs/bitcoinjs-bip38/index.js new file mode 100644 index 0000000..51c20e4 --- /dev/null +++ b/libs/bitcoinjs-bip38/index.js @@ -0,0 +1,244 @@ +var aes = require('browserify-aes') +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var bs58check = require('bs58check') +var createHash = require('create-hash') +var scrypt = require('scryptsy') +var xor = require('buffer-xor/inplace') + +var ecurve = require('ecurve') +var curve = ecurve.getCurveByName('secp256k1') + +var BigInteger = require('bigi') + +// constants +var SCRYPT_PARAMS = { + N: 16384, // specified by BIP38 + r: 8, + p: 8 +} +var NULL = Buffer.alloc(0) + +function hash160 (buffer) { + var hash + try { + hash = createHash('rmd160') + } catch (e) { + hash = createHash('ripemd160') + } + return hash.update( + createHash('sha256').update(buffer).digest() + ).digest() +} + +function hash256 (buffer) { + return createHash('sha256').update( + createHash('sha256').update(buffer).digest() + ).digest() +} + +function getAddress (d, compressed) { + var Q = curve.G.multiply(d).getEncoded(compressed) + var hash = hash160(Q) + var payload = Buffer.allocUnsafe(21) + payload.writeUInt8(0x00, 0) // XXX TODO FIXME bitcoin only??? damn you BIP38 + hash.copy(payload, 1) + + return bs58check.encode(payload) +} + +function encryptRaw (buffer, compressed, passphrase, progressCallback, scryptParams) { + if (buffer.length !== 32) throw new Error('Invalid private key length') + scryptParams = scryptParams || SCRYPT_PARAMS + + var d = BigInteger.fromBuffer(buffer) + var address = getAddress(d, compressed) + var secret = Buffer.from(passphrase, 'utf8') + var salt = hash256(address).slice(0, 4) + + var N = scryptParams.N + var r = scryptParams.r + var p = scryptParams.p + + var scryptBuf = scrypt(secret, salt, N, r, p, 64, progressCallback) + var derivedHalf1 = scryptBuf.slice(0, 32) + var derivedHalf2 = scryptBuf.slice(32, 64) + + var xorBuf = xor(derivedHalf1, buffer) + var cipher = aes.createCipheriv('aes-256-ecb', derivedHalf2, NULL) + cipher.setAutoPadding(false) + cipher.end(xorBuf) + + var cipherText = cipher.read() + + // 0x01 | 0x42 | flagByte | salt (4) | cipherText (32) + var result = Buffer.allocUnsafe(7 + 32) + result.writeUInt8(0x01, 0) + result.writeUInt8(0x42, 1) + result.writeUInt8(compressed ? 0xe0 : 0xc0, 2) + salt.copy(result, 3) + cipherText.copy(result, 7) + + return result +} + +function encrypt (buffer, compressed, passphrase, progressCallback, scryptParams) { + return bs58check.encode(encryptRaw(buffer, compressed, passphrase, progressCallback, scryptParams)) +} + +// some of the techniques borrowed from: https://github.com/pointbiz/bitaddress.org +function decryptRaw (buffer, passphrase, progressCallback, scryptParams) { + // 39 bytes: 2 bytes prefix, 37 bytes payload + if (buffer.length !== 39) throw new Error('Invalid BIP38 data length') + if (buffer.readUInt8(0) !== 0x01) throw new Error('Invalid BIP38 prefix') + scryptParams = scryptParams || SCRYPT_PARAMS + + // check if BIP38 EC multiply + var type = buffer.readUInt8(1) + if (type === 0x43) return decryptECMult(buffer, passphrase, progressCallback, scryptParams) + if (type !== 0x42) throw new Error('Invalid BIP38 type') + + passphrase = Buffer.from(passphrase, 'utf8') + + var flagByte = buffer.readUInt8(2) + var compressed = flagByte === 0xe0 + if (!compressed && flagByte !== 0xc0) throw new Error('Invalid BIP38 compression flag') + + var N = scryptParams.N + var r = scryptParams.r + var p = scryptParams.p + + var salt = buffer.slice(3, 7) + var scryptBuf = scrypt(passphrase, salt, N, r, p, 64, progressCallback) + var derivedHalf1 = scryptBuf.slice(0, 32) + var derivedHalf2 = scryptBuf.slice(32, 64) + + var privKeyBuf = buffer.slice(7, 7 + 32) + var decipher = aes.createDecipheriv('aes-256-ecb', derivedHalf2, NULL) + decipher.setAutoPadding(false) + decipher.end(privKeyBuf) + + var plainText = decipher.read() + var privateKey = xor(derivedHalf1, plainText) + + // verify salt matches address + var d = BigInteger.fromBuffer(privateKey) + var address = getAddress(d, compressed) + var checksum = hash256(address).slice(0, 4) + assert.deepStrictEqual(salt, checksum) + + return { + privateKey: privateKey, + compressed: compressed + } +} + +function decrypt (string, passphrase, progressCallback, scryptParams) { + return decryptRaw(bs58check.decode(string), passphrase, progressCallback, scryptParams) +} + +function decryptECMult (buffer, passphrase, progressCallback, scryptParams) { + passphrase = Buffer.from(passphrase, 'utf8') + buffer = buffer.slice(1) // FIXME: we can avoid this + scryptParams = scryptParams || SCRYPT_PARAMS + + var flag = buffer.readUInt8(1) + var compressed = (flag & 0x20) !== 0 + var hasLotSeq = (flag & 0x04) !== 0 + + assert.strictEqual((flag & 0x24), flag, 'Invalid private key.') + + var addressHash = buffer.slice(2, 6) + var ownerEntropy = buffer.slice(6, 14) + var ownerSalt + + // 4 bytes ownerSalt if 4 bytes lot/sequence + if (hasLotSeq) { + ownerSalt = ownerEntropy.slice(0, 4) + + // else, 8 bytes ownerSalt + } else { + ownerSalt = ownerEntropy + } + + var encryptedPart1 = buffer.slice(14, 22) // First 8 bytes + var encryptedPart2 = buffer.slice(22, 38) // 16 bytes + + var N = scryptParams.N + var r = scryptParams.r + var p = scryptParams.p + var preFactor = scrypt(passphrase, ownerSalt, N, r, p, 32, progressCallback) + + var passFactor + if (hasLotSeq) { + var hashTarget = Buffer.concat([preFactor, ownerEntropy]) + passFactor = hash256(hashTarget) + } else { + passFactor = preFactor + } + + var passInt = BigInteger.fromBuffer(passFactor) + var passPoint = curve.G.multiply(passInt).getEncoded(true) + + var seedBPass = scrypt(passPoint, Buffer.concat([addressHash, ownerEntropy]), 1024, 1, 1, 64) + var derivedHalf1 = seedBPass.slice(0, 32) + var derivedHalf2 = seedBPass.slice(32, 64) + + var decipher = aes.createDecipheriv('aes-256-ecb', derivedHalf2, Buffer.alloc(0)) + decipher.setAutoPadding(false) + decipher.end(encryptedPart2) + + var decryptedPart2 = decipher.read() + var tmp = xor(decryptedPart2, derivedHalf1.slice(16, 32)) + var seedBPart2 = tmp.slice(8, 16) + + var decipher2 = aes.createDecipheriv('aes-256-ecb', derivedHalf2, Buffer.alloc(0)) + decipher2.setAutoPadding(false) + decipher2.write(encryptedPart1) // first 8 bytes + decipher2.end(tmp.slice(0, 8)) // last 8 bytes + + var seedBPart1 = xor(decipher2.read(), derivedHalf1.slice(0, 16)) + var seedB = Buffer.concat([seedBPart1, seedBPart2], 24) + var factorB = BigInteger.fromBuffer(hash256(seedB)) + + // d = passFactor * factorB (mod n) + var d = passInt.multiply(factorB).mod(curve.n) + + return { + privateKey: d.toBuffer(32), + compressed: compressed + } +} + +function verify (string) { + var decoded = bs58check.decodeUnsafe(string) + if (!decoded) return false + + if (decoded.length !== 39) return false + if (decoded.readUInt8(0) !== 0x01) return false + + var type = decoded.readUInt8(1) + var flag = decoded.readUInt8(2) + + // encrypted WIF + if (type === 0x42) { + if (flag !== 0xc0 && flag !== 0xe0) return false + + // EC mult + } else if (type === 0x43) { + if ((flag & ~0x24)) return false + } else { + return false + } + + return true +} + +module.exports = { + decrypt: decrypt, + decryptECMult: decryptECMult, + decryptRaw: decryptRaw, + encrypt: encrypt, + encryptRaw: encryptRaw, + verify: verify +} diff --git a/libs/bitcoinjs-bip38/package.json b/libs/bitcoinjs-bip38/package.json new file mode 100644 index 0000000..480a5fa --- /dev/null +++ b/libs/bitcoinjs-bip38/package.json @@ -0,0 +1,38 @@ +{ + "name": "bip38", + "version": "2.0.2", + "description": "BIP38 is a standard process to encrypt Bitcoin and crypto currency private keys that is impervious to brute force attacks thus protecting the user.", + "main": "index.js", + "keywords": [ + "bitcoin", + "crypto", + "cryptography", + "litecoin" + ], + "homepage": "http://cryptocoinjs.com/modules/currency/bip38/", + "author": "JP Richardson", + "dependencies": { + "bigi": "^1.2.0", + "browserify-aes": "^1.0.1", + "bs58check": "<3.0.0", + "buffer-xor": "^1.0.2", + "create-hash": "^1.1.1", + "ecurve": "^1.0.0", + "scryptsy": "^2.0.0" + }, + "devDependencies": { + }, + "repository": { + "url": "git@github.com:bitcoinjs/bip38.git", + "type": "git" + }, + "scripts": { + "browser-test": "mochify --wd -R spec --timeout 100000", + "build": "browserify index.js --standalone bitcoinjs-bip38 > /tmp/bitcoinjs-bip38.js", + "coverage": "istanbul cover _mocha -- --reporter list test/*.js", + "coveralls": "npm run-script coverage && coveralls < coverage/lcov.info", + "standard": "standard", + "test": "npm run standard && npm run unit", + "unit": "mocha --ui bdd --timeout 240000" + } +} diff --git a/libs/bitcoinjs-bip38/readme.md b/libs/bitcoinjs-bip38/readme.md new file mode 100644 index 0000000..a97a316 --- /dev/null +++ b/libs/bitcoinjs-bip38/readme.md @@ -0,0 +1,4 @@ +Build (will create a bundle and copy it to /tmp/bitcoinjs-bip38.js): + + npm install + npm run build -- 2.41.0