mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-20 18:21:57 +01:00
240 lines
7.0 KiB
JavaScript
240 lines
7.0 KiB
JavaScript
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) {
|
|
return createHash('rmd160').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)
|
|
}
|
|
|
|
async 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 = await 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
|
|
async 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 await 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 = await 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.deepEqual(salt, checksum)
|
|
|
|
return {
|
|
privateKey: privateKey,
|
|
compressed: compressed
|
|
}
|
|
}
|
|
|
|
async function decrypt (string, passphrase, progressCallback, scryptParams) {
|
|
return await decryptRaw(bs58check.decode(string), passphrase, progressCallback, scryptParams)
|
|
}
|
|
|
|
async 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.equal((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 = await 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 = await 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
|
|
}
|