]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/BIP39.git/blame - src/js/index.js
QR code shown on hover for certain bits of data
[perso/Immae/Projets/Cryptomonnaies/BIP39.git] / src / js / index.js
CommitLineData
ebd8d4e8
IC
1(function() {
2
5ee7bb9e
IC
3 // mnemonics is populated as required by getLanguage
4 var mnemonics = { "english": new Mnemonic("english") };
5 var mnemonic = mnemonics["english"];
3e0ed16a 6 var seed = null
ebd8d4e8
IC
7 var bip32RootKey = null;
8 var bip32ExtendedKey = null;
1759e5e8 9 var network = bitcoin.networks.bitcoin;
ebd8d4e8
IC
10 var addressRowTemplate = $("#address-row-template");
11
700901cd
IC
12 var showIndex = true;
13 var showAddress = true;
1b12b2f5 14 var showPubKey = true;
700901cd 15 var showPrivKey = true;
e00964cc 16 var showQr = true;
700901cd 17
c6624d51 18 var entropyChangeTimeoutEvent = null;
ebd8d4e8 19 var phraseChangeTimeoutEvent = null;
efe41586 20 var rootKeyChangedTimeoutEvent = null;
ebd8d4e8
IC
21
22 var DOM = {};
d6cedc94
IC
23 DOM.network = $(".network");
24 DOM.phraseNetwork = $("#network-phrase");
c6624d51
IC
25 DOM.useEntropy = $(".use-entropy");
26 DOM.entropyContainer = $(".entropy-container");
27 DOM.entropy = $(".entropy");
0a84fe6a
IC
28 DOM.entropyFiltered = DOM.entropyContainer.find(".filtered");
29 DOM.entropyType = DOM.entropyContainer.find(".type");
30 DOM.entropyStrength = DOM.entropyContainer.find(".strength");
31 DOM.entropyEventCount = DOM.entropyContainer.find(".event-count");
32 DOM.entropyBits = DOM.entropyContainer.find(".bits");
33 DOM.entropyBitsPerEvent = DOM.entropyContainer.find(".bits-per-event");
34 DOM.entropyWordCount = DOM.entropyContainer.find(".word-count");
35 DOM.entropyBinary = DOM.entropyContainer.find(".binary");
36 DOM.entropyMnemonicLength = DOM.entropyContainer.find(".mnemonic-length");
ebd8d4e8 37 DOM.phrase = $(".phrase");
1abcc511 38 DOM.passphrase = $(".passphrase");
c6624d51 39 DOM.generateContainer = $(".generate-container");
ebd8d4e8 40 DOM.generate = $(".generate");
3e0ed16a 41 DOM.seed = $(".seed");
ebd8d4e8
IC
42 DOM.rootKey = $(".root-key");
43 DOM.extendedPrivKey = $(".extended-priv-key");
44 DOM.extendedPubKey = $(".extended-pub-key");
d6cedc94
IC
45 DOM.bip32tab = $("#bip32-tab");
46 DOM.bip44tab = $("#bip44-tab");
47 DOM.bip32panel = $("#bip32");
48 DOM.bip44panel = $("#bip44");
ebd8d4e8
IC
49 DOM.bip32path = $("#bip32-path");
50 DOM.bip44path = $("#bip44-path");
51 DOM.bip44purpose = $("#bip44 .purpose");
52 DOM.bip44coin = $("#bip44 .coin");
53 DOM.bip44account = $("#bip44 .account");
54 DOM.bip44change = $("#bip44 .change");
88df3739 55 DOM.generatedStrength = $(".generate-container .strength");
146e089e 56 DOM.hardenedAddresses = $(".hardened-addresses");
ebd8d4e8
IC
57 DOM.addresses = $(".addresses");
58 DOM.rowsToAdd = $(".rows-to-add");
59 DOM.more = $(".more");
60 DOM.feedback = $(".feedback");
61 DOM.tab = $(".derivation-type a");
62 DOM.indexToggle = $(".index-toggle");
63 DOM.addressToggle = $(".address-toggle");
1b12b2f5 64 DOM.publicKeyToggle = $(".public-key-toggle");
ebd8d4e8 65 DOM.privateKeyToggle = $(".private-key-toggle");
5ee7bb9e 66 DOM.languages = $(".languages a");
e00964cc
IC
67 DOM.qrContainer = $(".qr-container");
68 DOM.qrImage = DOM.qrContainer.find(".qr-image");
69 DOM.qrHint = DOM.qrContainer.find(".qr-hint");
70 DOM.showQrEls = $("[data-show-qr]");
ebd8d4e8 71
ebd8d4e8
IC
72 function init() {
73 // Events
d6cedc94 74 DOM.network.on("change", networkChanged);
c6624d51
IC
75 DOM.useEntropy.on("change", setEntropyVisibility);
76 DOM.entropy.on("input", delayedEntropyChanged);
3599674d 77 DOM.entropyMnemonicLength.on("change", entropyChanged);
a19a5498
IC
78 DOM.phrase.on("input", delayedPhraseChanged);
79 DOM.passphrase.on("input", delayedPhraseChanged);
ebd8d4e8
IC
80 DOM.generate.on("click", generateClicked);
81 DOM.more.on("click", showMore);
efe41586
IC
82 DOM.rootKey.on("input", delayedRootKeyChanged);
83 DOM.bip32path.on("input", calcForDerivationPath);
84 DOM.bip44purpose.on("input", calcForDerivationPath);
85 DOM.bip44coin.on("input", calcForDerivationPath);
86 DOM.bip44account.on("input", calcForDerivationPath);
87 DOM.bip44change.on("input", calcForDerivationPath);
88 DOM.tab.on("shown.bs.tab", calcForDerivationPath);
146e089e 89 DOM.hardenedAddresses.on("change", calcForDerivationPath);
ebd8d4e8
IC
90 DOM.indexToggle.on("click", toggleIndexes);
91 DOM.addressToggle.on("click", toggleAddresses);
1b12b2f5 92 DOM.publicKeyToggle.on("click", togglePublicKeys);
ebd8d4e8 93 DOM.privateKeyToggle.on("click", togglePrivateKeys);
5ee7bb9e 94 DOM.languages.on("click", languageChanged);
e00964cc 95 setQrEvents(DOM.showQrEls);
ebd8d4e8
IC
96 disableForms();
97 hidePending();
98 hideValidationError();
7f15cb6e 99 populateNetworkSelect();
ebd8d4e8
IC
100 }
101
102 // Event handlers
103
d6cedc94 104 function networkChanged(e) {
54563907
IC
105 var networkIndex = e.target.value;
106 networks[networkIndex].onSelect();
107 if (seed != null) {
108 phraseChanged();
109 }
110 else {
111 rootKeyChanged();
112 }
d6cedc94
IC
113 }
114
c6624d51
IC
115 function setEntropyVisibility() {
116 if (isUsingOwnEntropy()) {
117 DOM.entropyContainer.removeClass("hidden");
118 DOM.generateContainer.addClass("hidden");
119 DOM.phrase.prop("readonly", true);
120 DOM.entropy.focus();
121 entropyChanged();
122 }
123 else {
124 DOM.entropyContainer.addClass("hidden");
125 DOM.generateContainer.removeClass("hidden");
126 DOM.phrase.prop("readonly", false);
057722b0 127 hidePending();
c6624d51
IC
128 }
129 }
130
ebd8d4e8
IC
131 function delayedPhraseChanged() {
132 hideValidationError();
133 showPending();
134 if (phraseChangeTimeoutEvent != null) {
135 clearTimeout(phraseChangeTimeoutEvent);
136 }
137 phraseChangeTimeoutEvent = setTimeout(phraseChanged, 400);
138 }
139
140 function phraseChanged() {
141 showPending();
142 hideValidationError();
5ee7bb9e 143 setMnemonicLanguage();
ebd8d4e8
IC
144 // Get the mnemonic phrase
145 var phrase = DOM.phrase.val();
146 var errorText = findPhraseErrors(phrase);
147 if (errorText) {
148 showValidationError(errorText);
149 return;
150 }
efe41586
IC
151 // Calculate and display
152 var passphrase = DOM.passphrase.val();
153 calcBip32RootKeyFromSeed(phrase, passphrase);
154 calcForDerivationPath();
155 hidePending();
156 }
157
c6624d51
IC
158 function delayedEntropyChanged() {
159 hideValidationError();
160 showPending();
161 if (entropyChangeTimeoutEvent != null) {
162 clearTimeout(entropyChangeTimeoutEvent);
163 }
164 entropyChangeTimeoutEvent = setTimeout(entropyChanged, 400);
165 }
166
167 function entropyChanged() {
057722b0
IC
168 // If blank entropy, clear mnemonic, addresses, errors
169 if (DOM.entropy.val().trim().length == 0) {
170 clearDisplay();
0a84fe6a 171 clearEntropyFeedback();
057722b0
IC
172 DOM.phrase.val("");
173 showValidationError("Blank entropy");
174 return;
175 }
176 // Get the current phrase to detect changes
177 var phrase = DOM.phrase.val();
178 // Set the phrase from the entropy
c6624d51 179 setMnemonicFromEntropy();
057722b0
IC
180 // Recalc addresses if the phrase has changed
181 var newPhrase = DOM.phrase.val();
182 if (newPhrase != phrase) {
183 if (newPhrase.length == 0) {
184 clearDisplay();
185 }
186 else {
187 phraseChanged();
188 }
189 }
190 else {
191 hidePending();
192 }
c6624d51
IC
193 }
194
efe41586
IC
195 function delayedRootKeyChanged() {
196 // Warn if there is an existing mnemonic or passphrase.
197 if (DOM.phrase.val().length > 0 || DOM.passphrase.val().length > 0) {
198 if (!confirm("This will clear existing mnemonic and passphrase")) {
199 DOM.rootKey.val(bip32RootKey);
200 return
201 }
202 }
203 hideValidationError();
204 showPending();
205 // Clear existing mnemonic and passphrase
206 DOM.phrase.val("");
207 DOM.passphrase.val("");
208 seed = null;
209 if (rootKeyChangedTimeoutEvent != null) {
210 clearTimeout(rootKeyChangedTimeoutEvent);
211 }
212 rootKeyChangedTimeoutEvent = setTimeout(rootKeyChanged, 400);
213 }
214
215 function rootKeyChanged() {
216 showPending();
217 hideValidationError();
218 // Validate the root key TODO
219 var rootKeyBase58 = DOM.rootKey.val();
220 var errorText = validateRootKey(rootKeyBase58);
221 if (errorText) {
222 showValidationError(errorText);
223 return;
224 }
225 // Calculate and display
226 calcBip32RootKeyFromBase58(rootKeyBase58);
227 calcForDerivationPath();
228 hidePending();
229 }
230
231 function calcForDerivationPath() {
232 showPending();
233 hideValidationError();
ebd8d4e8 234 // Get the derivation path
38523d36
IC
235 var derivationPath = getDerivationPath();
236 var errorText = findDerivationPathErrors(derivationPath);
ebd8d4e8
IC
237 if (errorText) {
238 showValidationError(errorText);
239 return;
240 }
efe41586 241 calcBip32ExtendedKey(derivationPath);
ebd8d4e8
IC
242 displayBip32Info();
243 hidePending();
244 }
245
246 function generateClicked() {
c6624d51
IC
247 if (isUsingOwnEntropy()) {
248 return;
249 }
ebd8d4e8
IC
250 clearDisplay();
251 showPending();
252 setTimeout(function() {
5ee7bb9e 253 setMnemonicLanguage();
ebd8d4e8
IC
254 var phrase = generateRandomPhrase();
255 if (!phrase) {
256 return;
257 }
258 phraseChanged();
259 }, 50);
260 }
261
5ee7bb9e
IC
262 function languageChanged() {
263 setTimeout(function() {
264 setMnemonicLanguage();
265 if (DOM.phrase.val().length > 0) {
266 var newPhrase = convertPhraseToNewLanguage();
267 DOM.phrase.val(newPhrase);
268 phraseChanged();
269 }
270 else {
271 DOM.generate.trigger("click");
272 }
273 }, 50);
274 }
275
ebd8d4e8 276 function toggleIndexes() {
700901cd 277 showIndex = !showIndex;
ebd8d4e8
IC
278 $("td.index span").toggleClass("invisible");
279 }
280
281 function toggleAddresses() {
700901cd 282 showAddress = !showAddress;
ebd8d4e8
IC
283 $("td.address span").toggleClass("invisible");
284 }
285
1b12b2f5
IC
286 function togglePublicKeys() {
287 showPubKey = !showPubKey;
288 $("td.pubkey span").toggleClass("invisible");
289 }
290
ebd8d4e8 291 function togglePrivateKeys() {
700901cd 292 showPrivKey = !showPrivKey;
ebd8d4e8
IC
293 $("td.privkey span").toggleClass("invisible");
294 }
295
296 // Private methods
297
298 function generateRandomPhrase() {
299 if (!hasStrongRandom()) {
300 var errorText = "This browser does not support strong randomness";
301 showValidationError(errorText);
302 return;
303 }
88df3739 304 var numWords = parseInt(DOM.generatedStrength.val());
ebd8d4e8
IC
305 var strength = numWords / 3 * 32;
306 var words = mnemonic.generate(strength);
307 DOM.phrase.val(words);
308 return words;
309 }
310
efe41586 311 function calcBip32RootKeyFromSeed(phrase, passphrase) {
3e0ed16a 312 seed = mnemonic.toSeed(phrase, passphrase);
1759e5e8 313 bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network);
efe41586
IC
314 }
315
316 function calcBip32RootKeyFromBase58(rootKeyBase58) {
3821c0d3 317 bip32RootKey = bitcoin.HDNode.fromBase58(rootKeyBase58, network);
efe41586
IC
318 }
319
320 function calcBip32ExtendedKey(path) {
ebd8d4e8
IC
321 bip32ExtendedKey = bip32RootKey;
322 // Derive the key from the path
323 var pathBits = path.split("/");
324 for (var i=0; i<pathBits.length; i++) {
325 var bit = pathBits[i];
326 var index = parseInt(bit);
327 if (isNaN(index)) {
328 continue;
329 }
330 var hardened = bit[bit.length-1] == "'";
331 if (hardened) {
332 bip32ExtendedKey = bip32ExtendedKey.deriveHardened(index);
333 }
334 else {
335 bip32ExtendedKey = bip32ExtendedKey.derive(index);
336 }
337 }
338 }
339
340 function showValidationError(errorText) {
341 DOM.feedback
342 .text(errorText)
343 .show();
344 }
345
346 function hideValidationError() {
347 DOM.feedback
348 .text("")
349 .hide();
350 }
351
352 function findPhraseErrors(phrase) {
ebd8d4e8 353 // Preprocess the words
783981de 354 phrase = mnemonic.normalizeString(phrase);
5ee7bb9e 355 var words = phraseToWordArray(phrase);
057722b0
IC
356 // Detect blank phrase
357 if (words.length == 0) {
358 return "Blank mnemonic";
359 }
563e401a 360 // Check each word
5ee7bb9e
IC
361 for (var i=0; i<words.length; i++) {
362 var word = words[i];
363 var language = getLanguage();
364 if (WORDLISTS[language].indexOf(word) == -1) {
563e401a
IC
365 console.log("Finding closest match to " + word);
366 var nearestWord = findNearestWord(word);
367 return word + " not in wordlist, did you mean " + nearestWord + "?";
368 }
369 }
ebd8d4e8 370 // Check the words are valid
5ee7bb9e 371 var properPhrase = wordArrayToPhrase(words);
ebd8d4e8
IC
372 var isValid = mnemonic.check(properPhrase);
373 if (!isValid) {
374 return "Invalid mnemonic";
375 }
376 return false;
377 }
378
efe41586
IC
379 function validateRootKey(rootKeyBase58) {
380 try {
381 bitcoin.HDNode.fromBase58(rootKeyBase58);
382 }
383 catch (e) {
384 return "Invalid root key";
385 }
386 return "";
387 }
388
38523d36
IC
389 function getDerivationPath() {
390 if (DOM.bip44tab.hasClass("active")) {
391 var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44);
392 var coin = parseIntNoNaN(DOM.bip44coin.val(), 0);
393 var account = parseIntNoNaN(DOM.bip44account.val(), 0);
394 var change = parseIntNoNaN(DOM.bip44change.val(), 0);
395 var path = "m/";
396 path += purpose + "'/";
397 path += coin + "'/";
398 path += account + "'/";
399 path += change;
400 DOM.bip44path.val(path);
401 var derivationPath = DOM.bip44path.val();
402 console.log("Using derivation path from BIP44 tab: " + derivationPath);
403 return derivationPath;
404 }
405 else if (DOM.bip32tab.hasClass("active")) {
406 var derivationPath = DOM.bip32path.val();
407 console.log("Using derivation path from BIP32 tab: " + derivationPath);
408 return derivationPath;
409 }
410 else {
411 console.log("Unknown derivation path");
412 }
413 }
414
ebd8d4e8 415 function findDerivationPathErrors(path) {
30c9e79d
IC
416 // TODO is not perfect but is better than nothing
417 // Inspired by
418 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
419 // and
420 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys
421 var maxDepth = 255; // TODO verify this!!
422 var maxIndexValue = Math.pow(2, 31); // TODO verify this!!
423 if (path[0] != "m") {
424 return "First character must be 'm'";
425 }
426 if (path.length > 1) {
427 if (path[1] != "/") {
428 return "Separator must be '/'";
429 }
430 var indexes = path.split("/");
431 if (indexes.length > maxDepth) {
432 return "Derivation depth is " + indexes.length + ", must be less than " + maxDepth;
433 }
434 for (var depth = 1; depth<indexes.length; depth++) {
435 var index = indexes[depth];
436 var invalidChars = index.replace(/^[0-9]+'?$/g, "")
437 if (invalidChars.length > 0) {
438 return "Invalid characters " + invalidChars + " found at depth " + depth;
439 }
440 var indexValue = parseInt(index.replace("'", ""));
441 if (isNaN(depth)) {
442 return "Invalid number at depth " + depth;
443 }
444 if (indexValue > maxIndexValue) {
445 return "Value of " + indexValue + " at depth " + depth + " must be less than " + maxIndexValue;
446 }
447 }
448 }
ebd8d4e8
IC
449 return false;
450 }
451
452 function displayBip32Info() {
453 // Display the key
3e0ed16a 454 DOM.seed.val(seed);
ebd8d4e8
IC
455 var rootKey = bip32RootKey.toBase58();
456 DOM.rootKey.val(rootKey);
457 var extendedPrivKey = bip32ExtendedKey.toBase58();
458 DOM.extendedPrivKey.val(extendedPrivKey);
459 var extendedPubKey = bip32ExtendedKey.toBase58(false);
460 DOM.extendedPubKey.val(extendedPubKey);
461 // Display the addresses and privkeys
462 clearAddressesList();
463 displayAddresses(0, 20);
464 }
465
466 function displayAddresses(start, total) {
467 for (var i=0; i<total; i++) {
a8c45487
IC
468 var index = i + start;
469 new TableRow(index);
ebd8d4e8
IC
470 }
471 }
472
a8c45487
IC
473 function TableRow(index) {
474
146e089e
IC
475 var useHardenedAddresses = DOM.hardenedAddresses.prop("checked");
476
a8c45487
IC
477 function init() {
478 calculateValues();
479 }
480
481 function calculateValues() {
482 setTimeout(function() {
146e089e
IC
483 var key = "";
484 if (useHardenedAddresses) {
485 key = bip32ExtendedKey.deriveHardened(index);
486 }
487 else {
488 key = bip32ExtendedKey.derive(index);
489 }
a8c45487
IC
490 var address = key.getAddress().toString();
491 var privkey = key.privKey.toWIF(network);
1b12b2f5 492 var pubkey = key.pubKey.toHex();
38523d36 493 var indexText = getDerivationPath() + "/" + index;
146e089e
IC
494 if (useHardenedAddresses) {
495 indexText = indexText + "'";
496 }
1b12b2f5 497 addAddressToList(indexText, address, pubkey, privkey);
a8c45487
IC
498 }, 50)
499 }
500
501 init();
502
503 }
504
ebd8d4e8
IC
505 function showMore() {
506 var start = DOM.addresses.children().length;
507 var rowsToAdd = parseInt(DOM.rowsToAdd.val());
508 if (isNaN(rowsToAdd)) {
509 rowsToAdd = 20;
510 DOM.rowsToAdd.val("20");
511 }
512 if (rowsToAdd > 200) {
513 var msg = "Generating " + rowsToAdd + " rows could take a while. ";
514 msg += "Do you want to continue?";
515 if (!confirm(msg)) {
516 return;
517 }
518 }
ebd8d4e8 519 displayAddresses(start, rowsToAdd);
ebd8d4e8
IC
520 }
521
522 function clearDisplay() {
523 clearAddressesList();
524 clearKey();
525 hideValidationError();
526 }
527
528 function clearAddressesList() {
529 DOM.addresses.empty();
530 }
531
532 function clearKey() {
533 DOM.rootKey.val("");
534 DOM.extendedPrivKey.val("");
535 DOM.extendedPubKey.val("");
536 }
537
1b12b2f5 538 function addAddressToList(indexText, address, pubkey, privkey) {
ebd8d4e8 539 var row = $(addressRowTemplate.html());
700901cd
IC
540 // Elements
541 var indexCell = row.find(".index span");
542 var addressCell = row.find(".address span");
1b12b2f5 543 var pubkeyCell = row.find(".pubkey span");
700901cd
IC
544 var privkeyCell = row.find(".privkey span");
545 // Content
ae30fed8 546 indexCell.text(indexText);
700901cd 547 addressCell.text(address);
1b12b2f5 548 pubkeyCell.text(pubkey);
700901cd
IC
549 privkeyCell.text(privkey);
550 // Visibility
551 if (!showIndex) {
552 indexCell.addClass("invisible");
553 }
554 if (!showAddress) {
555 addressCell.addClass("invisible");
556 }
1b12b2f5
IC
557 if (!showPubKey) {
558 pubkeyCell.addClass("invisible");
559 }
700901cd 560 if (!showPrivKey) {
6d628db7 561 privkeyCell.addClass("invisible");
700901cd 562 }
ebd8d4e8 563 DOM.addresses.append(row);
e00964cc
IC
564 var rowShowQrEls = row.find("[data-show-qr]");
565 setQrEvents(rowShowQrEls);
ebd8d4e8
IC
566 }
567
568 function hasStrongRandom() {
569 return 'crypto' in window && window['crypto'] !== null;
570 }
571
572 function disableForms() {
573 $("form").on("submit", function(e) {
574 e.preventDefault();
575 });
576 }
577
ebd8d4e8
IC
578 function parseIntNoNaN(val, defaultVal) {
579 var v = parseInt(val);
580 if (isNaN(v)) {
581 return defaultVal;
582 }
583 return v;
584 }
585
586 function showPending() {
587 DOM.feedback
588 .text("Calculating...")
589 .show();
590 }
591
563e401a 592 function findNearestWord(word) {
5ee7bb9e
IC
593 var language = getLanguage();
594 var words = WORDLISTS[language];
563e401a
IC
595 var minDistance = 99;
596 var closestWord = words[0];
597 for (var i=0; i<words.length; i++) {
598 var comparedTo = words[i];
599 var distance = Levenshtein.get(word, comparedTo);
600 if (distance < minDistance) {
601 closestWord = comparedTo;
602 minDistance = distance;
603 }
604 }
605 return closestWord;
606 }
607
ebd8d4e8
IC
608 function hidePending() {
609 DOM.feedback
610 .text("")
611 .hide();
612 }
613
7f15cb6e
IC
614 function populateNetworkSelect() {
615 for (var i=0; i<networks.length; i++) {
616 var network = networks[i];
617 var option = $("<option>");
618 option.attr("value", i);
619 option.text(network.name);
620 DOM.phraseNetwork.append(option);
621 }
622 }
623
5ee7bb9e
IC
624 function getLanguage() {
625 var defaultLanguage = "english";
626 // Try to get from existing phrase
627 var language = getLanguageFromPhrase();
628 // Try to get from url if not from phrase
629 if (language.length == 0) {
630 language = getLanguageFromUrl();
631 }
632 // Default to English if no other option
633 if (language.length == 0) {
634 language = defaultLanguage;
635 }
636 return language;
637 }
638
639 function getLanguageFromPhrase(phrase) {
640 // Check if how many words from existing phrase match a language.
641 var language = "";
642 if (!phrase) {
643 phrase = DOM.phrase.val();
644 }
645 if (phrase.length > 0) {
646 var words = phraseToWordArray(phrase);
647 var languageMatches = {};
648 for (l in WORDLISTS) {
649 // Track how many words match in this language
650 languageMatches[l] = 0;
651 for (var i=0; i<words.length; i++) {
652 var wordInLanguage = WORDLISTS[l].indexOf(words[i]) > -1;
653 if (wordInLanguage) {
654 languageMatches[l]++;
655 }
656 }
657 // Find languages with most word matches.
658 // This is made difficult due to commonalities between Chinese
659 // simplified vs traditional.
660 var mostMatches = 0;
661 var mostMatchedLanguages = [];
662 for (var l in languageMatches) {
663 var numMatches = languageMatches[l];
664 if (numMatches > mostMatches) {
665 mostMatches = numMatches;
666 mostMatchedLanguages = [l];
667 }
668 else if (numMatches == mostMatches) {
669 mostMatchedLanguages.push(l);
670 }
671 }
672 }
673 if (mostMatchedLanguages.length > 0) {
674 // Use first language and warn if multiple detected
675 language = mostMatchedLanguages[0];
676 if (mostMatchedLanguages.length > 1) {
677 console.warn("Multiple possible languages");
678 console.warn(mostMatchedLanguages);
679 }
680 }
681 }
682 return language;
683 }
684
685 function getLanguageFromUrl() {
c6624d51
IC
686 for (var language in WORDLISTS) {
687 if (window.location.hash.indexOf(language) > -1) {
688 return language;
689 }
690 }
691 return "";
5ee7bb9e
IC
692 }
693
694 function setMnemonicLanguage() {
695 var language = getLanguage();
696 // Load the bip39 mnemonic generator for this language if required
697 if (!(language in mnemonics)) {
698 mnemonics[language] = new Mnemonic(language);
699 }
700 mnemonic = mnemonics[language];
701 }
702
703 function convertPhraseToNewLanguage() {
704 var oldLanguage = getLanguageFromPhrase();
705 var newLanguage = getLanguageFromUrl();
706 var oldPhrase = DOM.phrase.val();
707 var oldWords = phraseToWordArray(oldPhrase);
708 var newWords = [];
709 for (var i=0; i<oldWords.length; i++) {
710 var oldWord = oldWords[i];
711 var index = WORDLISTS[oldLanguage].indexOf(oldWord);
712 var newWord = WORDLISTS[newLanguage][index];
713 newWords.push(newWord);
714 }
715 newPhrase = wordArrayToPhrase(newWords);
716 return newPhrase;
717 }
718
719 // TODO look at jsbip39 - mnemonic.splitWords
720 function phraseToWordArray(phrase) {
721 var words = phrase.split(/\s/g);
722 var noBlanks = [];
723 for (var i=0; i<words.length; i++) {
724 var word = words[i];
725 if (word.length > 0) {
726 noBlanks.push(word);
727 }
728 }
729 return noBlanks;
730 }
731
732 // TODO look at jsbip39 - mnemonic.joinWords
733 function wordArrayToPhrase(words) {
734 var phrase = words.join(" ");
735 var language = getLanguageFromPhrase(phrase);
736 if (language == "japanese") {
737 phrase = words.join("\u3000");
738 }
739 return phrase;
740 }
741
c6624d51
IC
742 function isUsingOwnEntropy() {
743 return DOM.useEntropy.prop("checked");
744 }
745
746 function setMnemonicFromEntropy() {
0a84fe6a 747 clearEntropyFeedback();
057722b0 748 // Get entropy value
c6624d51 749 var entropyStr = DOM.entropy.val();
057722b0 750 // Work out minimum base for entropy
c6624d51 751 var entropy = Entropy.fromString(entropyStr);
057722b0 752 if (entropy.binaryStr.length == 0) {
c6624d51
IC
753 return;
754 }
755 // Show entropy details
1cf1bbaf 756 showEntropyFeedback(entropy);
3599674d
IC
757 // Use entropy hash if not using raw entropy
758 var bits = entropy.binaryStr;
759 var mnemonicLength = DOM.entropyMnemonicLength.val();
760 if (mnemonicLength != "raw") {
761 // Get bits by hashing entropy with SHA256
762 var hash = sjcl.hash.sha256.hash(entropy.cleanStr);
763 var hex = sjcl.codec.hex.fromBits(hash);
764 bits = BigInteger.parse(hex, 16).toString(2);
765 for (var i=0; i<256-bits.length; i++) {
766 bits = "0" + bits;
767 }
768 // Truncate hash to suit number of words
769 mnemonicLength = parseInt(mnemonicLength);
770 var numberOfBits = 32 * mnemonicLength / 3;
771 bits = bits.substring(0, numberOfBits);
772 }
c6624d51 773 // Discard trailing entropy
3599674d 774 var bitsToUse = Math.floor(bits.length / 32) * 32;
d6fd8ebf
IC
775 var start = bits.length - bitsToUse;
776 var binaryStr = bits.substring(start);
c6624d51
IC
777 // Convert entropy string to numeric array
778 var entropyArr = [];
adc8ce12
IC
779 for (var i=0; i<binaryStr.length / 8; i++) {
780 var byteAsBits = binaryStr.substring(i*8, i*8+8);
781 var entropyByte = parseInt(byteAsBits, 2);
c6624d51
IC
782 entropyArr.push(entropyByte)
783 }
784 // Convert entropy array to mnemonic
785 var phrase = mnemonic.toMnemonic(entropyArr);
786 // Set the mnemonic in the UI
787 DOM.phrase.val(phrase);
788 }
789
0a84fe6a
IC
790 function clearEntropyFeedback() {
791 DOM.entropyStrength.text("...");
1cf1bbaf 792 DOM.entropyType.text("");
0a84fe6a
IC
793 DOM.entropyWordCount.text("0");
794 DOM.entropyEventCount.text("0");
795 DOM.entropyBitsPerEvent.text("0");
796 DOM.entropyBits.text("0");
797 DOM.entropyFiltered.html("&nbsp;");
798 DOM.entropyBinary.html("&nbsp;");
c6624d51
IC
799 }
800
1cf1bbaf 801 function showEntropyFeedback(entropy) {
6422c1cd 802 var numberOfBits = entropy.binaryStr.length;
1cf1bbaf 803 var strength = "extremely weak";
6422c1cd 804 if (numberOfBits >= 64) {
1cf1bbaf
IC
805 strength = "very weak";
806 }
6422c1cd 807 if (numberOfBits >= 96) {
1cf1bbaf
IC
808 strength = "weak";
809 }
6422c1cd 810 if (numberOfBits >= 128) {
1cf1bbaf
IC
811 strength = "strong";
812 }
6422c1cd 813 if (numberOfBits >= 160) {
1cf1bbaf
IC
814 strength = "very strong";
815 }
6422c1cd 816 if (numberOfBits >= 192) {
1cf1bbaf
IC
817 strength = "extremely strong";
818 }
b299a6a7
IC
819 // If time to crack is less than one day, and password is considered
820 // strong or better based on the number of bits, rename strength to
821 // 'easily cracked'.
9bc39377
IC
822 try {
823 var z = zxcvbn(entropy.base.parts.join(""));
824 var timeToCrack = z.crack_times_seconds.offline_fast_hashing_1e10_per_second;
825 if (timeToCrack < 86400 && entropy.binaryStr.length >= 128) {
826 strength = "easily cracked";
827 if (z.feedback.warning != "") {
828 strength = strength + " - " + z.feedback.warning;
829 };
830 }
831 }
832 catch (e) {
833 strength = "unknown";
834 console.log("Error detecting entropy strength with zxcvbn:");
835 console.log(e);
b299a6a7 836 }
391c7f26 837 var entropyTypeStr = getEntropyTypeStr(entropy);
6422c1cd 838 var wordCount = Math.floor(numberOfBits / 32) * 3;
94959756 839 var bitsPerEvent = entropy.bitsPerEvent.toFixed(2);
b54c1218 840 DOM.entropyFiltered.html(entropy.cleanHtml);
391c7f26 841 DOM.entropyType.text(entropyTypeStr);
1cf1bbaf
IC
842 DOM.entropyStrength.text(strength);
843 DOM.entropyEventCount.text(entropy.base.ints.length);
6422c1cd 844 DOM.entropyBits.text(numberOfBits);
0a84fe6a
IC
845 DOM.entropyWordCount.text(wordCount);
846 DOM.entropyBinary.text(entropy.binaryStr);
6422c1cd 847 DOM.entropyBitsPerEvent.text(bitsPerEvent);
02f05d3e
IC
848 }
849
391c7f26
IC
850 function getEntropyTypeStr(entropy) {
851 var typeStr = entropy.base.str;
852 // Add some detail if these are cards
853 if (entropy.base.asInt == 52) {
854 var cardDetail = []; // array of message strings
855 // Detect duplicates
856 var dupes = [];
857 var dupeTracker = {};
858 for (var i=0; i<entropy.base.parts.length; i++) {
859 var card = entropy.base.parts[i];
5c653a12
IC
860 var cardUpper = card.toUpperCase();
861 if (cardUpper in dupeTracker) {
391c7f26
IC
862 dupes.push(card);
863 }
5c653a12 864 dupeTracker[cardUpper] = true;
391c7f26
IC
865 }
866 if (dupes.length > 0) {
867 var dupeWord = "duplicates";
868 if (dupes.length == 1) {
869 dupeWord = "duplicate";
870 }
871 var msg = dupes.length + " " + dupeWord + ": " + dupes.slice(0,3).join(" ");
872 if (dupes.length > 3) {
873 msg += "...";
874 }
875 cardDetail.push(msg);
876 }
877 // Detect full deck
878 var uniqueCards = [];
879 for (var uniqueCard in dupeTracker) {
880 uniqueCards.push(uniqueCard);
881 }
882 if (uniqueCards.length == 52) {
883 cardDetail.unshift("full deck");
884 }
bbc29c80
IC
885 // Detect missing cards
886 var values = "A23456789TJQK";
887 var suits = "CDHS";
888 var missingCards = [];
889 for (var i=0; i<suits.length; i++) {
890 for (var j=0; j<values.length; j++) {
891 var card = values[j] + suits[i];
892 if (!(card in dupeTracker)) {
893 missingCards.push(card);
894 }
895 }
896 }
897 // Display missing cards if six or less, ie clearly going for full deck
898 if (missingCards.length > 0 && missingCards.length <= 6) {
899 var msg = missingCards.length + " missing: " + missingCards.slice(0,3).join(" ");
900 if (missingCards.length > 3) {
901 msg += "...";
902 }
903 cardDetail.push(msg);
904 }
391c7f26
IC
905 // Add card details to typeStr
906 if (cardDetail.length > 0) {
907 typeStr += " (" + cardDetail.join(", ") + ")";
908 }
909 }
910 return typeStr;
911 }
912
e00964cc
IC
913 function setQrEvents(els) {
914 els.on("mouseenter", createQr);
915 els.on("mouseleave", destroyQr);
916 els.on("click", toggleQr);
917 }
918
919 function createQr(e) {
920 var content = e.target.textContent || e.target.value;
921 if (content) {
922 var size = 130;
923 DOM.qrImage.qrcode({width: size, height: size, text: content});
924 if (!showQr) {
925 DOM.qrImage.addClass("hidden");
926 }
927 DOM.qrContainer.removeClass("hidden");
928 }
929 }
930
931 function destroyQr() {
932 DOM.qrImage.text("");
933 DOM.qrContainer.addClass("hidden");
934 }
935
936 function toggleQr() {
937 showQr = !showQr;
938 DOM.qrImage.toggleClass("hidden");
939 DOM.qrHint.toggleClass("hidden");
940 }
941
7f15cb6e
IC
942 var networks = [
943 {
7a995731
IC
944 name: "Bitcoin",
945 onSelect: function() {
1759e5e8 946 network = bitcoin.networks.bitcoin;
7a995731 947 DOM.bip44coin.val(0);
7a995731
IC
948 },
949 },
7f15cb6e 950 {
7a995731
IC
951 name: "Bitcoin Testnet",
952 onSelect: function() {
1759e5e8 953 network = bitcoin.networks.testnet;
7a995731 954 DOM.bip44coin.val(1);
7a995731
IC
955 },
956 },
7f15cb6e 957 {
7a995731
IC
958 name: "Litecoin",
959 onSelect: function() {
1759e5e8 960 network = bitcoin.networks.litecoin;
7a995731
IC
961 DOM.bip44coin.val(2);
962 },
963 },
7f15cb6e 964 {
7a995731
IC
965 name: "Dogecoin",
966 onSelect: function() {
1759e5e8 967 network = bitcoin.networks.dogecoin;
7a995731
IC
968 DOM.bip44coin.val(3);
969 },
970 },
e3a9508c
IC
971 {
972 name: "ShadowCash",
973 onSelect: function() {
974 network = bitcoin.networks.shadow;
975 DOM.bip44coin.val(35);
976 },
977 },
978 {
979 name: "ShadowCash Testnet",
980 onSelect: function() {
981 network = bitcoin.networks.shadowtn;
982 DOM.bip44coin.val(1);
983 },
984 },
a3baa26e
IC
985 {
986 name: "Viacoin",
987 onSelect: function() {
988 network = bitcoin.networks.viacoin;
989 DOM.bip44coin.val(14);
990 },
991 },
992 {
993 name: "Viacoin Testnet",
994 onSelect: function() {
995 network = bitcoin.networks.viacointestnet;
996 DOM.bip44coin.val(1);
997 },
998 },
999 {
1000 name: "Jumbucks",
1001 onSelect: function() {
1002 network = bitcoin.networks.jumbucks;
1003 DOM.bip44coin.val(26);
1004 },
1005 },
5c434a8a
CM
1006 {
1007 name: "CLAM",
1008 onSelect: function() {
1009 network = bitcoin.networks.clam;
1010 DOM.bip44coin.val(23);
1011 },
1012 },
82f91834
DG
1013 {
1014 name: "DASH",
1015 onSelect: function() {
1016 network = bitcoin.networks.dash;
1017 DOM.bip44coin.val(5);
1018 },
1019 },
07ac4350 1020 {
1021 name: "Namecoin",
1022 onSelect: function() {
1023 network = bitcoin.networks.namecoin;
1024 DOM.bip44coin.val(7);
1025 },
1026 },
1027 {
1028 name: "Peercoin",
1029 onSelect: function() {
1030 network = bitcoin.networks.peercoin;
1031 DOM.bip44coin.val(6);
1032 },
1033 },
7f15cb6e 1034 ]
7a995731 1035
ebd8d4e8
IC
1036 init();
1037
1038})();