]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/BIP39.git/blame - src/js/index.js
BIP32 Root Key can be specified by user
[perso/Immae/Projets/Cryptomonnaies/BIP39.git] / src / js / index.js
CommitLineData
ebd8d4e8
IC
1(function() {
2
3 var mnemonic = new Mnemonic("english");
3e0ed16a 4 var seed = null
ebd8d4e8
IC
5 var bip32RootKey = null;
6 var bip32ExtendedKey = null;
1759e5e8 7 var network = bitcoin.networks.bitcoin;
ebd8d4e8
IC
8 var addressRowTemplate = $("#address-row-template");
9
700901cd
IC
10 var showIndex = true;
11 var showAddress = true;
12 var showPrivKey = true;
13
ebd8d4e8 14 var phraseChangeTimeoutEvent = null;
efe41586 15 var rootKeyChangedTimeoutEvent = null;
ebd8d4e8
IC
16
17 var DOM = {};
d6cedc94
IC
18 DOM.network = $(".network");
19 DOM.phraseNetwork = $("#network-phrase");
ebd8d4e8 20 DOM.phrase = $(".phrase");
1abcc511 21 DOM.passphrase = $(".passphrase");
ebd8d4e8 22 DOM.generate = $(".generate");
3e0ed16a 23 DOM.seed = $(".seed");
ebd8d4e8
IC
24 DOM.rootKey = $(".root-key");
25 DOM.extendedPrivKey = $(".extended-priv-key");
26 DOM.extendedPubKey = $(".extended-pub-key");
d6cedc94
IC
27 DOM.bip32tab = $("#bip32-tab");
28 DOM.bip44tab = $("#bip44-tab");
29 DOM.bip32panel = $("#bip32");
30 DOM.bip44panel = $("#bip44");
ebd8d4e8
IC
31 DOM.bip32path = $("#bip32-path");
32 DOM.bip44path = $("#bip44-path");
33 DOM.bip44purpose = $("#bip44 .purpose");
34 DOM.bip44coin = $("#bip44 .coin");
35 DOM.bip44account = $("#bip44 .account");
36 DOM.bip44change = $("#bip44 .change");
37 DOM.strength = $(".strength");
38 DOM.addresses = $(".addresses");
39 DOM.rowsToAdd = $(".rows-to-add");
40 DOM.more = $(".more");
41 DOM.feedback = $(".feedback");
42 DOM.tab = $(".derivation-type a");
43 DOM.indexToggle = $(".index-toggle");
44 DOM.addressToggle = $(".address-toggle");
45 DOM.privateKeyToggle = $(".private-key-toggle");
46
ebd8d4e8
IC
47 function init() {
48 // Events
d6cedc94 49 DOM.network.on("change", networkChanged);
a19a5498
IC
50 DOM.phrase.on("input", delayedPhraseChanged);
51 DOM.passphrase.on("input", delayedPhraseChanged);
ebd8d4e8
IC
52 DOM.generate.on("click", generateClicked);
53 DOM.more.on("click", showMore);
efe41586
IC
54 DOM.rootKey.on("input", delayedRootKeyChanged);
55 DOM.bip32path.on("input", calcForDerivationPath);
56 DOM.bip44purpose.on("input", calcForDerivationPath);
57 DOM.bip44coin.on("input", calcForDerivationPath);
58 DOM.bip44account.on("input", calcForDerivationPath);
59 DOM.bip44change.on("input", calcForDerivationPath);
60 DOM.tab.on("shown.bs.tab", calcForDerivationPath);
ebd8d4e8
IC
61 DOM.indexToggle.on("click", toggleIndexes);
62 DOM.addressToggle.on("click", toggleAddresses);
63 DOM.privateKeyToggle.on("click", togglePrivateKeys);
64 disableForms();
65 hidePending();
66 hideValidationError();
7f15cb6e 67 populateNetworkSelect();
ebd8d4e8
IC
68 }
69
70 // Event handlers
71
d6cedc94 72 function networkChanged(e) {
7a995731 73 var network = e.target.value;
7f15cb6e 74 networks[network].onSelect();
efe41586 75 displayBip32Info();
d6cedc94
IC
76 }
77
ebd8d4e8
IC
78 function delayedPhraseChanged() {
79 hideValidationError();
80 showPending();
81 if (phraseChangeTimeoutEvent != null) {
82 clearTimeout(phraseChangeTimeoutEvent);
83 }
84 phraseChangeTimeoutEvent = setTimeout(phraseChanged, 400);
85 }
86
87 function phraseChanged() {
88 showPending();
89 hideValidationError();
90 // Get the mnemonic phrase
91 var phrase = DOM.phrase.val();
92 var errorText = findPhraseErrors(phrase);
93 if (errorText) {
94 showValidationError(errorText);
95 return;
96 }
efe41586
IC
97 // Calculate and display
98 var passphrase = DOM.passphrase.val();
99 calcBip32RootKeyFromSeed(phrase, passphrase);
100 calcForDerivationPath();
101 hidePending();
102 }
103
104 function delayedRootKeyChanged() {
105 // Warn if there is an existing mnemonic or passphrase.
106 if (DOM.phrase.val().length > 0 || DOM.passphrase.val().length > 0) {
107 if (!confirm("This will clear existing mnemonic and passphrase")) {
108 DOM.rootKey.val(bip32RootKey);
109 return
110 }
111 }
112 hideValidationError();
113 showPending();
114 // Clear existing mnemonic and passphrase
115 DOM.phrase.val("");
116 DOM.passphrase.val("");
117 seed = null;
118 if (rootKeyChangedTimeoutEvent != null) {
119 clearTimeout(rootKeyChangedTimeoutEvent);
120 }
121 rootKeyChangedTimeoutEvent = setTimeout(rootKeyChanged, 400);
122 }
123
124 function rootKeyChanged() {
125 showPending();
126 hideValidationError();
127 // Validate the root key TODO
128 var rootKeyBase58 = DOM.rootKey.val();
129 var errorText = validateRootKey(rootKeyBase58);
130 if (errorText) {
131 showValidationError(errorText);
132 return;
133 }
134 // Calculate and display
135 calcBip32RootKeyFromBase58(rootKeyBase58);
136 calcForDerivationPath();
137 hidePending();
138 }
139
140 function calcForDerivationPath() {
141 showPending();
142 hideValidationError();
ebd8d4e8 143 // Get the derivation path
38523d36
IC
144 var derivationPath = getDerivationPath();
145 var errorText = findDerivationPathErrors(derivationPath);
ebd8d4e8
IC
146 if (errorText) {
147 showValidationError(errorText);
148 return;
149 }
efe41586 150 calcBip32ExtendedKey(derivationPath);
ebd8d4e8
IC
151 displayBip32Info();
152 hidePending();
153 }
154
155 function generateClicked() {
156 clearDisplay();
157 showPending();
158 setTimeout(function() {
159 var phrase = generateRandomPhrase();
160 if (!phrase) {
161 return;
162 }
163 phraseChanged();
164 }, 50);
165 }
166
ebd8d4e8 167 function toggleIndexes() {
700901cd 168 showIndex = !showIndex;
ebd8d4e8
IC
169 $("td.index span").toggleClass("invisible");
170 }
171
172 function toggleAddresses() {
700901cd 173 showAddress = !showAddress;
ebd8d4e8
IC
174 $("td.address span").toggleClass("invisible");
175 }
176
177 function togglePrivateKeys() {
700901cd 178 showPrivKey = !showPrivKey;
ebd8d4e8
IC
179 $("td.privkey span").toggleClass("invisible");
180 }
181
182 // Private methods
183
184 function generateRandomPhrase() {
185 if (!hasStrongRandom()) {
186 var errorText = "This browser does not support strong randomness";
187 showValidationError(errorText);
188 return;
189 }
190 var numWords = parseInt(DOM.strength.val());
ebd8d4e8
IC
191 var strength = numWords / 3 * 32;
192 var words = mnemonic.generate(strength);
193 DOM.phrase.val(words);
194 return words;
195 }
196
efe41586 197 function calcBip32RootKeyFromSeed(phrase, passphrase) {
3e0ed16a 198 seed = mnemonic.toSeed(phrase, passphrase);
1759e5e8 199 bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network);
efe41586
IC
200 }
201
202 function calcBip32RootKeyFromBase58(rootKeyBase58) {
203 bip32RootKey = bitcoin.HDNode.fromBase58(rootKeyBase58);
204 }
205
206 function calcBip32ExtendedKey(path) {
ebd8d4e8
IC
207 bip32ExtendedKey = bip32RootKey;
208 // Derive the key from the path
209 var pathBits = path.split("/");
210 for (var i=0; i<pathBits.length; i++) {
211 var bit = pathBits[i];
212 var index = parseInt(bit);
213 if (isNaN(index)) {
214 continue;
215 }
216 var hardened = bit[bit.length-1] == "'";
217 if (hardened) {
218 bip32ExtendedKey = bip32ExtendedKey.deriveHardened(index);
219 }
220 else {
221 bip32ExtendedKey = bip32ExtendedKey.derive(index);
222 }
223 }
224 }
225
226 function showValidationError(errorText) {
227 DOM.feedback
228 .text(errorText)
229 .show();
230 }
231
232 function hideValidationError() {
233 DOM.feedback
234 .text("")
235 .hide();
236 }
237
238 function findPhraseErrors(phrase) {
239 // TODO make this right
240 // Preprocess the words
783981de 241 phrase = mnemonic.normalizeString(phrase);
ebd8d4e8
IC
242 var parts = phrase.split(" ");
243 var proper = [];
244 for (var i=0; i<parts.length; i++) {
245 var part = parts[i];
246 if (part.length > 0) {
247 // TODO check that lowercasing is always valid to do
248 proper.push(part.toLowerCase());
249 }
250 }
ebd8d4e8 251 var properPhrase = proper.join(' ');
563e401a
IC
252 // Check each word
253 for (var i=0; i<proper.length; i++) {
254 var word = proper[i];
255 if (WORDLISTS["english"].indexOf(word) == -1) {
256 console.log("Finding closest match to " + word);
257 var nearestWord = findNearestWord(word);
258 return word + " not in wordlist, did you mean " + nearestWord + "?";
259 }
260 }
ebd8d4e8
IC
261 // Check the words are valid
262 var isValid = mnemonic.check(properPhrase);
263 if (!isValid) {
264 return "Invalid mnemonic";
265 }
266 return false;
267 }
268
efe41586
IC
269 function validateRootKey(rootKeyBase58) {
270 try {
271 bitcoin.HDNode.fromBase58(rootKeyBase58);
272 }
273 catch (e) {
274 return "Invalid root key";
275 }
276 return "";
277 }
278
38523d36
IC
279 function getDerivationPath() {
280 if (DOM.bip44tab.hasClass("active")) {
281 var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44);
282 var coin = parseIntNoNaN(DOM.bip44coin.val(), 0);
283 var account = parseIntNoNaN(DOM.bip44account.val(), 0);
284 var change = parseIntNoNaN(DOM.bip44change.val(), 0);
285 var path = "m/";
286 path += purpose + "'/";
287 path += coin + "'/";
288 path += account + "'/";
289 path += change;
290 DOM.bip44path.val(path);
291 var derivationPath = DOM.bip44path.val();
292 console.log("Using derivation path from BIP44 tab: " + derivationPath);
293 return derivationPath;
294 }
295 else if (DOM.bip32tab.hasClass("active")) {
296 var derivationPath = DOM.bip32path.val();
297 console.log("Using derivation path from BIP32 tab: " + derivationPath);
298 return derivationPath;
299 }
300 else {
301 console.log("Unknown derivation path");
302 }
303 }
304
ebd8d4e8 305 function findDerivationPathErrors(path) {
30c9e79d
IC
306 // TODO is not perfect but is better than nothing
307 // Inspired by
308 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
309 // and
310 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys
311 var maxDepth = 255; // TODO verify this!!
312 var maxIndexValue = Math.pow(2, 31); // TODO verify this!!
313 if (path[0] != "m") {
314 return "First character must be 'm'";
315 }
316 if (path.length > 1) {
317 if (path[1] != "/") {
318 return "Separator must be '/'";
319 }
320 var indexes = path.split("/");
321 if (indexes.length > maxDepth) {
322 return "Derivation depth is " + indexes.length + ", must be less than " + maxDepth;
323 }
324 for (var depth = 1; depth<indexes.length; depth++) {
325 var index = indexes[depth];
326 var invalidChars = index.replace(/^[0-9]+'?$/g, "")
327 if (invalidChars.length > 0) {
328 return "Invalid characters " + invalidChars + " found at depth " + depth;
329 }
330 var indexValue = parseInt(index.replace("'", ""));
331 if (isNaN(depth)) {
332 return "Invalid number at depth " + depth;
333 }
334 if (indexValue > maxIndexValue) {
335 return "Value of " + indexValue + " at depth " + depth + " must be less than " + maxIndexValue;
336 }
337 }
338 }
ebd8d4e8
IC
339 return false;
340 }
341
342 function displayBip32Info() {
343 // Display the key
3e0ed16a 344 DOM.seed.val(seed);
ebd8d4e8
IC
345 var rootKey = bip32RootKey.toBase58();
346 DOM.rootKey.val(rootKey);
347 var extendedPrivKey = bip32ExtendedKey.toBase58();
348 DOM.extendedPrivKey.val(extendedPrivKey);
349 var extendedPubKey = bip32ExtendedKey.toBase58(false);
350 DOM.extendedPubKey.val(extendedPubKey);
351 // Display the addresses and privkeys
352 clearAddressesList();
353 displayAddresses(0, 20);
354 }
355
356 function displayAddresses(start, total) {
357 for (var i=0; i<total; i++) {
a8c45487
IC
358 var index = i + start;
359 new TableRow(index);
ebd8d4e8
IC
360 }
361 }
362
a8c45487
IC
363 function TableRow(index) {
364
365 function init() {
366 calculateValues();
367 }
368
369 function calculateValues() {
370 setTimeout(function() {
371 var key = bip32ExtendedKey.derive(index);
372 var address = key.getAddress().toString();
373 var privkey = key.privKey.toWIF(network);
38523d36
IC
374 var indexText = getDerivationPath() + "/" + index;
375 addAddressToList(indexText, address, privkey);
a8c45487
IC
376 }, 50)
377 }
378
379 init();
380
381 }
382
ebd8d4e8
IC
383 function showMore() {
384 var start = DOM.addresses.children().length;
385 var rowsToAdd = parseInt(DOM.rowsToAdd.val());
386 if (isNaN(rowsToAdd)) {
387 rowsToAdd = 20;
388 DOM.rowsToAdd.val("20");
389 }
390 if (rowsToAdd > 200) {
391 var msg = "Generating " + rowsToAdd + " rows could take a while. ";
392 msg += "Do you want to continue?";
393 if (!confirm(msg)) {
394 return;
395 }
396 }
ebd8d4e8 397 displayAddresses(start, rowsToAdd);
ebd8d4e8
IC
398 }
399
400 function clearDisplay() {
401 clearAddressesList();
402 clearKey();
403 hideValidationError();
404 }
405
406 function clearAddressesList() {
407 DOM.addresses.empty();
408 }
409
410 function clearKey() {
411 DOM.rootKey.val("");
412 DOM.extendedPrivKey.val("");
413 DOM.extendedPubKey.val("");
414 }
415
38523d36 416 function addAddressToList(indexText, address, privkey) {
ebd8d4e8 417 var row = $(addressRowTemplate.html());
700901cd
IC
418 // Elements
419 var indexCell = row.find(".index span");
420 var addressCell = row.find(".address span");
421 var privkeyCell = row.find(".privkey span");
422 // Content
ae30fed8 423 indexCell.text(indexText);
700901cd
IC
424 addressCell.text(address);
425 privkeyCell.text(privkey);
426 // Visibility
427 if (!showIndex) {
428 indexCell.addClass("invisible");
429 }
430 if (!showAddress) {
431 addressCell.addClass("invisible");
432 }
433 if (!showPrivKey) {
6d628db7 434 privkeyCell.addClass("invisible");
700901cd 435 }
ebd8d4e8
IC
436 DOM.addresses.append(row);
437 }
438
439 function hasStrongRandom() {
440 return 'crypto' in window && window['crypto'] !== null;
441 }
442
443 function disableForms() {
444 $("form").on("submit", function(e) {
445 e.preventDefault();
446 });
447 }
448
ebd8d4e8
IC
449 function parseIntNoNaN(val, defaultVal) {
450 var v = parseInt(val);
451 if (isNaN(v)) {
452 return defaultVal;
453 }
454 return v;
455 }
456
457 function showPending() {
458 DOM.feedback
459 .text("Calculating...")
460 .show();
461 }
462
563e401a
IC
463 function findNearestWord(word) {
464 var words = WORDLISTS["english"];
465 var minDistance = 99;
466 var closestWord = words[0];
467 for (var i=0; i<words.length; i++) {
468 var comparedTo = words[i];
469 var distance = Levenshtein.get(word, comparedTo);
470 if (distance < minDistance) {
471 closestWord = comparedTo;
472 minDistance = distance;
473 }
474 }
475 return closestWord;
476 }
477
ebd8d4e8
IC
478 function hidePending() {
479 DOM.feedback
480 .text("")
481 .hide();
482 }
483
7f15cb6e
IC
484 function populateNetworkSelect() {
485 for (var i=0; i<networks.length; i++) {
486 var network = networks[i];
487 var option = $("<option>");
488 option.attr("value", i);
489 option.text(network.name);
490 DOM.phraseNetwork.append(option);
491 }
492 }
493
494 var networks = [
495 {
7a995731
IC
496 name: "Bitcoin",
497 onSelect: function() {
1759e5e8 498 network = bitcoin.networks.bitcoin;
7a995731 499 DOM.bip44coin.val(0);
7a995731
IC
500 },
501 },
7f15cb6e 502 {
7a995731
IC
503 name: "Bitcoin Testnet",
504 onSelect: function() {
1759e5e8 505 network = bitcoin.networks.testnet;
7a995731 506 DOM.bip44coin.val(1);
7a995731
IC
507 },
508 },
7f15cb6e 509 {
7a995731
IC
510 name: "Litecoin",
511 onSelect: function() {
1759e5e8 512 network = bitcoin.networks.litecoin;
7a995731
IC
513 DOM.bip44coin.val(2);
514 },
515 },
7f15cb6e 516 {
7a995731
IC
517 name: "Dogecoin",
518 onSelect: function() {
1759e5e8 519 network = bitcoin.networks.dogecoin;
7a995731
IC
520 DOM.bip44coin.val(3);
521 },
522 },
e3a9508c
IC
523 {
524 name: "ShadowCash",
525 onSelect: function() {
526 network = bitcoin.networks.shadow;
527 DOM.bip44coin.val(35);
528 },
529 },
530 {
531 name: "ShadowCash Testnet",
532 onSelect: function() {
533 network = bitcoin.networks.shadowtn;
534 DOM.bip44coin.val(1);
535 },
536 },
a3baa26e
IC
537 {
538 name: "Viacoin",
539 onSelect: function() {
540 network = bitcoin.networks.viacoin;
541 DOM.bip44coin.val(14);
542 },
543 },
544 {
545 name: "Viacoin Testnet",
546 onSelect: function() {
547 network = bitcoin.networks.viacointestnet;
548 DOM.bip44coin.val(1);
549 },
550 },
551 {
552 name: "Jumbucks",
553 onSelect: function() {
554 network = bitcoin.networks.jumbucks;
555 DOM.bip44coin.val(26);
556 },
557 },
5c434a8a
CM
558 {
559 name: "CLAM",
560 onSelect: function() {
561 network = bitcoin.networks.clam;
562 DOM.bip44coin.val(23);
563 },
564 },
7f15cb6e 565 ]
7a995731 566
ebd8d4e8
IC
567 init();
568
569})();