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