Skip to content

Commit 8b84f6c

Browse files
Weston SiegenthalerWeston Siegenthaler
Weston Siegenthaler
authored and
Weston Siegenthaler
committed
support for bip38 ec-multiply key generation (courtesy of Zeilap)
1 parent d3ec255 commit 8b84f6c

File tree

3 files changed

+166
-3
lines changed

3 files changed

+166
-3
lines changed

src/bip38.js

+116-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ Bitcoin.BIP38 = (function () {
125125
passfactor = Bitcoin.Util.dsha256(prefactorB);
126126
}
127127
var kp = new Bitcoin.ECKey(passfactor);
128-
var passpoint = kp.getPubCompressed();
128+
kp.compressed = true;
129+
var passpoint = kp.getPub();
129130

130131
var encryptedPart2 = hex.slice(23, 23+16);
131132

@@ -151,6 +152,120 @@ Bitcoin.BIP38 = (function () {
151152
}
152153
}
153154

155+
/**
156+
* Generates an intermediate point based on a password which can later be used
157+
* to directly generate new BIP38-encrypted private keys without actually knowing
158+
* the password.
159+
* @author Zeilap
160+
*/
161+
BIP38.generateIntermediate = function(passphrase, lotNum, sequenceNum) {
162+
var noNumbers = lotNum == null || sequenceNum == null;
163+
var ownerEntropy, ownerSalt;
164+
165+
if(noNumbers) {
166+
ownerSalt = ownerEntropy = new Array(8);
167+
rng.nextBytes(ownerEntropy);
168+
} else {
169+
// 1) generate 4 random bytes
170+
var ownerSalt = Array(4);
171+
172+
rng.nextBytes(ownerSalt);
173+
174+
// 2) Encode the lot and sequence numbers as a 4 byte quantity (big-endian):
175+
// lotnumber * 4096 + sequencenumber. Call these four bytes lotsequence.
176+
var lotSequence = nbv(4096*lotNum + sequenceNum).toByteArrayUnsigned();
177+
178+
// 3) Concatenate ownersalt + lotsequence and call this ownerentropy.
179+
var ownerEntropy = ownerSalt.concat(lotSequence);
180+
}
181+
182+
// 4) Derive a key from the passphrase using scrypt
183+
var prefactor = scrypt(passphrase, ownerSalt, BIP38.scryptParams.N, BIP38.scryptParams.r, BIP38.scryptParams.p, 32);
184+
185+
// Take SHA256(SHA256(prefactor + ownerentropy)) and call this passfactor
186+
var passfactorBytes = noNumbers? prefactor : Bitcoin.Util.dsha256(prefactor.concat(ownerEntropy));
187+
var passfactor = BigInteger.fromByteArrayUnsigned(passfactorBytes);
188+
189+
// 5) Compute the elliptic curve point G * passfactor, and convert the result to compressed notation (33 bytes)
190+
var passpoint = ecparams.getG().multiply(passfactor).getEncoded(1);
191+
192+
// 6) Convey ownersalt and passpoint to the party generating the keys, along with a checksum to ensure integrity.
193+
// magic bytes "2C E9 B3 E1 FF 39 E2 51" followed by ownerentropy, and then passpoint
194+
var magicBytes = [0x2C, 0xE9, 0xB3, 0xE1, 0xFF, 0x39, 0xE2, 0x51];
195+
if(noNumbers) magicBytes[7] = 0x53;
196+
197+
var intermediate = magicBytes.concat(ownerEntropy).concat(passpoint);
198+
199+
// base58check encode
200+
intermediate = intermediate.concat(Bitcoin.Util.dsha256(intermediate).slice(0,4));
201+
return Bitcoin.Base58.encode(intermediate);
202+
};
203+
204+
/**
205+
* Creates new private key using an intermediate EC point.
206+
*/
207+
BIP38.newAddressFromIntermediate = function(intermediate, compressed) {
208+
var result = {};
209+
210+
// decode IPS
211+
var x = Bitcoin.Base58.decode(intermediate);
212+
//TODO if(x.slice(49, 4) !== Bitcoin.Util.dsha256(x.slice(0,49)).slice(0,4)) {
213+
// throw new Error("Invalid intermediate passphrase string");
214+
//}
215+
var noNumbers = (x[7] === 0x53);
216+
var ownerEntropy = x.slice(8, 8+8);
217+
var passpoint = x.slice(16, 16+33);
218+
219+
// 1) Set flagbyte.
220+
// set bit 0x20 for compressed key
221+
// set bit 0x04 if ownerentropy contains a value for lotsequence
222+
var flagByte = (compressed? 0x20 : 0x00) | (noNumbers? 0x00 : 0x04);
223+
224+
// 2) Generate 24 random bytes, call this seedb.
225+
var seedB = new Array(24);
226+
rng.nextBytes(seedB);
227+
228+
// Take SHA256(SHA256(seedb)) to yield 32 bytes, call this factorb.
229+
var factorB = Bitcoin.Util.dsha256(seedB);
230+
231+
// 3) ECMultiply passpoint by factorb. Use the resulting EC point as a public key and hash it into a Bitcoin
232+
// address using either compressed or uncompressed public key methodology (specify which methodology is used
233+
// inside flagbyte). This is the generated Bitcoin address, call it generatedAddress.
234+
var ec = ecparams.getCurve();
235+
var generatedPoint = ec.decodePointHex(Crypto.util.bytesToHex(passpoint));
236+
var generatedBytes = generatedPoint.multiply(BigInteger.fromByteArrayUnsigned(factorB)).getEncoded(compressed);
237+
var generatedAddress = new Bitcoin.Address(Bitcoin.Util.sha256ripe160(generatedBytes));
238+
239+
// 4) Take the first four bytes of SHA256(SHA256(generatedaddress)) and call it addresshash.
240+
var addressHash = Bitcoin.Util.dsha256(generatedAddress.toString()).slice(0,4);
241+
242+
// 5) Now we will encrypt seedb. Derive a second key from passpoint using scrypt
243+
var derivedBytes = scrypt(passpoint, addressHash.concat(ownerEntropy), 1024, 1, 1, 64);
244+
245+
// 6) Do AES256Encrypt(seedb[0...15]] xor derivedhalf1[0...15], derivedhalf2), call the 16-byte result encryptedpart1
246+
for(var i = 0; i < 16; ++i) {
247+
seedB[i] ^= derivedBytes[i];
248+
}
249+
var encryptedPart1 = Crypto.AES.encrypt(seedB.slice(0,16), derivedBytes.slice(32), AES_opts);
250+
251+
// 7) Do AES256Encrypt((encryptedpart1[8...15] + seedb[16...23]) xor derivedhalf1[16...31], derivedhalf2), call the 16-byte result encryptedseedb.
252+
var message2 = encryptedPart1.slice(8, 8+8).concat(seedB.slice(16, 16+8));
253+
for(var i = 0; i < 16; ++i) {
254+
message2[i] ^= derivedBytes[i+16];
255+
}
256+
var encryptedSeedB = Crypto.AES.encrypt(message2, derivedBytes.slice(32), AES_opts);
257+
258+
// 0x01 0x43 + flagbyte + addresshash + ownerentropy + encryptedpart1[0...7] + encryptedPart2
259+
var encryptedKey = [ 0x01, 0x43, flagByte ].concat(addressHash).concat(ownerEntropy).concat(encryptedPart1.slice(0,8)).concat(encryptedSeedB);
260+
261+
// base58check encode
262+
encryptedKey = encryptedKey.concat(Bitcoin.Util.dsha256(encryptedKey).slice(0,4));
263+
264+
result.address = generatedAddress;
265+
result.bip38PrivateKey = Bitcoin.Base58.encode(encryptedKey);
266+
return result;
267+
};
268+
154269
/**
155270
* Detects keys encrypted according to BIP-38 (58 base58 characters starting with 6P)
156271
*/

src/jsbn/ec.js

+21-2
Original file line numberDiff line numberDiff line change
@@ -282,15 +282,33 @@ function curveFpFromBigInteger(x) {
282282
return new ECFieldElementFp(this.q, x);
283283
}
284284

285+
function curveFpDecompressPoint(yOdd, X) {
286+
if(this.q.mod(BigInteger.valueOf(4)).equals(BigInteger.valueOf(3))) {
287+
// y^2 = x^3 + ax^2 + b, so we need to perform sqrt to recover y
288+
var ySquared = X.multiply(X.square().add(this.a)).add(this.b);
289+
290+
// sqrt(a) = a^((q-1)/4) if q = 3 mod 4
291+
var Y = ySquared.x.modPow(this.q.add(BigInteger.ONE).divide(BigInteger.valueOf(4)), this.q);
292+
293+
if(Y.testBit(0) !== yOdd) {
294+
Y = this.q.subtract(Y);
295+
}
296+
297+
return new ECPointFp(this, X, this.fromBigInteger(Y));
298+
} else {
299+
throw new Error("point decompression only implements sqrt for q = 3 mod 4");
300+
}
301+
};
302+
285303
// for now, work with hex strings because they're easier in JS
286304
function curveFpDecodePointHex(s) {
287305
switch(parseInt(s.substr(0,2), 16)) { // first byte
288306
case 0:
289307
return this.infinity;
290308
case 2:
309+
return this.decompressPoint(false, this.fromBigInteger(new BigInteger(s.substr(2), 16)));
291310
case 3:
292-
// point compression not supported yet
293-
return null;
311+
return this.decompressPoint(true, this.fromBigInteger(new BigInteger(s.substr(2), 16)));
294312
case 4:
295313
case 6:
296314
case 7:
@@ -314,3 +332,4 @@ ECCurveFp.prototype.equals = curveFpEquals;
314332
ECCurveFp.prototype.getInfinity = curveFpGetInfinity;
315333
ECCurveFp.prototype.fromBigInteger = curveFpFromBigInteger;
316334
ECCurveFp.prototype.decodePointHex = curveFpDecodePointHex;
335+
ECCurveFp.prototype.decompressPoint = curveFpDecompressPoint;

test/test.js

+29
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,32 @@ test("Compression, no EC multiply #2", function () {
166166
equal(Bitcoin.ECKey.decodeEncryptedFormat(encrypted, pw).getWalletImportFormat(), wif, "Key decrypted successfully.");
167167
});
168168

169+
170+
// NOTE: Testing BIP38 keys generated with EC-multiply is difficult due to their non-deterministic nature.
171+
// This test only verifies that a new address/key can be generated with a password and later decrypted
172+
// with the same password.
173+
174+
test("EC multiply, no compression, no lot/sequence numbers", function () {
175+
expect(2);
176+
177+
var pw = "TestingOneTwoThree";
178+
var intermediate = Bitcoin.BIP38.generateIntermediate(pw);
179+
var encryptedKey = Bitcoin.BIP38.newAddressFromIntermediate(intermediate, false);
180+
var decryptedKey = Bitcoin.BIP38.decode(encryptedKey.bip38PrivateKey, pw);
181+
182+
ok(Bitcoin.ECKey.isBIP38Format(encryptedKey.bip38PrivateKey), "New EC-multiplied key appears to be valid BIP38 format.");
183+
equal(encryptedKey.address.toString(), decryptedKey.getAddress().toString(), "Address of new EC-multiplied key matches address after decryption with password.");
184+
});
185+
186+
test("EC multiply, no compression, lot/sequence numbers", function () {
187+
expect(2);
188+
189+
var pw = "MOLON LABE", lot = 263183, seq = 1;
190+
var intermediate = Bitcoin.BIP38.generateIntermediate(pw, lot, seq);
191+
var encryptedKey = Bitcoin.BIP38.newAddressFromIntermediate(intermediate, false);
192+
var decryptedKey = Bitcoin.BIP38.decode(encryptedKey.bip38PrivateKey, pw);
193+
194+
ok(Bitcoin.ECKey.isBIP38Format(encryptedKey.bip38PrivateKey), "New EC-multiplied key appears to be valid BIP38 format.");
195+
equal(encryptedKey.address.toString(), decryptedKey.getAddress().toString(), "Address of new EC-multiplied key matches address after decryption with password.");
196+
});
197+

0 commit comments

Comments
 (0)