├── .gitignore ├── .min-wd ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── fixtures.json ├── index.js └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | coverage 3 | node_modules 4 | *.rdb 5 | *.log 6 | -------------------------------------------------------------------------------- /.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 | language: node_js 2 | before_install: 3 | - "npm install npm -g" 4 | node_js: 5 | - "4" 6 | - "5" 7 | - "6" 8 | - "7" 9 | - "8" 10 | after_success: 11 | - "npm run coveralls" 12 | sudo: false 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2.0.0 / 2016-12-20 2 | ------------------ 3 | - removed class instantiation. Removed `coinstring` dep. 4 | 5 | 1.4.0 / 2015-11-03 6 | ------------------ 7 | - added `progressCallback`. See: https://github.com/bitcoinjs/bip38/pull/16 8 | 9 | 1.3.0 / 2015-06-04 10 | ------------------ 11 | - use `createHash` and `aes` directly. https://github.com/cryptocoinjs/bip38 12 | - JavaScript Standard Style 13 | 14 | 1.2.0 / 2015-01-05 15 | ------------------ 16 | - removed dependency upon `aes` package since Browserify now supports aes [Daniel Cousens](https://github.com/cryptocoinjs/bip38/pull/6) 17 | - removed `crypto-browserify` devDep and removed `browser` field from `package.json`; no longer necessary 18 | - added method `verify()` [Daniel Cousens](https://github.com/cryptocoinjs/bip38/pull/7) 19 | 20 | 1.1.1 / 2014-09-19 21 | ------------------ 22 | - bugfix: enforce zero padding [Daniel Cousens](https://github.com/cryptocoinjs/bip38/commit/e73598d0fc1d1b3c04c132c34053e96bec6bd201) 23 | - add MIT license to package.json 24 | 25 | 1.1.0 / 2014-07-11 26 | ------------------ 27 | - added methods `encryptRaw` and `decryptRaw` [Daniel Cousens](https://github.com/cryptocoinjs/bip38/pull/4) 28 | 29 | 1.0.0 / 2014-06-10 30 | ------------------ 31 | - upgraded `"scryptsy": "~0.2.0"` to `"scryptsy": "^1.0.0"` 32 | - upgraded `"bigi": "~0.2.0"` to `"bigi": "^1.2.0"` 33 | - removed `ecurve-names` dep 34 | - upgraded `"ecurve": "~0.3.0"` to `"ecurve": "^0.8.0"` 35 | - removed semicolons per http://cryptocoinjs.com/about/contributing/#semicolons 36 | - removed `crypto-hashing` dep 37 | - removed `bs58` dep, added `coinstring` dep 38 | - removed `terst` for `assert` 39 | - added TravisCI 40 | - added Coveralls 41 | - added testling 42 | - removed static level methods, must call `new` 43 | 44 | 0.1.0 / 2014-03-05 45 | ------------------ 46 | - added support to decrypt ECMultiplied keys, #1 47 | - made constructor work without `new` 48 | - upgraded deps `ecurve`, `ecurve-names`, and `scryptsy` 49 | 50 | 0.0.1 / 2014-02-28 51 | ------------------ 52 | - initial release 53 | -------------------------------------------------------------------------------- /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 imprevious 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 | ### API 33 | ### encrypt(buffer, compressed, passphrase[, progressCallback, scryptParams]) 34 | 35 | ``` javascript 36 | var bip38 = require('bip38') 37 | var wif = require('wif') 38 | 39 | var myWifString = '5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR' 40 | var decoded = wif.decode(myWifString) 41 | 42 | var encryptedKey = bip38.encrypt(decoded.privateKey, decoded.compressed, 'TestingOneTwoThree') 43 | console.log(encryptedKey) 44 | // => '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg' 45 | ``` 46 | 47 | 48 | ### decrypt(encryptedKey, passhprase[, progressCallback, scryptParams]) 49 | 50 | ``` javascript 51 | var bip38 = require('bip38') 52 | var wif = require('wif') 53 | 54 | var encryptedKey = '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg' 55 | var decryptedKey = bip38.decrypt(encryptedKey, 'TestingOneTwoThree', function (status) { 56 | console.log(status.percent) // will print the precent every time current increases by 1000 57 | }) 58 | 59 | console.log(wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed)) 60 | // => '5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR' 61 | ``` 62 | 63 | 64 | # References 65 | - https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki 66 | - https://github.com/pointbiz/bitaddress.org/issues/56 (Safari 6.05 issue) 67 | - https://github.com/casascius/Bitcoin-Address-Utility/tree/master/Model 68 | - https://github.com/nomorecoin/python-bip38-testing/blob/master/bip38.py 69 | - https://github.com/pointbiz/bitaddress.org/blob/master/src/ninja.key.js 70 | 71 | -------------------------------------------------------------------------------- /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 | return createHash('rmd160').update( 24 | createHash('sha256').update(buffer).digest() 25 | ).digest() 26 | } 27 | 28 | function hash256 (buffer) { 29 | return createHash('sha256').update( 30 | createHash('sha256').update(buffer).digest() 31 | ).digest() 32 | } 33 | 34 | function getAddress (d, compressed) { 35 | var Q = curve.G.multiply(d).getEncoded(compressed) 36 | var hash = hash160(Q) 37 | var payload = Buffer.allocUnsafe(21) 38 | payload.writeUInt8(0x00, 0) // XXX TODO FIXME bitcoin only??? damn you BIP38 39 | hash.copy(payload, 1) 40 | 41 | return bs58check.encode(payload) 42 | } 43 | 44 | function encryptRaw (buffer, compressed, passphrase, progressCallback, scryptParams) { 45 | if (buffer.length !== 32) throw new Error('Invalid private key length') 46 | scryptParams = scryptParams || SCRYPT_PARAMS 47 | 48 | var d = BigInteger.fromBuffer(buffer) 49 | var address = getAddress(d, compressed) 50 | var secret = Buffer.from(passphrase, 'utf8') 51 | var salt = hash256(address).slice(0, 4) 52 | 53 | var N = scryptParams.N 54 | var r = scryptParams.r 55 | var p = scryptParams.p 56 | 57 | var scryptBuf = scrypt(secret, salt, N, r, p, 64, progressCallback) 58 | var derivedHalf1 = scryptBuf.slice(0, 32) 59 | var derivedHalf2 = scryptBuf.slice(32, 64) 60 | 61 | var xorBuf = xor(derivedHalf1, buffer) 62 | var cipher = aes.createCipheriv('aes-256-ecb', derivedHalf2, NULL) 63 | cipher.setAutoPadding(false) 64 | cipher.end(xorBuf) 65 | 66 | var cipherText = cipher.read() 67 | 68 | // 0x01 | 0x42 | flagByte | salt (4) | cipherText (32) 69 | var result = Buffer.allocUnsafe(7 + 32) 70 | result.writeUInt8(0x01, 0) 71 | result.writeUInt8(0x42, 1) 72 | result.writeUInt8(compressed ? 0xe0 : 0xc0, 2) 73 | salt.copy(result, 3) 74 | cipherText.copy(result, 7) 75 | 76 | return result 77 | } 78 | 79 | function encrypt (buffer, compressed, passphrase, progressCallback, scryptParams) { 80 | return bs58check.encode(encryptRaw(buffer, compressed, passphrase, progressCallback, scryptParams)) 81 | } 82 | 83 | // some of the techniques borrowed from: https://github.com/pointbiz/bitaddress.org 84 | function decryptRaw (buffer, passphrase, progressCallback, scryptParams) { 85 | // 39 bytes: 2 bytes prefix, 37 bytes payload 86 | if (buffer.length !== 39) throw new Error('Invalid BIP38 data length') 87 | if (buffer.readUInt8(0) !== 0x01) throw new Error('Invalid BIP38 prefix') 88 | scryptParams = scryptParams || SCRYPT_PARAMS 89 | 90 | // check if BIP38 EC multiply 91 | var type = buffer.readUInt8(1) 92 | if (type === 0x43) return decryptECMult(buffer, passphrase, progressCallback, scryptParams) 93 | if (type !== 0x42) throw new Error('Invalid BIP38 type') 94 | 95 | passphrase = Buffer.from(passphrase, 'utf8') 96 | 97 | var flagByte = buffer.readUInt8(2) 98 | var compressed = flagByte === 0xe0 99 | if (!compressed && flagByte !== 0xc0) throw new Error('Invalid BIP38 compression flag') 100 | 101 | var N = scryptParams.N 102 | var r = scryptParams.r 103 | var p = scryptParams.p 104 | 105 | var salt = buffer.slice(3, 7) 106 | var scryptBuf = scrypt(passphrase, salt, N, r, p, 64, progressCallback) 107 | var derivedHalf1 = scryptBuf.slice(0, 32) 108 | var derivedHalf2 = scryptBuf.slice(32, 64) 109 | 110 | var privKeyBuf = buffer.slice(7, 7 + 32) 111 | var decipher = aes.createDecipheriv('aes-256-ecb', derivedHalf2, NULL) 112 | decipher.setAutoPadding(false) 113 | decipher.end(privKeyBuf) 114 | 115 | var plainText = decipher.read() 116 | var privateKey = xor(derivedHalf1, plainText) 117 | 118 | // verify salt matches address 119 | var d = BigInteger.fromBuffer(privateKey) 120 | var address = getAddress(d, compressed) 121 | var checksum = hash256(address).slice(0, 4) 122 | assert.deepEqual(salt, checksum) 123 | 124 | return { 125 | privateKey: privateKey, 126 | compressed: compressed 127 | } 128 | } 129 | 130 | function decrypt (string, passphrase, progressCallback, scryptParams) { 131 | return decryptRaw(bs58check.decode(string), passphrase, progressCallback, scryptParams) 132 | } 133 | 134 | function decryptECMult (buffer, passphrase, progressCallback, scryptParams) { 135 | passphrase = Buffer.from(passphrase, 'utf8') 136 | buffer = buffer.slice(1) // FIXME: we can avoid this 137 | scryptParams = scryptParams || SCRYPT_PARAMS 138 | 139 | var flag = buffer.readUInt8(1) 140 | var compressed = (flag & 0x20) !== 0 141 | var hasLotSeq = (flag & 0x04) !== 0 142 | 143 | assert.equal((flag & 0x24), flag, 'Invalid private key.') 144 | 145 | var addressHash = buffer.slice(2, 6) 146 | var ownerEntropy = buffer.slice(6, 14) 147 | var ownerSalt 148 | 149 | // 4 bytes ownerSalt if 4 bytes lot/sequence 150 | if (hasLotSeq) { 151 | ownerSalt = ownerEntropy.slice(0, 4) 152 | 153 | // else, 8 bytes ownerSalt 154 | } else { 155 | ownerSalt = ownerEntropy 156 | } 157 | 158 | var encryptedPart1 = buffer.slice(14, 22) // First 8 bytes 159 | var encryptedPart2 = buffer.slice(22, 38) // 16 bytes 160 | 161 | var N = scryptParams.N 162 | var r = scryptParams.r 163 | var p = scryptParams.p 164 | var preFactor = scrypt(passphrase, ownerSalt, N, r, p, 32, progressCallback) 165 | 166 | var passFactor 167 | if (hasLotSeq) { 168 | var hashTarget = Buffer.concat([preFactor, ownerEntropy]) 169 | passFactor = hash256(hashTarget) 170 | } else { 171 | passFactor = preFactor 172 | } 173 | 174 | var passInt = BigInteger.fromBuffer(passFactor) 175 | var passPoint = curve.G.multiply(passInt).getEncoded(true) 176 | 177 | var seedBPass = scrypt(passPoint, Buffer.concat([addressHash, ownerEntropy]), 1024, 1, 1, 64) 178 | var derivedHalf1 = seedBPass.slice(0, 32) 179 | var derivedHalf2 = seedBPass.slice(32, 64) 180 | 181 | var decipher = aes.createDecipheriv('aes-256-ecb', derivedHalf2, Buffer.alloc(0)) 182 | decipher.setAutoPadding(false) 183 | decipher.end(encryptedPart2) 184 | 185 | var decryptedPart2 = decipher.read() 186 | var tmp = xor(decryptedPart2, derivedHalf1.slice(16, 32)) 187 | var seedBPart2 = tmp.slice(8, 16) 188 | 189 | var decipher2 = aes.createDecipheriv('aes-256-ecb', derivedHalf2, Buffer.alloc(0)) 190 | decipher2.setAutoPadding(false) 191 | decipher2.write(encryptedPart1) // first 8 bytes 192 | decipher2.end(tmp.slice(0, 8)) // last 8 bytes 193 | 194 | var seedBPart1 = xor(decipher2.read(), derivedHalf1.slice(0, 16)) 195 | var seedB = Buffer.concat([seedBPart1, seedBPart2], 24) 196 | var factorB = BigInteger.fromBuffer(hash256(seedB)) 197 | 198 | // d = passFactor * factorB (mod n) 199 | var d = passInt.multiply(factorB).mod(curve.n) 200 | 201 | return { 202 | privateKey: d.toBuffer(32), 203 | compressed: compressed 204 | } 205 | } 206 | 207 | function verify (string) { 208 | var decoded = bs58check.decodeUnsafe(string) 209 | if (!decoded) return false 210 | 211 | if (decoded.length !== 39) return false 212 | if (decoded.readUInt8(0) !== 0x01) return false 213 | 214 | var type = decoded.readUInt8(1) 215 | var flag = decoded.readUInt8(2) 216 | 217 | // encrypted WIF 218 | if (type === 0x42) { 219 | if (flag !== 0xc0 && flag !== 0xe0) return false 220 | 221 | // EC mult 222 | } else if (type === 0x43) { 223 | if ((flag & ~0x24)) return false 224 | } else { 225 | return false 226 | } 227 | 228 | return true 229 | } 230 | 231 | module.exports = { 232 | decrypt: decrypt, 233 | decryptECMult: decryptECMult, 234 | decryptRaw: decryptRaw, 235 | encrypt: encrypt, 236 | encryptRaw: encryptRaw, 237 | verify: verify 238 | } 239 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bip38", 3 | "version": "2.0.2", 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 | "scryptsy": "^2.0.0" 22 | }, 23 | "devDependencies": { 24 | "coveralls": "^2.10.0", 25 | "istanbul": "^0.2.11", 26 | "mocha": "^2.3.3", 27 | "mochify": "^2.1.1", 28 | "standard": "^9.0.2", 29 | "wif": "^2.0.1" 30 | }, 31 | "repository": { 32 | "url": "git@github.com:bitcoinjs/bip38.git", 33 | "type": "git" 34 | }, 35 | "scripts": { 36 | "browser-test": "mochify --wd -R spec --timeout 100000", 37 | "coverage": "istanbul cover _mocha -- --reporter list test/*.js", 38 | "coveralls": "npm run-script coverage && coveralls < coverage/lcov.info", 39 | "standard": "standard", 40 | "test": "npm run standard && npm run unit", 41 | "unit": "mocha --ui bdd --timeout 240000" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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": "TestingOneTwoThree", 19 | "bip38": "6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo", 20 | "wif": "L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP", 21 | "address": "164MQi977u9GUteHr4EPH27VkkdxmfCvGW", 22 | "description": "no EC multiply / compression #1" 23 | }, 24 | { 25 | "passphrase": "Satoshi", 26 | "bip38": "6PYLtMnXvfG3oJde97zRyLYFZCYizPU5T3LwgdYJz1fRhh16bU7u6PPmY7", 27 | "wif": "KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7", 28 | "address": "1HmPbwsvG5qJ3KJfxzsZRZWhbm1xBMuS8B", 29 | "description": "no EC multiply / compression #2" 30 | }, 31 | { 32 | "passphrase": "TestingOneTwoThree", 33 | "bip38": "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", 34 | "wif": "5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2", 35 | "address": "1PE6TQi6HTVNz5DLwB1LcpMBALubfuN2z2", 36 | "description": "EC multiply / no compression, no lot sequence #1", 37 | "decryptOnly": true, 38 | "code": "passphrasepxFy57B9v8HtUsszJYKReoNDV6VHjUSGt8EVJmux9n1J3Ltf1gRxyDGXqnf9qm" 39 | }, 40 | { 41 | "passphrase": "Satoshi", 42 | "bip38": "6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd", 43 | "wif": "5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH", 44 | "address": "1CqzrtZC6mXSAhoxtFwVjz8LtwLJjDYU3V", 45 | "description": "EC multiply / no compression, no lot sequence #2", 46 | "decryptOnly": true, 47 | "code": "passphraseoRDGAXTWzbp72eVbtUDdn1rwpgPUGjNZEc6CGBo8i5EC1FPW8wcnLdq4ThKzAS" 48 | }, 49 | { 50 | "passphrase": "MOLON LABE", 51 | "bip38": "6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j", 52 | "wif": "5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8", 53 | "address": "1Jscj8ALrYu2y9TD8NrpvDBugPedmbj4Yh", 54 | "description": "EC multiply / no compression, lot sequence #1", 55 | "decryptOnly": true, 56 | "confirm": "cfrm38V8aXBn7JWA1ESmFMUn6erxeBGZGAxJPY4e36S9QWkzZKtaVqLNMgnifETYw7BPwWC9aPD", 57 | "code": "passphraseaB8feaLQDENqCgr4gKZpmf4VoaT6qdjJNJiv7fsKvjqavcJxvuR1hy25aTu5sX", 58 | "lot": 263183, 59 | "seq": 1 60 | }, 61 | { 62 | "passphrase": "ΜΟΛΩΝ ΛΑΒΕ", 63 | "bip38": "6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5mar2ngH", 64 | "wif": "5KMKKuUmAkiNbA3DazMQiLfDq47qs8MAEThm4yL8R2PhV1ov33D", 65 | "address": "1Lurmih3KruL4xDB5FmHof38yawNtP9oGf", 66 | "description": "EC multiply / no compression, lot sequence #1", 67 | "decryptOnly": true, 68 | "confirm": "cfrm38V8G4qq2ywYEFfWLD5Cc6msj9UwsG2Mj4Z6QdGJAFQpdatZLavkgRd1i4iBMdRngDqDs51", 69 | "code": "passphrased3z9rQJHSyBkNBwTRPkUGNVEVrUAcfAXDyRU1V28ie6hNFbqDwbFBvsTK7yWVK", 70 | "lot": 806938, 71 | "sequence": 1 72 | } 73 | ], 74 | "invalid": { 75 | "decrypt": [], 76 | "encrypt": [], 77 | "verify": [ 78 | { 79 | "description": "Invalid base58", 80 | "exception": "Invalid checksum", 81 | "base58": "6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5marXXXX" 82 | }, 83 | { 84 | "description": "Length > 39", 85 | "exception": "Invalid BIP38 data length", 86 | "hex": "0142c000000000000000000000000000000000000000000000000000000000000000000000000000", 87 | "base58": "QmxDezFMDL7ExfYmsETsQXAtBbw5YE1CDyA8pm1AGpMpVVUpsVy1yXv4VTL" 88 | }, 89 | { 90 | "description": "Length < 39", 91 | "exception": "Invalid BIP38 data length", 92 | "hex": "0142c00000000000000000000000000000000000000000000000000000000000000000000000", 93 | "base58": "2DnNxWcx4Prn8wmjbkvtYGDALsq8BMWxQ33KnXkeH8vrxE41psDLXRmK3" 94 | }, 95 | { 96 | "description": "prefix !== 0x01", 97 | "exception": "Invalid BIP38 prefix", 98 | "hex": "0242c0000000000000000000000000000000000000000000000000000000000000000000000000", 99 | "base58": "AfE1YY4Wr2FLAENaH9PVaLRdyk714V4rhwiJMSGyQCGFB3rhGDCs2R7c4s" 100 | }, 101 | { 102 | "description": "flag !== 0xc0 && flag !== 0xe0", 103 | "exception": "Invalid BIP38 type", 104 | "hex": "0101ff000000000000000000000000000000000000000000000000000000000000000000000000", 105 | "base58": "5JjnYkbFBmUnhGeDMVhR7aSitLToe1odEfXDBeg4RMK6JmAm9g7rkm7qY3" 106 | }, 107 | { 108 | "description": "EC Mult: ~(flag & 0x24)", 109 | "exception": "Invalid BIP38 type", 110 | "hex": "0101db000000000000000000000000000000000000000000000000000000000000000000000000", 111 | "base58": "5JbtdQFKSemRTqMuWrJgSfzE8AX2jdz1KiZuMmuUcv9iXha1s6UarQTciW" 112 | }, 113 | { 114 | "description": "EC Mult: ~(flag & 0x24)", 115 | "exception": "Invalid BIP38 type", 116 | "hex": "010135000000000000000000000000000000000000000000000000000000000000000000000000", 117 | "base58": "5HyV7HSYdHUgLf7w36mxMHDPH9muTgUYHEj6cEogKMuV7ae8VRM3VEg56w" 118 | } 119 | ] 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /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 | describe('bip38', function () { 10 | this.timeout(200000) 11 | 12 | describe('decrypt', function () { 13 | fixtures.valid.forEach(function (f) { 14 | it('should decrypt ' + f.description, function () { 15 | var result = bip38.decrypt(f.bip38, f.passphrase) 16 | 17 | assert.equal(wif.encode(0x80, result.privateKey, result.compressed), f.wif) 18 | }) 19 | }) 20 | 21 | fixtures.invalid.decrypt.forEach(function (f) { 22 | it('should throw ' + f.description, function () { 23 | assert.throws(function () { 24 | bip38.decrypt(f.bip38, f.passphrase) 25 | }, new RegExp(f.description, 'i')) 26 | }) 27 | }) 28 | 29 | fixtures.invalid.verify.forEach(function (f) { 30 | it('should throw because ' + f.description, function () { 31 | assert.throws(function () { 32 | bip38.decrypt(f.base58, 'foobar') 33 | }, new RegExp(f.exception)) 34 | }) 35 | }) 36 | }) 37 | 38 | describe('encrypt', function () { 39 | fixtures.valid.forEach(function (f) { 40 | if (f.decryptOnly) return 41 | 42 | it('should encrypt ' + f.description, function () { 43 | var buffer = bs58check.decode(f.wif) 44 | 45 | assert.equal(bip38.encrypt(buffer.slice(1, 33), !!buffer[33], f.passphrase), f.bip38) 46 | }) 47 | }) 48 | }) 49 | 50 | describe('verify', function () { 51 | fixtures.valid.forEach(function (f) { 52 | it('should return true for ' + f.bip38, function () { 53 | assert(bip38.verify(f.bip38)) 54 | }) 55 | }) 56 | 57 | fixtures.invalid.verify.forEach(function (f) { 58 | it('should return false for ' + f.description, function () { 59 | assert(!bip38.verify(f.base58)) 60 | }) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --ui bdd 3 | --timeout 7000 --------------------------------------------------------------------------------