]>
Commit | Line | Data |
---|---|---|
5db3540e IC |
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 | } |