diff options
Diffstat (limited to 'libs/bitcoinjs-bip38')
-rw-r--r-- | libs/bitcoinjs-bip38/index.js | 244 | ||||
-rw-r--r-- | libs/bitcoinjs-bip38/package.json | 38 | ||||
-rw-r--r-- | libs/bitcoinjs-bip38/readme.md | 4 |
3 files changed, 286 insertions, 0 deletions
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 @@ | |||
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 encryptRaw (buffer, compressed, passphrase, progressCallback, scryptParams) { | ||
51 | if (buffer.length !== 32) throw new Error('Invalid private key length') | ||
52 | scryptParams = scryptParams || SCRYPT_PARAMS | ||
53 | |||
54 | var d = BigInteger.fromBuffer(buffer) | ||
55 | var address = getAddress(d, compressed) | ||
56 | var secret = Buffer.from(passphrase, 'utf8') | ||
57 | var salt = hash256(address).slice(0, 4) | ||
58 | |||
59 | var N = scryptParams.N | ||
60 | var r = scryptParams.r | ||
61 | var p = scryptParams.p | ||
62 | |||
63 | var scryptBuf = scrypt(secret, salt, N, r, p, 64, progressCallback) | ||
64 | var derivedHalf1 = scryptBuf.slice(0, 32) | ||
65 | var derivedHalf2 = scryptBuf.slice(32, 64) | ||
66 | |||
67 | var xorBuf = xor(derivedHalf1, buffer) | ||
68 | var cipher = aes.createCipheriv('aes-256-ecb', derivedHalf2, NULL) | ||
69 | cipher.setAutoPadding(false) | ||
70 | cipher.end(xorBuf) | ||
71 | |||
72 | var cipherText = cipher.read() | ||
73 | |||
74 | // 0x01 | 0x42 | flagByte | salt (4) | cipherText (32) | ||
75 | var result = Buffer.allocUnsafe(7 + 32) | ||
76 | result.writeUInt8(0x01, 0) | ||
77 | result.writeUInt8(0x42, 1) | ||
78 | result.writeUInt8(compressed ? 0xe0 : 0xc0, 2) | ||
79 | salt.copy(result, 3) | ||
80 | cipherText.copy(result, 7) | ||
81 | |||
82 | return result | ||
83 | } | ||
84 | |||
85 | function encrypt (buffer, compressed, passphrase, progressCallback, scryptParams) { | ||
86 | return bs58check.encode(encryptRaw(buffer, compressed, passphrase, progressCallback, scryptParams)) | ||
87 | } | ||
88 | |||
89 | // some of the techniques borrowed from: https://github.com/pointbiz/bitaddress.org | ||
90 | function decryptRaw (buffer, passphrase, progressCallback, scryptParams) { | ||
91 | // 39 bytes: 2 bytes prefix, 37 bytes payload | ||
92 | if (buffer.length !== 39) throw new Error('Invalid BIP38 data length') | ||
93 | if (buffer.readUInt8(0) !== 0x01) throw new Error('Invalid BIP38 prefix') | ||
94 | scryptParams = scryptParams || SCRYPT_PARAMS | ||
95 | |||
96 | // check if BIP38 EC multiply | ||
97 | var type = buffer.readUInt8(1) | ||
98 | if (type === 0x43) return decryptECMult(buffer, passphrase, progressCallback, scryptParams) | ||
99 | if (type !== 0x42) throw new Error('Invalid BIP38 type') | ||
100 | |||
101 | passphrase = Buffer.from(passphrase, 'utf8') | ||
102 | |||
103 | var flagByte = buffer.readUInt8(2) | ||
104 | var compressed = flagByte === 0xe0 | ||
105 | if (!compressed && flagByte !== 0xc0) throw new Error('Invalid BIP38 compression flag') | ||
106 | |||
107 | var N = scryptParams.N | ||
108 | var r = scryptParams.r | ||
109 | var p = scryptParams.p | ||
110 | |||
111 | var salt = buffer.slice(3, 7) | ||
112 | var scryptBuf = scrypt(passphrase, salt, N, r, p, 64, progressCallback) | ||
113 | var derivedHalf1 = scryptBuf.slice(0, 32) | ||
114 | var derivedHalf2 = scryptBuf.slice(32, 64) | ||
115 | |||
116 | var privKeyBuf = buffer.slice(7, 7 + 32) | ||
117 | var decipher = aes.createDecipheriv('aes-256-ecb', derivedHalf2, NULL) | ||
118 | decipher.setAutoPadding(false) | ||
119 | decipher.end(privKeyBuf) | ||
120 | |||
121 | var plainText = decipher.read() | ||
122 | var privateKey = xor(derivedHalf1, plainText) | ||
123 | |||
124 | // verify salt matches address | ||
125 | var d = BigInteger.fromBuffer(privateKey) | ||
126 | var address = getAddress(d, compressed) | ||
127 | var checksum = hash256(address).slice(0, 4) | ||
128 | assert.deepStrictEqual(salt, checksum) | ||
129 | |||
130 | return { | ||
131 | privateKey: privateKey, | ||
132 | compressed: compressed | ||
133 | } | ||
134 | } | ||
135 | |||
136 | function decrypt (string, passphrase, progressCallback, scryptParams) { | ||
137 | return decryptRaw(bs58check.decode(string), passphrase, progressCallback, scryptParams) | ||
138 | } | ||
139 | |||
140 | function decryptECMult (buffer, passphrase, progressCallback, scryptParams) { | ||
141 | passphrase = Buffer.from(passphrase, 'utf8') | ||
142 | buffer = buffer.slice(1) // FIXME: we can avoid this | ||
143 | scryptParams = scryptParams || SCRYPT_PARAMS | ||
144 | |||
145 | var flag = buffer.readUInt8(1) | ||
146 | var compressed = (flag & 0x20) !== 0 | ||
147 | var hasLotSeq = (flag & 0x04) !== 0 | ||
148 | |||
149 | assert.strictEqual((flag & 0x24), flag, 'Invalid private key.') | ||
150 | |||
151 | var addressHash = buffer.slice(2, 6) | ||
152 | var ownerEntropy = buffer.slice(6, 14) | ||
153 | var ownerSalt | ||
154 | |||
155 | // 4 bytes ownerSalt if 4 bytes lot/sequence | ||
156 | if (hasLotSeq) { | ||
157 | ownerSalt = ownerEntropy.slice(0, 4) | ||
158 | |||
159 | // else, 8 bytes ownerSalt | ||
160 | } else { | ||
161 | ownerSalt = ownerEntropy | ||
162 | } | ||
163 | |||
164 | var encryptedPart1 = buffer.slice(14, 22) // First 8 bytes | ||
165 | var encryptedPart2 = buffer.slice(22, 38) // 16 bytes | ||
166 | |||
167 | var N = scryptParams.N | ||
168 | var r = scryptParams.r | ||
169 | var p = scryptParams.p | ||
170 | var preFactor = scrypt(passphrase, ownerSalt, N, r, p, 32, progressCallback) | ||
171 | |||
172 | var passFactor | ||
173 | if (hasLotSeq) { | ||
174 | var hashTarget = Buffer.concat([preFactor, ownerEntropy]) | ||
175 | passFactor = hash256(hashTarget) | ||
176 | } else { | ||
177 | passFactor = preFactor | ||
178 | } | ||
179 | |||
180 | var passInt = BigInteger.fromBuffer(passFactor) | ||
181 | var passPoint = curve.G.multiply(passInt).getEncoded(true) | ||
182 | |||
183 | var seedBPass = scrypt(passPoint, Buffer.concat([addressHash, ownerEntropy]), 1024, 1, 1, 64) | ||
184 | var derivedHalf1 = seedBPass.slice(0, 32) | ||
185 | var derivedHalf2 = seedBPass.slice(32, 64) | ||
186 | |||
187 | var decipher = aes.createDecipheriv('aes-256-ecb', derivedHalf2, Buffer.alloc(0)) | ||
188 | decipher.setAutoPadding(false) | ||
189 | decipher.end(encryptedPart2) | ||
190 | |||
191 | var decryptedPart2 = decipher.read() | ||
192 | var tmp = xor(decryptedPart2, derivedHalf1.slice(16, 32)) | ||
193 | var seedBPart2 = tmp.slice(8, 16) | ||
194 | |||
195 | var decipher2 = aes.createDecipheriv('aes-256-ecb', derivedHalf2, Buffer.alloc(0)) | ||
196 | decipher2.setAutoPadding(false) | ||
197 | decipher2.write(encryptedPart1) // first 8 bytes | ||
198 | decipher2.end(tmp.slice(0, 8)) // last 8 bytes | ||
199 | |||
200 | var seedBPart1 = xor(decipher2.read(), derivedHalf1.slice(0, 16)) | ||
201 | var seedB = Buffer.concat([seedBPart1, seedBPart2], 24) | ||
202 | var factorB = BigInteger.fromBuffer(hash256(seedB)) | ||
203 | |||
204 | // d = passFactor * factorB (mod n) | ||
205 | var d = passInt.multiply(factorB).mod(curve.n) | ||
206 | |||
207 | return { | ||
208 | privateKey: d.toBuffer(32), | ||
209 | compressed: compressed | ||
210 | } | ||
211 | } | ||
212 | |||
213 | function verify (string) { | ||
214 | var decoded = bs58check.decodeUnsafe(string) | ||
215 | if (!decoded) return false | ||
216 | |||
217 | if (decoded.length !== 39) return false | ||
218 | if (decoded.readUInt8(0) !== 0x01) return false | ||
219 | |||
220 | var type = decoded.readUInt8(1) | ||
221 | var flag = decoded.readUInt8(2) | ||
222 | |||
223 | // encrypted WIF | ||
224 | if (type === 0x42) { | ||
225 | if (flag !== 0xc0 && flag !== 0xe0) return false | ||
226 | |||
227 | // EC mult | ||
228 | } else if (type === 0x43) { | ||
229 | if ((flag & ~0x24)) return false | ||
230 | } else { | ||
231 | return false | ||
232 | } | ||
233 | |||
234 | return true | ||
235 | } | ||
236 | |||
237 | module.exports = { | ||
238 | decrypt: decrypt, | ||
239 | decryptECMult: decryptECMult, | ||
240 | decryptRaw: decryptRaw, | ||
241 | encrypt: encrypt, | ||
242 | encryptRaw: encryptRaw, | ||
243 | verify: verify | ||
244 | } | ||
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 @@ | |||
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 | }, | ||
25 | "repository": { | ||
26 | "url": "git@github.com:bitcoinjs/bip38.git", | ||
27 | "type": "git" | ||
28 | }, | ||
29 | "scripts": { | ||
30 | "browser-test": "mochify --wd -R spec --timeout 100000", | ||
31 | "build": "browserify index.js --standalone bitcoinjs-bip38 > /tmp/bitcoinjs-bip38.js", | ||
32 | "coverage": "istanbul cover _mocha -- --reporter list test/*.js", | ||
33 | "coveralls": "npm run-script coverage && coveralls < coverage/lcov.info", | ||
34 | "standard": "standard", | ||
35 | "test": "npm run standard && npm run unit", | ||
36 | "unit": "mocha --ui bdd --timeout 240000" | ||
37 | } | ||
38 | } | ||
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 @@ | |||
1 | Build (will create a bundle and copy it to /tmp/bitcoinjs-bip38.js): | ||
2 | |||
3 | npm install | ||
4 | npm run build | ||