├── .gitignore ├── .min-wd ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test ├── fixtures.json ├── index.js └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | coverage 3 | .nyc_output 4 | node_modules 5 | *.rdb 6 | *.log 7 | -------------------------------------------------------------------------------- /.min-wd: -------------------------------------------------------------------------------- 1 | { 2 | "hostname" : "localhost", 3 | "port" : 4444, 4 | "browsers" : [{ 5 | "name" : "chrome" 6 | }] 7 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | .min-wd 3 | .gitignore 4 | .travis.yml 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "lts/*" 5 | env: 6 | - TEST_SUITE=ci:test 7 | script: npm run-script $TEST_SUITE 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 3.1.1 / 2020-05-31 2 | ------------------ 3 | - Add safe-buffer as explicit dependency 4 | 5 | 3.1.0 / 2020-04-09 6 | ------------------ 7 | - Add async methods 8 | 9 | 3.0.0 / 2019-09-12 10 | ------------------ 11 | - Fixed backwards incompatible bug with passphrase NFC normalization 12 | 13 | 2.0.3 / 2019-05-22 14 | ------------------ 15 | - made compatible for Electron v4 16 | 17 | 2.0.2 / 2017-12-14 18 | ------------------ 19 | - use safe-buffer 20 | - upgrade scryptsy 21 | 22 | 2.0.1 / 2017-04-20 23 | ------------------ 24 | - upgrade bs58check 25 | - index: rm unused address parameter 26 | 27 | 2.0.0 / 2016-12-20 28 | ------------------ 29 | - removed class instantiation. Removed `coinstring` dep. 30 | 31 | 1.4.0 / 2015-11-03 32 | ------------------ 33 | - added `progressCallback`. See: https://github.com/bitcoinjs/bip38/pull/16 34 | 35 | 1.3.0 / 2015-06-04 36 | ------------------ 37 | - use `createHash` and `aes` directly. https://github.com/cryptocoinjs/bip38 38 | - JavaScript Standard Style 39 | 40 | 1.2.0 / 2015-01-05 41 | ------------------ 42 | - removed dependency upon `aes` package since Browserify now supports aes [Daniel Cousens](https://github.com/cryptocoinjs/bip38/pull/6) 43 | - removed `crypto-browserify` devDep and removed `browser` field from `package.json`; no longer necessary 44 | - added method `verify()` [Daniel Cousens](https://github.com/cryptocoinjs/bip38/pull/7) 45 | 46 | 1.1.1 / 2014-09-19 47 | ------------------ 48 | - bugfix: enforce zero padding [Daniel Cousens](https://github.com/cryptocoinjs/bip38/commit/e73598d0fc1d1b3c04c132c34053e96bec6bd201) 49 | - add MIT license to package.json 50 | 51 | 1.1.0 / 2014-07-11 52 | ------------------ 53 | - added methods `encryptRaw` and `decryptRaw` [Daniel Cousens](https://github.com/cryptocoinjs/bip38/pull/4) 54 | 55 | 1.0.0 / 2014-06-10 56 | ------------------ 57 | - upgraded `"scryptsy": "~0.2.0"` to `"scryptsy": "^1.0.0"` 58 | - upgraded `"bigi": "~0.2.0"` to `"bigi": "^1.2.0"` 59 | - removed `ecurve-names` dep 60 | - upgraded `"ecurve": "~0.3.0"` to `"ecurve": "^0.8.0"` 61 | - removed semicolons per http://cryptocoinjs.com/about/contributing/#semicolons 62 | - removed `crypto-hashing` dep 63 | - removed `bs58` dep, added `coinstring` dep 64 | - removed `terst` for `assert` 65 | - added TravisCI 66 | - added Coveralls 67 | - added testling 68 | - removed static level methods, must call `new` 69 | 70 | 0.1.0 / 2014-03-05 71 | ------------------ 72 | - added support to decrypt ECMultiplied keys, #1 73 | - made constructor work without `new` 74 | - upgraded deps `ecurve`, `ecurve-names`, and `scryptsy` 75 | 76 | 0.0.1 / 2014-02-28 77 | ------------------ 78 | - initial release 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2014 Cryptocoinjs contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bip38 2 | 3 | [![build status](https://secure.travis-ci.org/bitcoinjs/bip38.svg)](http://travis-ci.org/bitcoinjs/bip38) 4 | [![Coverage Status](https://img.shields.io/coveralls/cryptocoinjs/bip38.svg)](https://coveralls.io/r/cryptocoinjs/bip38) 5 | [![Version](http://img.shields.io/npm/v/bip38.svg)](https://www.npmjs.org/package/bip38) 6 | 7 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 8 | 9 | A JavaScript component that adheres to the [BIP38](https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki) standard to secure your crypto currency private keys. Fully compliant with Node.js and the browser (via Browserify). 10 | 11 | 12 | ## Why? 13 | BIP38 is a standard process to encrypt Bitcoin and crypto currency private keys that is impervious to brute force attacks thus protecting the user. 14 | 15 | 16 | ## Package Info 17 | - homepage: [http://cryptocoinjs.com/modules/currency/bip38/](http://cryptocoinjs.com/modules/currency/bip38/) 18 | - github: [https://github.com/cryptocoinjs/bip38](https://github.com/cryptocoinjs/bip38) 19 | - tests: [https://github.com/cryptocoinjs/bip38/tree/master/test](https://github.com/cryptocoinjs/bip38/tree/master/test) 20 | - issues: [https://github.com/cryptocoinjs/bip38/issues](https://github.com/cryptocoinjs/bip38/issues) 21 | - license: **MIT** 22 | - versioning: [http://semver-ftw.org](http://semver-ftw.org) 23 | 24 | 25 | ## Usage 26 | 27 | ### Installation 28 | 29 | npm install --save bip38 30 | 31 | 32 | ### Async methods 33 | 34 | Async methods are available, but using them will be slower, but free up the event loop in intervals you choose. 35 | 36 | For benchmark results, please see the section in the README of the [scryptsy](https://github.com/cryptocoinjs/scryptsy/tree/395c3b09b21e06ea4a6cc2933e046c0984a414c5#benchmarks) library. Increasing the interval will decrease the performance hit, but increase the span between event loop free ups (UI drawing etc.) promiseInterval is the last optional parameter after scryptParams. There is no recommendation currently, as the performance trade off is app specific. 37 | 38 | ### API 39 | ### encrypt(buffer, compressed, passphrase[, progressCallback, scryptParams]) 40 | 41 | ``` javascript 42 | var bip38 = require('bip38') 43 | var wif = require('wif') 44 | 45 | var myWifString = '5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR' 46 | var decoded = wif.decode(myWifString) 47 | 48 | var encryptedKey = bip38.encrypt(decoded.privateKey, decoded.compressed, 'TestingOneTwoThree') 49 | console.log(encryptedKey) 50 | // => '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg' 51 | ``` 52 | 53 | 54 | ### decrypt(encryptedKey, passphrase[, progressCallback, scryptParams]) 55 | 56 | ``` javascript 57 | var bip38 = require('bip38') 58 | var wif = require('wif') 59 | 60 | var encryptedKey = '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg' 61 | var decryptedKey = bip38.decrypt(encryptedKey, 'TestingOneTwoThree', function (status) { 62 | console.log(status.percent) // will print the percent every time current increases by 1000 63 | }) 64 | 65 | console.log(wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed)) 66 | // => '5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR' 67 | ``` 68 | 69 | 70 | # References 71 | - https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki 72 | - https://github.com/pointbiz/bitaddress.org/issues/56 (Safari 6.05 issue) 73 | - https://github.com/casascius/Bitcoin-Address-Utility/tree/master/Model 74 | - https://github.com/nomorecoin/python-bip38-testing/blob/master/bip38.py 75 | - https://github.com/pointbiz/bitaddress.org/blob/master/src/ninja.key.js 76 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var aes = require('browserify-aes') 2 | var assert = require('assert') 3 | var Buffer = require('safe-buffer').Buffer 4 | var bs58check = require('bs58check') 5 | var createHash = require('create-hash') 6 | var scrypt = require('scryptsy') 7 | var xor = require('buffer-xor/inplace') 8 | 9 | var ecurve = require('ecurve') 10 | var curve = ecurve.getCurveByName('secp256k1') 11 | 12 | var BigInteger = require('bigi') 13 | 14 | // constants 15 | var SCRYPT_PARAMS = { 16 | N: 16384, // specified by BIP38 17 | r: 8, 18 | p: 8 19 | } 20 | var NULL = Buffer.alloc(0) 21 | 22 | function hash160 (buffer) { 23 | var hash 24 | try { 25 | hash = createHash('rmd160') 26 | } catch (e) { 27 | hash = createHash('ripemd160') 28 | } 29 | return hash.update( 30 | createHash('sha256').update(buffer).digest() 31 | ).digest() 32 | } 33 | 34 | function hash256 (buffer) { 35 | return createHash('sha256').update( 36 | createHash('sha256').update(buffer).digest() 37 | ).digest() 38 | } 39 | 40 | function getAddress (d, compressed) { 41 | var Q = curve.G.multiply(d).getEncoded(compressed) 42 | var hash = hash160(Q) 43 | var payload = Buffer.allocUnsafe(21) 44 | payload.writeUInt8(0x00, 0) // XXX TODO FIXME bitcoin only??? damn you BIP38 45 | hash.copy(payload, 1) 46 | 47 | return bs58check.encode(payload) 48 | } 49 | 50 | function prepareEncryptRaw (buffer, compressed, passphrase, scryptParams) { 51 | if (buffer.length !== 32) throw new Error('Invalid private key length') 52 | 53 | var d = BigInteger.fromBuffer(buffer) 54 | var address = getAddress(d, compressed) 55 | var secret = Buffer.from(passphrase.normalize('NFC'), 'utf8') 56 | var salt = hash256(address).slice(0, 4) 57 | 58 | var N = scryptParams.N 59 | var r = scryptParams.r 60 | var p = scryptParams.p 61 | 62 | return { 63 | secret, 64 | salt, 65 | N, 66 | r, 67 | p 68 | } 69 | } 70 | 71 | function finishEncryptRaw (buffer, compressed, salt, scryptBuf) { 72 | var derivedHalf1 = scryptBuf.slice(0, 32) 73 | var derivedHalf2 = scryptBuf.slice(32, 64) 74 | 75 | var xorBuf = xor(derivedHalf1, buffer) 76 | var cipher = aes.createCipheriv('aes-256-ecb', derivedHalf2, NULL) 77 | cipher.setAutoPadding(false) 78 | cipher.end(xorBuf) 79 | 80 | var cipherText = cipher.read() 81 | 82 | // 0x01 | 0x42 | flagByte | salt (4) | cipherText (32) 83 | var result = Buffer.allocUnsafe(7 + 32) 84 | result.writeUInt8(0x01, 0) 85 | result.writeUInt8(0x42, 1) 86 | result.writeUInt8(compressed ? 0xe0 : 0xc0, 2) 87 | salt.copy(result, 3) 88 | cipherText.copy(result, 7) 89 | 90 | return result 91 | } 92 | 93 | async function encryptRawAsync (buffer, compressed, passphrase, progressCallback, scryptParams, promiseInterval) { 94 | scryptParams = scryptParams || SCRYPT_PARAMS 95 | const { 96 | secret, 97 | salt, 98 | N, 99 | r, 100 | p 101 | } = prepareEncryptRaw(buffer, compressed, passphrase, scryptParams) 102 | 103 | var scryptBuf = await scrypt.async(secret, salt, N, r, p, 64, progressCallback, promiseInterval) 104 | 105 | return finishEncryptRaw(buffer, compressed, salt, scryptBuf) 106 | } 107 | 108 | function encryptRaw (buffer, compressed, passphrase, progressCallback, scryptParams) { 109 | scryptParams = scryptParams || SCRYPT_PARAMS 110 | const { 111 | secret, 112 | salt, 113 | N, 114 | r, 115 | p 116 | } = prepareEncryptRaw(buffer, compressed, passphrase, scryptParams) 117 | 118 | var scryptBuf = scrypt(secret, salt, N, r, p, 64, progressCallback) 119 | 120 | return finishEncryptRaw(buffer, compressed, salt, scryptBuf) 121 | } 122 | 123 | async function encryptAsync (buffer, compressed, passphrase, progressCallback, scryptParams, promiseInterval) { 124 | return bs58check.encode(await encryptRawAsync(buffer, compressed, passphrase, progressCallback, scryptParams, promiseInterval)) 125 | } 126 | 127 | function encrypt (buffer, compressed, passphrase, progressCallback, scryptParams) { 128 | return bs58check.encode(encryptRaw(buffer, compressed, passphrase, progressCallback, scryptParams)) 129 | } 130 | 131 | function prepareDecryptRaw (buffer, progressCallback, scryptParams) { 132 | // 39 bytes: 2 bytes prefix, 37 bytes payload 133 | if (buffer.length !== 39) throw new Error('Invalid BIP38 data length') 134 | if (buffer.readUInt8(0) !== 0x01) throw new Error('Invalid BIP38 prefix') 135 | 136 | // check if BIP38 EC multiply 137 | var type = buffer.readUInt8(1) 138 | if (type === 0x43) return { decryptEC: true } 139 | if (type !== 0x42) throw new Error('Invalid BIP38 type') 140 | 141 | var flagByte = buffer.readUInt8(2) 142 | var compressed = flagByte === 0xe0 143 | if (!compressed && flagByte !== 0xc0) throw new Error('Invalid BIP38 compression flag') 144 | 145 | var N = scryptParams.N 146 | var r = scryptParams.r 147 | var p = scryptParams.p 148 | 149 | var salt = buffer.slice(3, 7) 150 | return { 151 | salt, 152 | compressed, 153 | N, 154 | r, 155 | p 156 | } 157 | } 158 | 159 | function finishDecryptRaw (buffer, salt, compressed, scryptBuf) { 160 | var derivedHalf1 = scryptBuf.slice(0, 32) 161 | var derivedHalf2 = scryptBuf.slice(32, 64) 162 | 163 | var privKeyBuf = buffer.slice(7, 7 + 32) 164 | var decipher = aes.createDecipheriv('aes-256-ecb', derivedHalf2, NULL) 165 | decipher.setAutoPadding(false) 166 | decipher.end(privKeyBuf) 167 | 168 | var plainText = decipher.read() 169 | var privateKey = xor(derivedHalf1, plainText) 170 | 171 | // verify salt matches address 172 | var d = BigInteger.fromBuffer(privateKey) 173 | var address = getAddress(d, compressed) 174 | var checksum = hash256(address).slice(0, 4) 175 | assert.deepStrictEqual(salt, checksum) 176 | 177 | return { 178 | privateKey: privateKey, 179 | compressed: compressed 180 | } 181 | } 182 | 183 | async function decryptRawAsync (buffer, passphrase, progressCallback, scryptParams, promiseInterval) { 184 | scryptParams = scryptParams || SCRYPT_PARAMS 185 | const { 186 | salt, 187 | compressed, 188 | N, 189 | r, 190 | p, 191 | decryptEC 192 | } = prepareDecryptRaw(buffer, progressCallback, scryptParams) 193 | if (decryptEC === true) return decryptECMultAsync(buffer, passphrase, progressCallback, scryptParams, promiseInterval) 194 | 195 | var scryptBuf = await scrypt.async(passphrase.normalize('NFC'), salt, N, r, p, 64, progressCallback, promiseInterval) 196 | return finishDecryptRaw(buffer, salt, compressed, scryptBuf) 197 | } 198 | 199 | // some of the techniques borrowed from: https://github.com/pointbiz/bitaddress.org 200 | function decryptRaw (buffer, passphrase, progressCallback, scryptParams) { 201 | scryptParams = scryptParams || SCRYPT_PARAMS 202 | const { 203 | salt, 204 | compressed, 205 | N, 206 | r, 207 | p, 208 | decryptEC 209 | } = prepareDecryptRaw(buffer, progressCallback, scryptParams) 210 | if (decryptEC === true) return decryptECMult(buffer, passphrase, progressCallback, scryptParams) 211 | var scryptBuf = scrypt(passphrase.normalize('NFC'), salt, N, r, p, 64, progressCallback) 212 | return finishDecryptRaw(buffer, salt, compressed, scryptBuf) 213 | } 214 | 215 | async function decryptAsync (string, passphrase, progressCallback, scryptParams, promiseInterval) { 216 | return decryptRawAsync(bs58check.decode(string), passphrase, progressCallback, scryptParams, promiseInterval) 217 | } 218 | 219 | function decrypt (string, passphrase, progressCallback, scryptParams) { 220 | return decryptRaw(bs58check.decode(string), passphrase, progressCallback, scryptParams) 221 | } 222 | 223 | function prepareDecryptECMult (buffer, passphrase, progressCallback, scryptParams) { 224 | var flag = buffer.readUInt8(1) 225 | var compressed = (flag & 0x20) !== 0 226 | var hasLotSeq = (flag & 0x04) !== 0 227 | 228 | assert.strictEqual((flag & 0x24), flag, 'Invalid private key.') 229 | 230 | var addressHash = buffer.slice(2, 6) 231 | var ownerEntropy = buffer.slice(6, 14) 232 | var ownerSalt 233 | 234 | // 4 bytes ownerSalt if 4 bytes lot/sequence 235 | if (hasLotSeq) { 236 | ownerSalt = ownerEntropy.slice(0, 4) 237 | 238 | // else, 8 bytes ownerSalt 239 | } else { 240 | ownerSalt = ownerEntropy 241 | } 242 | 243 | var encryptedPart1 = buffer.slice(14, 22) // First 8 bytes 244 | var encryptedPart2 = buffer.slice(22, 38) // 16 bytes 245 | 246 | var N = scryptParams.N 247 | var r = scryptParams.r 248 | var p = scryptParams.p 249 | return { 250 | addressHash, 251 | encryptedPart1, 252 | encryptedPart2, 253 | ownerEntropy, 254 | ownerSalt, 255 | hasLotSeq, 256 | compressed, 257 | N, 258 | r, 259 | p 260 | } 261 | } 262 | 263 | function getPassIntAndPoint (preFactor, ownerEntropy, hasLotSeq) { 264 | var passFactor 265 | if (hasLotSeq) { 266 | var hashTarget = Buffer.concat([preFactor, ownerEntropy]) 267 | passFactor = hash256(hashTarget) 268 | } else { 269 | passFactor = preFactor 270 | } 271 | const passInt = BigInteger.fromBuffer(passFactor) 272 | return { 273 | passInt, 274 | passPoint: curve.G.multiply(passInt).getEncoded(true) 275 | } 276 | } 277 | 278 | function finishDecryptECMult (seedBPass, encryptedPart1, encryptedPart2, passInt, compressed) { 279 | var derivedHalf1 = seedBPass.slice(0, 32) 280 | var derivedHalf2 = seedBPass.slice(32, 64) 281 | 282 | var decipher = aes.createDecipheriv('aes-256-ecb', derivedHalf2, Buffer.alloc(0)) 283 | decipher.setAutoPadding(false) 284 | decipher.end(encryptedPart2) 285 | 286 | var decryptedPart2 = decipher.read() 287 | var tmp = xor(decryptedPart2, derivedHalf1.slice(16, 32)) 288 | var seedBPart2 = tmp.slice(8, 16) 289 | 290 | var decipher2 = aes.createDecipheriv('aes-256-ecb', derivedHalf2, Buffer.alloc(0)) 291 | decipher2.setAutoPadding(false) 292 | decipher2.write(encryptedPart1) // first 8 bytes 293 | decipher2.end(tmp.slice(0, 8)) // last 8 bytes 294 | 295 | var seedBPart1 = xor(decipher2.read(), derivedHalf1.slice(0, 16)) 296 | var seedB = Buffer.concat([seedBPart1, seedBPart2], 24) 297 | var factorB = BigInteger.fromBuffer(hash256(seedB)) 298 | 299 | // d = passFactor * factorB (mod n) 300 | var d = passInt.multiply(factorB).mod(curve.n) 301 | 302 | return { 303 | privateKey: d.toBuffer(32), 304 | compressed: compressed 305 | } 306 | } 307 | 308 | async function decryptECMultAsync (buffer, passphrase, progressCallback, scryptParams, promiseInterval) { 309 | buffer = buffer.slice(1) // FIXME: we can avoid this 310 | passphrase = Buffer.from(passphrase.normalize('NFC'), 'utf8') 311 | scryptParams = scryptParams || SCRYPT_PARAMS 312 | const { 313 | addressHash, 314 | encryptedPart1, 315 | encryptedPart2, 316 | ownerEntropy, 317 | ownerSalt, 318 | hasLotSeq, 319 | compressed, 320 | N, 321 | r, 322 | p 323 | } = prepareDecryptECMult(buffer, passphrase, progressCallback, scryptParams) 324 | 325 | var preFactor = await scrypt.async(passphrase, ownerSalt, N, r, p, 32, progressCallback, promiseInterval) 326 | 327 | const { 328 | passInt, 329 | passPoint 330 | } = getPassIntAndPoint(preFactor, ownerEntropy, hasLotSeq) 331 | 332 | var seedBPass = await scrypt.async(passPoint, Buffer.concat([addressHash, ownerEntropy]), 1024, 1, 1, 64, undefined, promiseInterval) 333 | 334 | return finishDecryptECMult(seedBPass, encryptedPart1, encryptedPart2, passInt, compressed) 335 | } 336 | 337 | function decryptECMult (buffer, passphrase, progressCallback, scryptParams) { 338 | buffer = buffer.slice(1) // FIXME: we can avoid this 339 | passphrase = Buffer.from(passphrase.normalize('NFC'), 'utf8') 340 | scryptParams = scryptParams || SCRYPT_PARAMS 341 | const { 342 | addressHash, 343 | encryptedPart1, 344 | encryptedPart2, 345 | ownerEntropy, 346 | ownerSalt, 347 | hasLotSeq, 348 | compressed, 349 | N, 350 | r, 351 | p 352 | } = prepareDecryptECMult(buffer, passphrase, progressCallback, scryptParams) 353 | var preFactor = scrypt(passphrase, ownerSalt, N, r, p, 32, progressCallback) 354 | 355 | const { 356 | passInt, 357 | passPoint 358 | } = getPassIntAndPoint(preFactor, ownerEntropy, hasLotSeq) 359 | 360 | var seedBPass = scrypt(passPoint, Buffer.concat([addressHash, ownerEntropy]), 1024, 1, 1, 64) 361 | 362 | return finishDecryptECMult(seedBPass, encryptedPart1, encryptedPart2, passInt, compressed) 363 | } 364 | 365 | function verify (string) { 366 | var decoded = bs58check.decodeUnsafe(string) 367 | if (!decoded) return false 368 | 369 | if (decoded.length !== 39) return false 370 | if (decoded.readUInt8(0) !== 0x01) return false 371 | 372 | var type = decoded.readUInt8(1) 373 | var flag = decoded.readUInt8(2) 374 | 375 | // encrypted WIF 376 | if (type === 0x42) { 377 | if (flag !== 0xc0 && flag !== 0xe0) return false 378 | 379 | // EC mult 380 | } else if (type === 0x43) { 381 | if ((flag & ~0x24)) return false 382 | } else { 383 | return false 384 | } 385 | 386 | return true 387 | } 388 | 389 | module.exports = { 390 | decrypt: decrypt, 391 | decryptECMult: decryptECMult, 392 | decryptRaw: decryptRaw, 393 | encrypt: encrypt, 394 | encryptRaw: encryptRaw, 395 | decryptAsync: decryptAsync, 396 | decryptECMultAsync: decryptECMultAsync, 397 | decryptRawAsync: decryptRawAsync, 398 | encryptAsync: encryptAsync, 399 | encryptRawAsync: encryptRawAsync, 400 | verify: verify 401 | } 402 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bip38", 3 | "version": "3.1.1", 4 | "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.", 5 | "main": "index.js", 6 | "keywords": [ 7 | "bitcoin", 8 | "crypto", 9 | "cryptography", 10 | "litecoin" 11 | ], 12 | "homepage": "http://cryptocoinjs.com/modules/currency/bip38/", 13 | "author": "JP Richardson", 14 | "dependencies": { 15 | "bigi": "^1.2.0", 16 | "browserify-aes": "^1.0.1", 17 | "bs58check": "<3.0.0", 18 | "buffer-xor": "^1.0.2", 19 | "create-hash": "^1.1.1", 20 | "ecurve": "^1.0.0", 21 | "safe-buffer": "~5.1.1", 22 | "scryptsy": "^2.1.0" 23 | }, 24 | "devDependencies": { 25 | "coveralls": "^3.0.4", 26 | "mocha": "^6.1.4", 27 | "mochify": "^6.2.0", 28 | "nyc": "^14.1.1", 29 | "standard": "^12.0.1", 30 | "wif": "^2.0.1" 31 | }, 32 | "repository": { 33 | "url": "git@github.com:bitcoinjs/bip38.git", 34 | "type": "git" 35 | }, 36 | "scripts": { 37 | "browser-test": "mochify --wd -R spec --timeout 100000", 38 | "ci:test": "npm run standard && npm run coveralls", 39 | "coverage": "nyc --check-coverage --reporter=lcov --reporter=text mocha", 40 | "coveralls": "npm run-script coverage && coveralls < coverage/lcov.info", 41 | "standard": "standard", 42 | "test": "npm run standard && npm run unit", 43 | "unit": "mocha --ui bdd --timeout 240000" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/fixtures.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid": [ 3 | { 4 | "passphrase": "TestingOneTwoThree", 5 | "bip38": "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", 6 | "wif": "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR", 7 | "address": "1Jq6MksXQVWzrznvZzxkV6oY57oWXD9TXB", 8 | "description": "no EC multiply / no compression #1" 9 | }, 10 | { 11 | "passphrase": "Satoshi", 12 | "bip38": "6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq", 13 | "wif": "5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5", 14 | "address": "1AvKt49sui9zfzGeo8EyL8ypvAhtR2KwbL", 15 | "description": "no EC multiply / no compression #2" 16 | }, 17 | { 18 | "passphrase": "\\u03D2\\u0301\\u{0000}\\u{00010400}\\u{0001F4A9}", 19 | "bip38": "6PRW5o9FLp4gJDDVqJQKJFTpMvdsSGJxMYHtHaQBF3ooa8mwD69bapcDQn", 20 | "wif": "5Jajm8eQ22H3pGWLEVCXyvND8dQZhiQhoLJNKjYXk9roUFTMSZ4", 21 | "address": "16ktGzmfrurhbhi6JGqsMWf7TyqK9HNAeF", 22 | "description": "no EC multiply / no compression #3" 23 | }, 24 | { 25 | "passphrase": "TestingOneTwoThree", 26 | "bip38": "6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo", 27 | "wif": "L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP", 28 | "address": "164MQi977u9GUteHr4EPH27VkkdxmfCvGW", 29 | "description": "no EC multiply / compression #1" 30 | }, 31 | { 32 | "passphrase": "Satoshi", 33 | "bip38": "6PYLtMnXvfG3oJde97zRyLYFZCYizPU5T3LwgdYJz1fRhh16bU7u6PPmY7", 34 | "wif": "KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7", 35 | "address": "1HmPbwsvG5qJ3KJfxzsZRZWhbm1xBMuS8B", 36 | "description": "no EC multiply / compression #2" 37 | }, 38 | { 39 | "passphrase": "TestingOneTwoThree", 40 | "bip38": "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", 41 | "wif": "5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2", 42 | "address": "1PE6TQi6HTVNz5DLwB1LcpMBALubfuN2z2", 43 | "description": "EC multiply / no compression, no lot sequence #1", 44 | "decryptOnly": true, 45 | "code": "passphrasepxFy57B9v8HtUsszJYKReoNDV6VHjUSGt8EVJmux9n1J3Ltf1gRxyDGXqnf9qm" 46 | }, 47 | { 48 | "passphrase": "Satoshi", 49 | "bip38": "6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd", 50 | "wif": "5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH", 51 | "address": "1CqzrtZC6mXSAhoxtFwVjz8LtwLJjDYU3V", 52 | "description": "EC multiply / no compression, no lot sequence #2", 53 | "decryptOnly": true, 54 | "code": "passphraseoRDGAXTWzbp72eVbtUDdn1rwpgPUGjNZEc6CGBo8i5EC1FPW8wcnLdq4ThKzAS" 55 | }, 56 | { 57 | "passphrase": "MOLON LABE", 58 | "bip38": "6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j", 59 | "wif": "5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8", 60 | "address": "1Jscj8ALrYu2y9TD8NrpvDBugPedmbj4Yh", 61 | "description": "EC multiply / no compression, lot sequence #1", 62 | "decryptOnly": true, 63 | "confirm": "cfrm38V8aXBn7JWA1ESmFMUn6erxeBGZGAxJPY4e36S9QWkzZKtaVqLNMgnifETYw7BPwWC9aPD", 64 | "code": "passphraseaB8feaLQDENqCgr4gKZpmf4VoaT6qdjJNJiv7fsKvjqavcJxvuR1hy25aTu5sX", 65 | "lot": 263183, 66 | "seq": 1 67 | }, 68 | { 69 | "passphrase": "ΜΟΛΩΝ ΛΑΒΕ", 70 | "bip38": "6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5mar2ngH", 71 | "wif": "5KMKKuUmAkiNbA3DazMQiLfDq47qs8MAEThm4yL8R2PhV1ov33D", 72 | "address": "1Lurmih3KruL4xDB5FmHof38yawNtP9oGf", 73 | "description": "EC multiply / no compression, lot sequence #1", 74 | "decryptOnly": true, 75 | "confirm": "cfrm38V8G4qq2ywYEFfWLD5Cc6msj9UwsG2Mj4Z6QdGJAFQpdatZLavkgRd1i4iBMdRngDqDs51", 76 | "code": "passphrased3z9rQJHSyBkNBwTRPkUGNVEVrUAcfAXDyRU1V28ie6hNFbqDwbFBvsTK7yWVK", 77 | "lot": 806938, 78 | "sequence": 1 79 | } 80 | ], 81 | "invalid": { 82 | "decrypt": [], 83 | "encrypt": [], 84 | "verify": [ 85 | { 86 | "description": "Invalid base58", 87 | "exception": "Invalid checksum", 88 | "base58": "6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5marXXXX" 89 | }, 90 | { 91 | "description": "Length > 39", 92 | "exception": "Invalid BIP38 data length", 93 | "hex": "0142c000000000000000000000000000000000000000000000000000000000000000000000000000", 94 | "base58": "QmxDezFMDL7ExfYmsETsQXAtBbw5YE1CDyA8pm1AGpMpVVUpsVy1yXv4VTL" 95 | }, 96 | { 97 | "description": "Length < 39", 98 | "exception": "Invalid BIP38 data length", 99 | "hex": "0142c00000000000000000000000000000000000000000000000000000000000000000000000", 100 | "base58": "2DnNxWcx4Prn8wmjbkvtYGDALsq8BMWxQ33KnXkeH8vrxE41psDLXRmK3" 101 | }, 102 | { 103 | "description": "prefix !== 0x01", 104 | "exception": "Invalid BIP38 prefix", 105 | "hex": "0242c0000000000000000000000000000000000000000000000000000000000000000000000000", 106 | "base58": "AfE1YY4Wr2FLAENaH9PVaLRdyk714V4rhwiJMSGyQCGFB3rhGDCs2R7c4s" 107 | }, 108 | { 109 | "description": "flag !== 0xc0 && flag !== 0xe0", 110 | "exception": "Invalid BIP38 type", 111 | "hex": "0101ff000000000000000000000000000000000000000000000000000000000000000000000000", 112 | "base58": "5JjnYkbFBmUnhGeDMVhR7aSitLToe1odEfXDBeg4RMK6JmAm9g7rkm7qY3" 113 | }, 114 | { 115 | "description": "EC Mult: ~(flag & 0x24)", 116 | "exception": "Invalid BIP38 type", 117 | "hex": "0101db000000000000000000000000000000000000000000000000000000000000000000000000", 118 | "base58": "5JbtdQFKSemRTqMuWrJgSfzE8AX2jdz1KiZuMmuUcv9iXha1s6UarQTciW" 119 | }, 120 | { 121 | "description": "EC Mult: ~(flag & 0x24)", 122 | "exception": "Invalid BIP38 type", 123 | "hex": "010135000000000000000000000000000000000000000000000000000000000000000000000000", 124 | "base58": "5HyV7HSYdHUgLf7w36mxMHDPH9muTgUYHEj6cEogKMuV7ae8VRM3VEg56w" 125 | } 126 | ] 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | var assert = require('assert') 4 | var bip38 = require('../') 5 | var bs58check = require('bs58check') 6 | var fixtures = require('./fixtures') 7 | var wif = require('wif') 8 | 9 | function replaceUnicode (str) { 10 | var map = { 11 | '\\u03D2\\u0301\\u{0000}\\u{00010400}\\u{0001F4A9}': '\u03D2\u0301\u{0000}\u{00010400}\u{0001F4A9}' 12 | } 13 | if (map[str]) str = map[str] 14 | return str 15 | } 16 | 17 | describe('bip38', function () { 18 | this.timeout(200000) 19 | 20 | describe('decrypt', function () { 21 | fixtures.valid.forEach(function (f) { 22 | it('should decrypt ' + f.description, function () { 23 | var result = bip38.decrypt(f.bip38, replaceUnicode(f.passphrase)) 24 | 25 | assert.strictEqual(wif.encode(0x80, result.privateKey, result.compressed), f.wif) 26 | }) 27 | }) 28 | 29 | fixtures.invalid.decrypt.forEach(function (f) { 30 | it('should throw ' + f.description, function () { 31 | assert.throws(function () { 32 | bip38.decrypt(f.bip38, f.passphrase) 33 | }, new RegExp(f.description, 'i')) 34 | }) 35 | }) 36 | 37 | fixtures.invalid.verify.forEach(function (f) { 38 | it('should throw because ' + f.description, function () { 39 | assert.throws(function () { 40 | bip38.decrypt(f.base58, 'foobar') 41 | }, new RegExp(f.exception)) 42 | }) 43 | }) 44 | }) 45 | 46 | describe('encrypt', function () { 47 | fixtures.valid.forEach(function (f) { 48 | if (f.decryptOnly) return 49 | 50 | it('should encrypt ' + f.description, function () { 51 | var buffer = bs58check.decode(f.wif) 52 | 53 | assert.strictEqual(bip38.encrypt(buffer.slice(1, 33), !!buffer[33], replaceUnicode(f.passphrase)), f.bip38) 54 | }) 55 | }) 56 | }) 57 | 58 | describe('decryptAsync', function () { 59 | fixtures.valid.forEach(function (f) { 60 | it('should decrypt ' + f.description, async function () { 61 | var result = await bip38.decryptAsync(f.bip38, replaceUnicode(f.passphrase)) 62 | 63 | assert.strictEqual(wif.encode(0x80, result.privateKey, result.compressed), f.wif) 64 | }) 65 | }) 66 | 67 | fixtures.invalid.decrypt.forEach(function (f) { 68 | it('should throw ' + f.description, async function () { 69 | assert.rejects(async function () { 70 | await bip38.decryptAsync(f.bip38, replaceUnicode(f.passphrase)) 71 | }, new RegExp(f.description, 'i')) 72 | }) 73 | }) 74 | 75 | fixtures.invalid.verify.forEach(function (f) { 76 | it('should throw because ' + f.description, async function () { 77 | assert.rejects(async function () { 78 | await bip38.decryptAsync(f.base58, 'foobar') 79 | }, new RegExp(f.exception)) 80 | }) 81 | }) 82 | }) 83 | 84 | describe('encryptAsync', function () { 85 | fixtures.valid.forEach(function (f) { 86 | if (f.decryptOnly) return 87 | 88 | it('should encrypt ' + f.description, async function () { 89 | var buffer = bs58check.decode(f.wif) 90 | 91 | assert.strictEqual(await bip38.encryptAsync(buffer.slice(1, 33), !!buffer[33], replaceUnicode(f.passphrase)), f.bip38) 92 | }) 93 | }) 94 | }) 95 | 96 | describe('verify', function () { 97 | fixtures.valid.forEach(function (f) { 98 | it('should return true for ' + f.bip38, function () { 99 | assert(bip38.verify(f.bip38)) 100 | }) 101 | }) 102 | 103 | fixtures.invalid.verify.forEach(function (f) { 104 | it('should return false for ' + f.description, function () { 105 | assert(!bip38.verify(f.base58)) 106 | }) 107 | }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --ui bdd 3 | --timeout 7000 --------------------------------------------------------------------------------