]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/BIP39.git/blob - src/js/index.js
Table uses monospace font.
[perso/Immae/Projets/Cryptomonnaies/BIP39.git] / src / js / index.js
1 (function() {
2
3 // mnemonics is populated as required by getLanguage
4 var mnemonics = { "english": new Mnemonic("english") };
5 var mnemonic = mnemonics["english"];
6 var seed = null
7 var bip32RootKey = null;
8 var bip32ExtendedKey = null;
9 var network = bitcoin.networks.bitcoin;
10 var addressRowTemplate = $("#address-row-template");
11
12 var showIndex = true;
13 var showAddress = true;
14 var showPubKey = true;
15 var showPrivKey = true;
16
17 var phraseChangeTimeoutEvent = null;
18 var rootKeyChangedTimeoutEvent = null;
19
20 var DOM = {};
21 DOM.network = $(".network");
22 DOM.phraseNetwork = $("#network-phrase");
23 DOM.phrase = $(".phrase");
24 DOM.passphrase = $(".passphrase");
25 DOM.generate = $(".generate");
26 DOM.seed = $(".seed");
27 DOM.rootKey = $(".root-key");
28 DOM.extendedPrivKey = $(".extended-priv-key");
29 DOM.extendedPubKey = $(".extended-pub-key");
30 DOM.bip32tab = $("#bip32-tab");
31 DOM.bip44tab = $("#bip44-tab");
32 DOM.bip32panel = $("#bip32");
33 DOM.bip44panel = $("#bip44");
34 DOM.bip32path = $("#bip32-path");
35 DOM.bip44path = $("#bip44-path");
36 DOM.bip44purpose = $("#bip44 .purpose");
37 DOM.bip44coin = $("#bip44 .coin");
38 DOM.bip44account = $("#bip44 .account");
39 DOM.bip44change = $("#bip44 .change");
40 DOM.strength = $(".strength");
41 DOM.hardenedAddresses = $(".hardened-addresses");
42 DOM.addresses = $(".addresses");
43 DOM.rowsToAdd = $(".rows-to-add");
44 DOM.more = $(".more");
45 DOM.feedback = $(".feedback");
46 DOM.tab = $(".derivation-type a");
47 DOM.indexToggle = $(".index-toggle");
48 DOM.addressToggle = $(".address-toggle");
49 DOM.publicKeyToggle = $(".public-key-toggle");
50 DOM.privateKeyToggle = $(".private-key-toggle");
51 DOM.languages = $(".languages a");
52
53 function init() {
54 // Events
55 DOM.network.on("change", networkChanged);
56 DOM.phrase.on("input", delayedPhraseChanged);
57 DOM.passphrase.on("input", delayedPhraseChanged);
58 DOM.generate.on("click", generateClicked);
59 DOM.more.on("click", showMore);
60 DOM.rootKey.on("input", delayedRootKeyChanged);
61 DOM.bip32path.on("input", calcForDerivationPath);
62 DOM.bip44purpose.on("input", calcForDerivationPath);
63 DOM.bip44coin.on("input", calcForDerivationPath);
64 DOM.bip44account.on("input", calcForDerivationPath);
65 DOM.bip44change.on("input", calcForDerivationPath);
66 DOM.tab.on("shown.bs.tab", calcForDerivationPath);
67 DOM.hardenedAddresses.on("change", calcForDerivationPath);
68 DOM.indexToggle.on("click", toggleIndexes);
69 DOM.addressToggle.on("click", toggleAddresses);
70 DOM.publicKeyToggle.on("click", togglePublicKeys);
71 DOM.privateKeyToggle.on("click", togglePrivateKeys);
72 DOM.languages.on("click", languageChanged);
73 disableForms();
74 hidePending();
75 hideValidationError();
76 populateNetworkSelect();
77 }
78
79 // Event handlers
80
81 function networkChanged(e) {
82 var networkIndex = e.target.value;
83 networks[networkIndex].onSelect();
84 if (seed != null) {
85 phraseChanged();
86 }
87 else {
88 rootKeyChanged();
89 }
90 }
91
92 function delayedPhraseChanged() {
93 hideValidationError();
94 showPending();
95 if (phraseChangeTimeoutEvent != null) {
96 clearTimeout(phraseChangeTimeoutEvent);
97 }
98 phraseChangeTimeoutEvent = setTimeout(phraseChanged, 400);
99 }
100
101 function phraseChanged() {
102 showPending();
103 hideValidationError();
104 setMnemonicLanguage();
105 // Get the mnemonic phrase
106 var phrase = DOM.phrase.val();
107 var errorText = findPhraseErrors(phrase);
108 if (errorText) {
109 showValidationError(errorText);
110 return;
111 }
112 // Calculate and display
113 var passphrase = DOM.passphrase.val();
114 calcBip32RootKeyFromSeed(phrase, passphrase);
115 calcForDerivationPath();
116 hidePending();
117 }
118
119 function delayedRootKeyChanged() {
120 // Warn if there is an existing mnemonic or passphrase.
121 if (DOM.phrase.val().length > 0 || DOM.passphrase.val().length > 0) {
122 if (!confirm("This will clear existing mnemonic and passphrase")) {
123 DOM.rootKey.val(bip32RootKey);
124 return
125 }
126 }
127 hideValidationError();
128 showPending();
129 // Clear existing mnemonic and passphrase
130 DOM.phrase.val("");
131 DOM.passphrase.val("");
132 seed = null;
133 if (rootKeyChangedTimeoutEvent != null) {
134 clearTimeout(rootKeyChangedTimeoutEvent);
135 }
136 rootKeyChangedTimeoutEvent = setTimeout(rootKeyChanged, 400);
137 }
138
139 function rootKeyChanged() {
140 showPending();
141 hideValidationError();
142 // Validate the root key TODO
143 var rootKeyBase58 = DOM.rootKey.val();
144 var errorText = validateRootKey(rootKeyBase58);
145 if (errorText) {
146 showValidationError(errorText);
147 return;
148 }
149 // Calculate and display
150 calcBip32RootKeyFromBase58(rootKeyBase58);
151 calcForDerivationPath();
152 hidePending();
153 }
154
155 function calcForDerivationPath() {
156 showPending();
157 hideValidationError();
158 // Get the derivation path
159 var derivationPath = getDerivationPath();
160 var errorText = findDerivationPathErrors(derivationPath);
161 if (errorText) {
162 showValidationError(errorText);
163 return;
164 }
165 calcBip32ExtendedKey(derivationPath);
166 displayBip32Info();
167 hidePending();
168 }
169
170 function generateClicked() {
171 clearDisplay();
172 showPending();
173 setTimeout(function() {
174 setMnemonicLanguage();
175 var phrase = generateRandomPhrase();
176 if (!phrase) {
177 return;
178 }
179 phraseChanged();
180 }, 50);
181 }
182
183 function languageChanged() {
184 setTimeout(function() {
185 setMnemonicLanguage();
186 if (DOM.phrase.val().length > 0) {
187 var newPhrase = convertPhraseToNewLanguage();
188 DOM.phrase.val(newPhrase);
189 phraseChanged();
190 }
191 else {
192 DOM.generate.trigger("click");
193 }
194 }, 50);
195 }
196
197 function toggleIndexes() {
198 showIndex = !showIndex;
199 $("td.index span").toggleClass("invisible");
200 }
201
202 function toggleAddresses() {
203 showAddress = !showAddress;
204 $("td.address span").toggleClass("invisible");
205 }
206
207 function togglePublicKeys() {
208 showPubKey = !showPubKey;
209 $("td.pubkey span").toggleClass("invisible");
210 }
211
212 function togglePrivateKeys() {
213 showPrivKey = !showPrivKey;
214 $("td.privkey span").toggleClass("invisible");
215 }
216
217 // Private methods
218
219 function generateRandomPhrase() {
220 if (!hasStrongRandom()) {
221 var errorText = "This browser does not support strong randomness";
222 showValidationError(errorText);
223 return;
224 }
225 var numWords = parseInt(DOM.strength.val());
226 var strength = numWords / 3 * 32;
227 var words = mnemonic.generate(strength);
228 DOM.phrase.val(words);
229 return words;
230 }
231
232 function calcBip32RootKeyFromSeed(phrase, passphrase) {
233 seed = mnemonic.toSeed(phrase, passphrase);
234 bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network);
235 }
236
237 function calcBip32RootKeyFromBase58(rootKeyBase58) {
238 bip32RootKey = bitcoin.HDNode.fromBase58(rootKeyBase58, network);
239 }
240
241 function calcBip32ExtendedKey(path) {
242 bip32ExtendedKey = bip32RootKey;
243 // Derive the key from the path
244 var pathBits = path.split("/");
245 for (var i=0; i<pathBits.length; i++) {
246 var bit = pathBits[i];
247 var index = parseInt(bit);
248 if (isNaN(index)) {
249 continue;
250 }
251 var hardened = bit[bit.length-1] == "'";
252 if (hardened) {
253 bip32ExtendedKey = bip32ExtendedKey.deriveHardened(index);
254 }
255 else {
256 bip32ExtendedKey = bip32ExtendedKey.derive(index);
257 }
258 }
259 }
260
261 function showValidationError(errorText) {
262 DOM.feedback
263 .text(errorText)
264 .show();
265 }
266
267 function hideValidationError() {
268 DOM.feedback
269 .text("")
270 .hide();
271 }
272
273 function findPhraseErrors(phrase) {
274 // TODO make this right
275 // Preprocess the words
276 phrase = mnemonic.normalizeString(phrase);
277 var words = phraseToWordArray(phrase);
278 // Check each word
279 for (var i=0; i<words.length; i++) {
280 var word = words[i];
281 var language = getLanguage();
282 if (WORDLISTS[language].indexOf(word) == -1) {
283 console.log("Finding closest match to " + word);
284 var nearestWord = findNearestWord(word);
285 return word + " not in wordlist, did you mean " + nearestWord + "?";
286 }
287 }
288 // Check the words are valid
289 var properPhrase = wordArrayToPhrase(words);
290 var isValid = mnemonic.check(properPhrase);
291 if (!isValid) {
292 return "Invalid mnemonic";
293 }
294 return false;
295 }
296
297 function validateRootKey(rootKeyBase58) {
298 try {
299 bitcoin.HDNode.fromBase58(rootKeyBase58);
300 }
301 catch (e) {
302 return "Invalid root key";
303 }
304 return "";
305 }
306
307 function getDerivationPath() {
308 if (DOM.bip44tab.hasClass("active")) {
309 var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44);
310 var coin = parseIntNoNaN(DOM.bip44coin.val(), 0);
311 var account = parseIntNoNaN(DOM.bip44account.val(), 0);
312 var change = parseIntNoNaN(DOM.bip44change.val(), 0);
313 var path = "m/";
314 path += purpose + "'/";
315 path += coin + "'/";
316 path += account + "'/";
317 path += change;
318 DOM.bip44path.val(path);
319 var derivationPath = DOM.bip44path.val();
320 console.log("Using derivation path from BIP44 tab: " + derivationPath);
321 return derivationPath;
322 }
323 else if (DOM.bip32tab.hasClass("active")) {
324 var derivationPath = DOM.bip32path.val();
325 console.log("Using derivation path from BIP32 tab: " + derivationPath);
326 return derivationPath;
327 }
328 else {
329 console.log("Unknown derivation path");
330 }
331 }
332
333 function findDerivationPathErrors(path) {
334 // TODO is not perfect but is better than nothing
335 // Inspired by
336 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
337 // and
338 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys
339 var maxDepth = 255; // TODO verify this!!
340 var maxIndexValue = Math.pow(2, 31); // TODO verify this!!
341 if (path[0] != "m") {
342 return "First character must be 'm'";
343 }
344 if (path.length > 1) {
345 if (path[1] != "/") {
346 return "Separator must be '/'";
347 }
348 var indexes = path.split("/");
349 if (indexes.length > maxDepth) {
350 return "Derivation depth is " + indexes.length + ", must be less than " + maxDepth;
351 }
352 for (var depth = 1; depth<indexes.length; depth++) {
353 var index = indexes[depth];
354 var invalidChars = index.replace(/^[0-9]+'?$/g, "")
355 if (invalidChars.length > 0) {
356 return "Invalid characters " + invalidChars + " found at depth " + depth;
357 }
358 var indexValue = parseInt(index.replace("'", ""));
359 if (isNaN(depth)) {
360 return "Invalid number at depth " + depth;
361 }
362 if (indexValue > maxIndexValue) {
363 return "Value of " + indexValue + " at depth " + depth + " must be less than " + maxIndexValue;
364 }
365 }
366 }
367 return false;
368 }
369
370 function displayBip32Info() {
371 // Display the key
372 DOM.seed.val(seed);
373 var rootKey = bip32RootKey.toBase58();
374 DOM.rootKey.val(rootKey);
375 var extendedPrivKey = bip32ExtendedKey.toBase58();
376 DOM.extendedPrivKey.val(extendedPrivKey);
377 var extendedPubKey = bip32ExtendedKey.toBase58(false);
378 DOM.extendedPubKey.val(extendedPubKey);
379 // Display the addresses and privkeys
380 clearAddressesList();
381 displayAddresses(0, 20);
382 }
383
384 function displayAddresses(start, total) {
385 for (var i=0; i<total; i++) {
386 var index = i + start;
387 new TableRow(index);
388 }
389 }
390
391 function TableRow(index) {
392
393 var useHardenedAddresses = DOM.hardenedAddresses.prop("checked");
394
395 function init() {
396 calculateValues();
397 }
398
399 function calculateValues() {
400 setTimeout(function() {
401 var key = "";
402 if (useHardenedAddresses) {
403 key = bip32ExtendedKey.deriveHardened(index);
404 }
405 else {
406 key = bip32ExtendedKey.derive(index);
407 }
408 var address = key.getAddress().toString();
409 var privkey = key.privKey.toWIF(network);
410 var pubkey = key.pubKey.toHex();
411 var indexText = getDerivationPath() + "/" + index;
412 if (useHardenedAddresses) {
413 indexText = indexText + "'";
414 }
415 addAddressToList(indexText, address, pubkey, privkey);
416 }, 50)
417 }
418
419 init();
420
421 }
422
423 function showMore() {
424 var start = DOM.addresses.children().length;
425 var rowsToAdd = parseInt(DOM.rowsToAdd.val());
426 if (isNaN(rowsToAdd)) {
427 rowsToAdd = 20;
428 DOM.rowsToAdd.val("20");
429 }
430 if (rowsToAdd > 200) {
431 var msg = "Generating " + rowsToAdd + " rows could take a while. ";
432 msg += "Do you want to continue?";
433 if (!confirm(msg)) {
434 return;
435 }
436 }
437 displayAddresses(start, rowsToAdd);
438 }
439
440 function clearDisplay() {
441 clearAddressesList();
442 clearKey();
443 hideValidationError();
444 }
445
446 function clearAddressesList() {
447 DOM.addresses.empty();
448 }
449
450 function clearKey() {
451 DOM.rootKey.val("");
452 DOM.extendedPrivKey.val("");
453 DOM.extendedPubKey.val("");
454 }
455
456 function addAddressToList(indexText, address, pubkey, privkey) {
457 var row = $(addressRowTemplate.html());
458 // Elements
459 var indexCell = row.find(".index span");
460 var addressCell = row.find(".address span");
461 var pubkeyCell = row.find(".pubkey span");
462 var privkeyCell = row.find(".privkey span");
463 // Content
464 indexCell.text(indexText);
465 addressCell.text(address);
466 pubkeyCell.text(pubkey);
467 privkeyCell.text(privkey);
468 // Visibility
469 if (!showIndex) {
470 indexCell.addClass("invisible");
471 }
472 if (!showAddress) {
473 addressCell.addClass("invisible");
474 }
475 if (!showPubKey) {
476 pubkeyCell.addClass("invisible");
477 }
478 if (!showPrivKey) {
479 privkeyCell.addClass("invisible");
480 }
481 DOM.addresses.append(row);
482 }
483
484 function hasStrongRandom() {
485 return 'crypto' in window && window['crypto'] !== null;
486 }
487
488 function disableForms() {
489 $("form").on("submit", function(e) {
490 e.preventDefault();
491 });
492 }
493
494 function parseIntNoNaN(val, defaultVal) {
495 var v = parseInt(val);
496 if (isNaN(v)) {
497 return defaultVal;
498 }
499 return v;
500 }
501
502 function showPending() {
503 DOM.feedback
504 .text("Calculating...")
505 .show();
506 }
507
508 function findNearestWord(word) {
509 var language = getLanguage();
510 var words = WORDLISTS[language];
511 var minDistance = 99;
512 var closestWord = words[0];
513 for (var i=0; i<words.length; i++) {
514 var comparedTo = words[i];
515 var distance = Levenshtein.get(word, comparedTo);
516 if (distance < minDistance) {
517 closestWord = comparedTo;
518 minDistance = distance;
519 }
520 }
521 return closestWord;
522 }
523
524 function hidePending() {
525 DOM.feedback
526 .text("")
527 .hide();
528 }
529
530 function populateNetworkSelect() {
531 for (var i=0; i<networks.length; i++) {
532 var network = networks[i];
533 var option = $("<option>");
534 option.attr("value", i);
535 option.text(network.name);
536 DOM.phraseNetwork.append(option);
537 }
538 }
539
540 function getLanguage() {
541 var defaultLanguage = "english";
542 // Try to get from existing phrase
543 var language = getLanguageFromPhrase();
544 // Try to get from url if not from phrase
545 if (language.length == 0) {
546 language = getLanguageFromUrl();
547 }
548 // Default to English if no other option
549 if (language.length == 0) {
550 language = defaultLanguage;
551 }
552 return language;
553 }
554
555 function getLanguageFromPhrase(phrase) {
556 // Check if how many words from existing phrase match a language.
557 var language = "";
558 if (!phrase) {
559 phrase = DOM.phrase.val();
560 }
561 if (phrase.length > 0) {
562 var words = phraseToWordArray(phrase);
563 var languageMatches = {};
564 for (l in WORDLISTS) {
565 // Track how many words match in this language
566 languageMatches[l] = 0;
567 for (var i=0; i<words.length; i++) {
568 var wordInLanguage = WORDLISTS[l].indexOf(words[i]) > -1;
569 if (wordInLanguage) {
570 languageMatches[l]++;
571 }
572 }
573 // Find languages with most word matches.
574 // This is made difficult due to commonalities between Chinese
575 // simplified vs traditional.
576 var mostMatches = 0;
577 var mostMatchedLanguages = [];
578 for (var l in languageMatches) {
579 var numMatches = languageMatches[l];
580 if (numMatches > mostMatches) {
581 mostMatches = numMatches;
582 mostMatchedLanguages = [l];
583 }
584 else if (numMatches == mostMatches) {
585 mostMatchedLanguages.push(l);
586 }
587 }
588 }
589 if (mostMatchedLanguages.length > 0) {
590 // Use first language and warn if multiple detected
591 language = mostMatchedLanguages[0];
592 if (mostMatchedLanguages.length > 1) {
593 console.warn("Multiple possible languages");
594 console.warn(mostMatchedLanguages);
595 }
596 }
597 }
598 return language;
599 }
600
601 function getLanguageFromUrl() {
602 return window.location.hash.substring(1);
603 }
604
605 function setMnemonicLanguage() {
606 var language = getLanguage();
607 // Load the bip39 mnemonic generator for this language if required
608 if (!(language in mnemonics)) {
609 mnemonics[language] = new Mnemonic(language);
610 }
611 mnemonic = mnemonics[language];
612 }
613
614 function convertPhraseToNewLanguage() {
615 var oldLanguage = getLanguageFromPhrase();
616 var newLanguage = getLanguageFromUrl();
617 var oldPhrase = DOM.phrase.val();
618 var oldWords = phraseToWordArray(oldPhrase);
619 var newWords = [];
620 for (var i=0; i<oldWords.length; i++) {
621 var oldWord = oldWords[i];
622 var index = WORDLISTS[oldLanguage].indexOf(oldWord);
623 var newWord = WORDLISTS[newLanguage][index];
624 newWords.push(newWord);
625 }
626 newPhrase = wordArrayToPhrase(newWords);
627 return newPhrase;
628 }
629
630 // TODO look at jsbip39 - mnemonic.splitWords
631 function phraseToWordArray(phrase) {
632 var words = phrase.split(/\s/g);
633 var noBlanks = [];
634 for (var i=0; i<words.length; i++) {
635 var word = words[i];
636 if (word.length > 0) {
637 noBlanks.push(word);
638 }
639 }
640 return noBlanks;
641 }
642
643 // TODO look at jsbip39 - mnemonic.joinWords
644 function wordArrayToPhrase(words) {
645 var phrase = words.join(" ");
646 var language = getLanguageFromPhrase(phrase);
647 if (language == "japanese") {
648 phrase = words.join("\u3000");
649 }
650 return phrase;
651 }
652
653 var networks = [
654 {
655 name: "Bitcoin",
656 onSelect: function() {
657 network = bitcoin.networks.bitcoin;
658 DOM.bip44coin.val(0);
659 },
660 },
661 {
662 name: "Bitcoin Testnet",
663 onSelect: function() {
664 network = bitcoin.networks.testnet;
665 DOM.bip44coin.val(1);
666 },
667 },
668 {
669 name: "Litecoin",
670 onSelect: function() {
671 network = bitcoin.networks.litecoin;
672 DOM.bip44coin.val(2);
673 },
674 },
675 {
676 name: "Dogecoin",
677 onSelect: function() {
678 network = bitcoin.networks.dogecoin;
679 DOM.bip44coin.val(3);
680 },
681 },
682 {
683 name: "ShadowCash",
684 onSelect: function() {
685 network = bitcoin.networks.shadow;
686 DOM.bip44coin.val(35);
687 },
688 },
689 {
690 name: "ShadowCash Testnet",
691 onSelect: function() {
692 network = bitcoin.networks.shadowtn;
693 DOM.bip44coin.val(1);
694 },
695 },
696 {
697 name: "Viacoin",
698 onSelect: function() {
699 network = bitcoin.networks.viacoin;
700 DOM.bip44coin.val(14);
701 },
702 },
703 {
704 name: "Viacoin Testnet",
705 onSelect: function() {
706 network = bitcoin.networks.viacointestnet;
707 DOM.bip44coin.val(1);
708 },
709 },
710 {
711 name: "Jumbucks",
712 onSelect: function() {
713 network = bitcoin.networks.jumbucks;
714 DOM.bip44coin.val(26);
715 },
716 },
717 {
718 name: "CLAM",
719 onSelect: function() {
720 network = bitcoin.networks.clam;
721 DOM.bip44coin.val(23);
722 },
723 },
724 {
725 name: "DASH",
726 onSelect: function() {
727 network = bitcoin.networks.dash;
728 DOM.bip44coin.val(5);
729 },
730 },
731 {
732 name: "Namecoin",
733 onSelect: function() {
734 network = bitcoin.networks.namecoin;
735 DOM.bip44coin.val(7);
736 },
737 },
738 {
739 name: "Peercoin",
740 onSelect: function() {
741 network = bitcoin.networks.peercoin;
742 DOM.bip44coin.val(6);
743 },
744 },
745 ]
746
747 init();
748
749 })();