3 // mnemonics is populated as required by getLanguage
4 var mnemonics
= { "english": new Mnemonic("english") };
5 var mnemonic
= mnemonics
["english"];
7 var bip32RootKey
= null;
8 var bip32ExtendedKey
= null;
9 var network
= bitcoin
.networks
.bitcoin
;
10 var addressRowTemplate
= $("#address-row-template");
13 var showAddress
= true;
14 var showPubKey
= true;
15 var showPrivKey
= true;
18 var entropyChangeTimeoutEvent
= null;
19 var phraseChangeTimeoutEvent
= null;
20 var rootKeyChangedTimeoutEvent
= null;
23 DOM
.network
= $(".network");
24 DOM
.phraseNetwork
= $("#network-phrase");
25 DOM
.useEntropy
= $(".use-entropy");
26 DOM
.entropyContainer
= $(".entropy-container");
27 DOM
.entropy
= $(".entropy");
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");
37 DOM
.phrase
= $(".phrase");
38 DOM
.passphrase
= $(".passphrase");
39 DOM
.generateContainer
= $(".generate-container");
40 DOM
.generate
= $(".generate");
41 DOM
.seed
= $(".seed");
42 DOM
.rootKey
= $(".root-key");
43 DOM
.extendedPrivKey
= $(".extended-priv-key");
44 DOM
.extendedPubKey
= $(".extended-pub-key");
45 DOM
.bip32tab
= $("#bip32-tab");
46 DOM
.bip44tab
= $("#bip44-tab");
47 DOM
.bip32panel
= $("#bip32");
48 DOM
.bip44panel
= $("#bip44");
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
.bip44accountXprv
= $("#bip44 .account-xprv");
55 DOM
.bip44accountXpub
= $("#bip44 .account-xpub");
56 DOM
.bip44change
= $("#bip44 .change");
57 DOM
.generatedStrength
= $(".generate-container .strength");
58 DOM
.hardenedAddresses
= $(".hardened-addresses");
59 DOM
.addresses
= $(".addresses");
60 DOM
.rowsToAdd
= $(".rows-to-add");
61 DOM
.more
= $(".more");
62 DOM
.feedback
= $(".feedback");
63 DOM
.tab
= $(".derivation-type a");
64 DOM
.indexToggle
= $(".index-toggle");
65 DOM
.addressToggle
= $(".address-toggle");
66 DOM
.publicKeyToggle
= $(".public-key-toggle");
67 DOM
.privateKeyToggle
= $(".private-key-toggle");
68 DOM
.languages
= $(".languages a");
69 DOM
.qrContainer
= $(".qr-container");
70 DOM
.qrImage
= DOM
.qrContainer
.find(".qr-image");
71 DOM
.qrHint
= DOM
.qrContainer
.find(".qr-hint");
72 DOM
.showQrEls
= $("[data-show-qr]");
76 DOM
.network
.on("change", networkChanged
);
77 DOM
.useEntropy
.on("change", setEntropyVisibility
);
78 DOM
.entropy
.on("input", delayedEntropyChanged
);
79 DOM
.entropyMnemonicLength
.on("change", entropyChanged
);
80 DOM
.phrase
.on("input", delayedPhraseChanged
);
81 DOM
.passphrase
.on("input", delayedPhraseChanged
);
82 DOM
.generate
.on("click", generateClicked
);
83 DOM
.more
.on("click", showMore
);
84 DOM
.rootKey
.on("input", delayedRootKeyChanged
);
85 DOM
.bip32path
.on("input", calcForDerivationPath
);
86 DOM
.bip44purpose
.on("input", calcForDerivationPath
);
87 DOM
.bip44coin
.on("input", calcForDerivationPath
);
88 DOM
.bip44account
.on("input", calcForDerivationPath
);
89 DOM
.bip44change
.on("input", calcForDerivationPath
);
90 DOM
.tab
.on("shown.bs.tab", calcForDerivationPath
);
91 DOM
.hardenedAddresses
.on("change", calcForDerivationPath
);
92 DOM
.indexToggle
.on("click", toggleIndexes
);
93 DOM
.addressToggle
.on("click", toggleAddresses
);
94 DOM
.publicKeyToggle
.on("click", togglePublicKeys
);
95 DOM
.privateKeyToggle
.on("click", togglePrivateKeys
);
96 DOM
.languages
.on("click", languageChanged
);
97 setQrEvents(DOM
.showQrEls
);
100 hideValidationError();
101 populateNetworkSelect();
106 function networkChanged(e
) {
107 var networkIndex
= e
.target
.value
;
108 networks
[networkIndex
].onSelect();
117 function setEntropyVisibility() {
118 if (isUsingOwnEntropy()) {
119 DOM
.entropyContainer
.removeClass("hidden");
120 DOM
.generateContainer
.addClass("hidden");
121 DOM
.phrase
.prop("readonly", true);
126 DOM
.entropyContainer
.addClass("hidden");
127 DOM
.generateContainer
.removeClass("hidden");
128 DOM
.phrase
.prop("readonly", false);
133 function delayedPhraseChanged() {
134 hideValidationError();
136 if (phraseChangeTimeoutEvent
!= null) {
137 clearTimeout(phraseChangeTimeoutEvent
);
139 phraseChangeTimeoutEvent
= setTimeout(phraseChanged
, 400);
142 function phraseChanged() {
144 hideValidationError();
145 setMnemonicLanguage();
146 // Get the mnemonic phrase
147 var phrase
= DOM
.phrase
.val();
148 var errorText
= findPhraseErrors(phrase
);
150 showValidationError(errorText
);
153 // Calculate and display
154 var passphrase
= DOM
.passphrase
.val();
155 calcBip32RootKeyFromSeed(phrase
, passphrase
);
156 calcForDerivationPath();
160 function delayedEntropyChanged() {
161 hideValidationError();
163 if (entropyChangeTimeoutEvent
!= null) {
164 clearTimeout(entropyChangeTimeoutEvent
);
166 entropyChangeTimeoutEvent
= setTimeout(entropyChanged
, 400);
169 function entropyChanged() {
170 // If blank entropy, clear mnemonic, addresses, errors
171 if (DOM
.entropy
.val().trim().length
== 0) {
173 clearEntropyFeedback();
175 showValidationError("Blank entropy");
178 // Get the current phrase to detect changes
179 var phrase
= DOM
.phrase
.val();
180 // Set the phrase from the entropy
181 setMnemonicFromEntropy();
182 // Recalc addresses if the phrase has changed
183 var newPhrase
= DOM
.phrase
.val();
184 if (newPhrase
!= phrase
) {
185 if (newPhrase
.length
== 0) {
197 function delayedRootKeyChanged() {
198 // Warn if there is an existing mnemonic or passphrase.
199 if (DOM
.phrase
.val().length
> 0 || DOM
.passphrase
.val().length
> 0) {
200 if (!confirm("This will clear existing mnemonic and passphrase")) {
201 DOM
.rootKey
.val(bip32RootKey
);
205 hideValidationError();
207 // Clear existing mnemonic and passphrase
209 DOM
.passphrase
.val("");
211 if (rootKeyChangedTimeoutEvent
!= null) {
212 clearTimeout(rootKeyChangedTimeoutEvent
);
214 rootKeyChangedTimeoutEvent
= setTimeout(rootKeyChanged
, 400);
217 function rootKeyChanged() {
219 hideValidationError();
220 // Validate the root key TODO
221 var rootKeyBase58
= DOM
.rootKey
.val();
222 var errorText
= validateRootKey(rootKeyBase58
);
224 showValidationError(errorText
);
227 // Calculate and display
228 calcBip32RootKeyFromBase58(rootKeyBase58
);
229 calcForDerivationPath();
233 function calcForDerivationPath() {
235 hideValidationError();
236 // Get the derivation path
237 var derivationPath
= getDerivationPath();
238 var errorText
= findDerivationPathErrors(derivationPath
);
240 showValidationError(errorText
);
243 bip32ExtendedKey
= calcBip32ExtendedKey(derivationPath
);
244 if (bip44TabSelected()) {
251 function generateClicked() {
252 if (isUsingOwnEntropy()) {
257 setTimeout(function() {
258 setMnemonicLanguage();
259 var phrase
= generateRandomPhrase();
267 function languageChanged() {
268 setTimeout(function() {
269 setMnemonicLanguage();
270 if (DOM
.phrase
.val().length
> 0) {
271 var newPhrase
= convertPhraseToNewLanguage();
272 DOM
.phrase
.val(newPhrase
);
276 DOM
.generate
.trigger("click");
281 function toggleIndexes() {
282 showIndex
= !showIndex
;
283 $("td.index span").toggleClass("invisible");
286 function toggleAddresses() {
287 showAddress
= !showAddress
;
288 $("td.address span").toggleClass("invisible");
291 function togglePublicKeys() {
292 showPubKey
= !showPubKey
;
293 $("td.pubkey span").toggleClass("invisible");
296 function togglePrivateKeys() {
297 showPrivKey
= !showPrivKey
;
298 $("td.privkey span").toggleClass("invisible");
303 function generateRandomPhrase() {
304 if (!hasStrongRandom()) {
305 var errorText
= "This browser does not support strong randomness";
306 showValidationError(errorText
);
309 var numWords
= parseInt(DOM
.generatedStrength
.val());
310 var strength
= numWords
/ 3 * 32;
311 var words
= mnemonic
.generate(strength
);
312 DOM
.phrase
.val(words
);
316 function calcBip32RootKeyFromSeed(phrase
, passphrase
) {
317 seed
= mnemonic
.toSeed(phrase
, passphrase
);
318 bip32RootKey
= bitcoin
.HDNode
.fromSeedHex(seed
, network
);
321 function calcBip32RootKeyFromBase58(rootKeyBase58
) {
322 bip32RootKey
= bitcoin
.HDNode
.fromBase58(rootKeyBase58
, network
);
325 function calcBip32ExtendedKey(path
) {
326 var extendedKey
= bip32RootKey
;
327 // Derive the key from the path
328 var pathBits
= path
.split("/");
329 for (var i
=0; i
<pathBits
.length
; i
++) {
330 var bit
= pathBits
[i
];
331 var index
= parseInt(bit
);
335 var hardened
= bit
[bit
.length
-1] == "'";
337 extendedKey
= extendedKey
.deriveHardened(index
);
340 extendedKey
= extendedKey
.derive(index
);
346 function showValidationError(errorText
) {
352 function hideValidationError() {
358 function findPhraseErrors(phrase
) {
359 // Preprocess the words
360 phrase
= mnemonic
.normalizeString(phrase
);
361 var words
= phraseToWordArray(phrase
);
362 // Detect blank phrase
363 if (words
.length
== 0) {
364 return "Blank mnemonic";
367 for (var i
=0; i
<words
.length
; i
++) {
369 var language
= getLanguage();
370 if (WORDLISTS
[language
].indexOf(word
) == -1) {
371 console
.log("Finding closest match to " + word
);
372 var nearestWord
= findNearestWord(word
);
373 return word
+ " not in wordlist, did you mean " + nearestWord
+ "?";
376 // Check the words are valid
377 var properPhrase
= wordArrayToPhrase(words
);
378 var isValid
= mnemonic
.check(properPhrase
);
380 return "Invalid mnemonic";
385 function validateRootKey(rootKeyBase58
) {
387 bitcoin
.HDNode
.fromBase58(rootKeyBase58
);
390 return "Invalid root key";
395 function getDerivationPath() {
396 if (bip44TabSelected()) {
397 var purpose
= parseIntNoNaN(DOM
.bip44purpose
.val(), 44);
398 var coin
= parseIntNoNaN(DOM
.bip44coin
.val(), 0);
399 var account
= parseIntNoNaN(DOM
.bip44account
.val(), 0);
400 var change
= parseIntNoNaN(DOM
.bip44change
.val(), 0);
402 path
+= purpose
+ "'/";
404 path
+= account
+ "'/";
406 DOM
.bip44path
.val(path
);
407 var derivationPath
= DOM
.bip44path
.val();
408 console
.log("Using derivation path from BIP44 tab: " + derivationPath
);
409 return derivationPath
;
411 else if (bip32TabSelected()) {
412 var derivationPath
= DOM
.bip32path
.val();
413 console
.log("Using derivation path from BIP32 tab: " + derivationPath
);
414 return derivationPath
;
417 console
.log("Unknown derivation path");
421 function findDerivationPathErrors(path
) {
422 // TODO is not perfect but is better than nothing
424 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
426 // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys
427 var maxDepth
= 255; // TODO verify this!!
428 var maxIndexValue
= Math
.pow(2, 31); // TODO verify this!!
429 if (path
[0] != "m") {
430 return "First character must be 'm'";
432 if (path
.length
> 1) {
433 if (path
[1] != "/") {
434 return "Separator must be '/'";
436 var indexes
= path
.split("/");
437 if (indexes
.length
> maxDepth
) {
438 return "Derivation depth is " + indexes
.length
+ ", must be less than " + maxDepth
;
440 for (var depth
= 1; depth
<indexes
.length
; depth
++) {
441 var index
= indexes
[depth
];
442 var invalidChars
= index
.replace(/^[0-9]+'?$/g, "")
443 if (invalidChars
.length
> 0) {
444 return "Invalid characters " + invalidChars
+ " found at depth " + depth
;
446 var indexValue
= parseInt(index
.replace("'", ""));
448 return "Invalid number at depth " + depth
;
450 if (indexValue
> maxIndexValue
) {
451 return "Value of " + indexValue
+ " at depth " + depth
+ " must be less than " + maxIndexValue
;
458 function displayBip44Info() {
459 // Get the derivation path for the account
460 var purpose
= parseIntNoNaN(DOM
.bip44purpose
.val(), 44);
461 var coin
= parseIntNoNaN(DOM
.bip44coin
.val(), 0);
462 var account
= parseIntNoNaN(DOM
.bip44account
.val(), 0);
464 path
+= purpose
+ "'/";
466 path
+= account
+ "'/";
467 // Calculate the account extended keys
468 var accountExtendedKey
= calcBip32ExtendedKey(path
);
469 var accountXprv
= accountExtendedKey
.toBase58();
470 var accountXpub
= accountExtendedKey
.toBase58(false);
471 // Display the extended keys
472 DOM
.bip44accountXprv
.val(accountXprv
);
473 DOM
.bip44accountXpub
.val(accountXpub
);
476 function displayBip32Info() {
479 var rootKey
= bip32RootKey
.toBase58();
480 DOM
.rootKey
.val(rootKey
);
481 var extendedPrivKey
= bip32ExtendedKey
.toBase58();
482 DOM
.extendedPrivKey
.val(extendedPrivKey
);
483 var extendedPubKey
= bip32ExtendedKey
.toBase58(false);
484 DOM
.extendedPubKey
.val(extendedPubKey
);
485 // Display the addresses and privkeys
486 clearAddressesList();
487 displayAddresses(0, 20);
490 function displayAddresses(start
, total
) {
491 for (var i
=0; i
<total
; i
++) {
492 var index
= i
+ start
;
497 function TableRow(index
) {
499 var useHardenedAddresses
= DOM
.hardenedAddresses
.prop("checked");
505 function calculateValues() {
506 setTimeout(function() {
508 if (useHardenedAddresses
) {
509 key
= bip32ExtendedKey
.deriveHardened(index
);
512 key
= bip32ExtendedKey
.derive(index
);
514 var address
= key
.getAddress().toString();
515 var privkey
= key
.privKey
.toWIF(network
);
516 var pubkey
= key
.pubKey
.toHex();
517 var indexText
= getDerivationPath() + "/" + index
;
518 if (useHardenedAddresses
) {
519 indexText
= indexText
+ "'";
521 addAddressToList(indexText
, address
, pubkey
, privkey
);
529 function showMore() {
530 var start
= DOM
.addresses
.children().length
;
531 var rowsToAdd
= parseInt(DOM
.rowsToAdd
.val());
532 if (isNaN(rowsToAdd
)) {
534 DOM
.rowsToAdd
.val("20");
536 if (rowsToAdd
> 200) {
537 var msg
= "Generating " + rowsToAdd
+ " rows could take a while. ";
538 msg
+= "Do you want to continue?";
543 displayAddresses(start
, rowsToAdd
);
546 function clearDisplay() {
547 clearAddressesList();
549 hideValidationError();
552 function clearAddressesList() {
553 DOM
.addresses
.empty();
556 function clearKey() {
558 DOM
.extendedPrivKey
.val("");
559 DOM
.extendedPubKey
.val("");
562 function addAddressToList(indexText
, address
, pubkey
, privkey
) {
563 var row
= $(addressRowTemplate
.html());
565 var indexCell
= row
.find(".index span");
566 var addressCell
= row
.find(".address span");
567 var pubkeyCell
= row
.find(".pubkey span");
568 var privkeyCell
= row
.find(".privkey span");
570 indexCell
.text(indexText
);
571 addressCell
.text(address
);
572 pubkeyCell
.text(pubkey
);
573 privkeyCell
.text(privkey
);
576 indexCell
.addClass("invisible");
579 addressCell
.addClass("invisible");
582 pubkeyCell
.addClass("invisible");
585 privkeyCell
.addClass("invisible");
587 DOM
.addresses
.append(row
);
588 var rowShowQrEls
= row
.find("[data-show-qr]");
589 setQrEvents(rowShowQrEls
);
592 function hasStrongRandom() {
593 return 'crypto' in window
&& window
['crypto'] !== null;
596 function disableForms() {
597 $("form").on("submit", function(e
) {
602 function parseIntNoNaN(val
, defaultVal
) {
603 var v
= parseInt(val
);
610 function showPending() {
612 .text("Calculating...")
616 function findNearestWord(word
) {
617 var language
= getLanguage();
618 var words
= WORDLISTS
[language
];
619 var minDistance
= 99;
620 var closestWord
= words
[0];
621 for (var i
=0; i
<words
.length
; i
++) {
622 var comparedTo
= words
[i
];
623 var distance
= Levenshtein
.get(word
, comparedTo
);
624 if (distance
< minDistance
) {
625 closestWord
= comparedTo
;
626 minDistance
= distance
;
632 function hidePending() {
638 function populateNetworkSelect() {
639 for (var i
=0; i
<networks
.length
; i
++) {
640 var network
= networks
[i
];
641 var option
= $("<option>");
642 option
.attr("value", i
);
643 option
.text(network
.name
);
644 DOM
.phraseNetwork
.append(option
);
648 function getLanguage() {
649 var defaultLanguage
= "english";
650 // Try to get from existing phrase
651 var language
= getLanguageFromPhrase();
652 // Try to get from url if not from phrase
653 if (language
.length
== 0) {
654 language
= getLanguageFromUrl();
656 // Default to English if no other option
657 if (language
.length
== 0) {
658 language
= defaultLanguage
;
663 function getLanguageFromPhrase(phrase
) {
664 // Check if how many words from existing phrase match a language.
667 phrase
= DOM
.phrase
.val();
669 if (phrase
.length
> 0) {
670 var words
= phraseToWordArray(phrase
);
671 var languageMatches
= {};
672 for (l
in WORDLISTS
) {
673 // Track how many words match in this language
674 languageMatches
[l
] = 0;
675 for (var i
=0; i
<words
.length
; i
++) {
676 var wordInLanguage
= WORDLISTS
[l
].indexOf(words
[i
]) > -1;
677 if (wordInLanguage
) {
678 languageMatches
[l
]++;
681 // Find languages with most word matches.
682 // This is made difficult due to commonalities between Chinese
683 // simplified vs traditional.
685 var mostMatchedLanguages
= [];
686 for (var l
in languageMatches
) {
687 var numMatches
= languageMatches
[l
];
688 if (numMatches
> mostMatches
) {
689 mostMatches
= numMatches
;
690 mostMatchedLanguages
= [l
];
692 else if (numMatches
== mostMatches
) {
693 mostMatchedLanguages
.push(l
);
697 if (mostMatchedLanguages
.length
> 0) {
698 // Use first language and warn if multiple detected
699 language
= mostMatchedLanguages
[0];
700 if (mostMatchedLanguages
.length
> 1) {
701 console
.warn("Multiple possible languages");
702 console
.warn(mostMatchedLanguages
);
709 function getLanguageFromUrl() {
710 for (var language
in WORDLISTS
) {
711 if (window
.location
.hash
.indexOf(language
) > -1) {
718 function setMnemonicLanguage() {
719 var language
= getLanguage();
720 // Load the bip39 mnemonic generator for this language if required
721 if (!(language
in mnemonics
)) {
722 mnemonics
[language
] = new Mnemonic(language
);
724 mnemonic
= mnemonics
[language
];
727 function convertPhraseToNewLanguage() {
728 var oldLanguage
= getLanguageFromPhrase();
729 var newLanguage
= getLanguageFromUrl();
730 var oldPhrase
= DOM
.phrase
.val();
731 var oldWords
= phraseToWordArray(oldPhrase
);
733 for (var i
=0; i
<oldWords
.length
; i
++) {
734 var oldWord
= oldWords
[i
];
735 var index
= WORDLISTS
[oldLanguage
].indexOf(oldWord
);
736 var newWord
= WORDLISTS
[newLanguage
][index
];
737 newWords
.push(newWord
);
739 newPhrase
= wordArrayToPhrase(newWords
);
743 // TODO look at jsbip39 - mnemonic.splitWords
744 function phraseToWordArray(phrase
) {
745 var words
= phrase
.split(/\s/g);
747 for (var i
=0; i
<words
.length
; i
++) {
749 if (word
.length
> 0) {
756 // TODO look at jsbip39 - mnemonic.joinWords
757 function wordArrayToPhrase(words
) {
758 var phrase
= words
.join(" ");
759 var language
= getLanguageFromPhrase(phrase
);
760 if (language
== "japanese") {
761 phrase
= words
.join("\u3000");
766 function isUsingOwnEntropy() {
767 return DOM
.useEntropy
.prop("checked");
770 function setMnemonicFromEntropy() {
771 clearEntropyFeedback();
773 var entropyStr
= DOM
.entropy
.val();
774 // Work out minimum base for entropy
775 var entropy
= Entropy
.fromString(entropyStr
);
776 if (entropy
.binaryStr
.length
== 0) {
779 // Show entropy details
780 showEntropyFeedback(entropy
);
781 // Use entropy hash if not using raw entropy
782 var bits
= entropy
.binaryStr
;
783 var mnemonicLength
= DOM
.entropyMnemonicLength
.val();
784 if (mnemonicLength
!= "raw") {
785 // Get bits by hashing entropy with SHA256
786 var hash
= sjcl
.hash
.sha256
.hash(entropy
.cleanStr
);
787 var hex
= sjcl
.codec
.hex
.fromBits(hash
);
788 bits
= BigInteger
.parse(hex
, 16).toString(2);
789 for (var i
=0; i
<256-bits
.length
; i
++) {
792 // Truncate hash to suit number of words
793 mnemonicLength
= parseInt(mnemonicLength
);
794 var numberOfBits
= 32 * mnemonicLength
/ 3;
795 bits
= bits
.substring(0, numberOfBits
);
797 // Discard trailing entropy
798 var bitsToUse
= Math
.floor(bits
.length
/ 32) * 32;
799 var start
= bits
.length
- bitsToUse
;
800 var binaryStr
= bits
.substring(start
);
801 // Convert entropy string to numeric array
803 for (var i
=0; i
<binaryStr
.length
/ 8; i
++) {
804 var byteAsBits
= binaryStr
.substring(i
*8, i
*8+8);
805 var entropyByte
= parseInt(byteAsBits
, 2);
806 entropyArr
.push(entropyByte
)
808 // Convert entropy array to mnemonic
809 var phrase
= mnemonic
.toMnemonic(entropyArr
);
810 // Set the mnemonic in the UI
811 DOM
.phrase
.val(phrase
);
814 function clearEntropyFeedback() {
815 DOM
.entropyStrength
.text("...");
816 DOM
.entropyType
.text("");
817 DOM
.entropyWordCount
.text("0");
818 DOM
.entropyEventCount
.text("0");
819 DOM
.entropyBitsPerEvent
.text("0");
820 DOM
.entropyBits
.text("0");
821 DOM
.entropyFiltered
.html(" ");
822 DOM
.entropyBinary
.html(" ");
825 function showEntropyFeedback(entropy
) {
826 var numberOfBits
= entropy
.binaryStr
.length
;
827 var strength
= "extremely weak";
828 if (numberOfBits
>= 64) {
829 strength
= "very weak";
831 if (numberOfBits
>= 96) {
834 if (numberOfBits
>= 128) {
837 if (numberOfBits
>= 160) {
838 strength
= "very strong";
840 if (numberOfBits
>= 192) {
841 strength
= "extremely strong";
843 // If time to crack is less than one day, and password is considered
844 // strong or better based on the number of bits, rename strength to
847 var z
= zxcvbn(entropy
.base
.parts
.join(""));
848 var timeToCrack
= z
.crack_times_seconds
.offline_fast_hashing_1e10_per_second
;
849 if (timeToCrack
< 86400 && entropy
.binaryStr
.length
>= 128) {
850 strength
= "easily cracked";
851 if (z
.feedback
.warning
!= "") {
852 strength
= strength
+ " - " + z
.feedback
.warning
;
857 strength
= "unknown";
858 console
.log("Error detecting entropy strength with zxcvbn:");
861 var entropyTypeStr
= getEntropyTypeStr(entropy
);
862 var wordCount
= Math
.floor(numberOfBits
/ 32) * 3;
863 var bitsPerEvent
= entropy
.bitsPerEvent
.toFixed(2);
864 DOM
.entropyFiltered
.html(entropy
.cleanHtml
);
865 DOM
.entropyType
.text(entropyTypeStr
);
866 DOM
.entropyStrength
.text(strength
);
867 DOM
.entropyEventCount
.text(entropy
.base
.ints
.length
);
868 DOM
.entropyBits
.text(numberOfBits
);
869 DOM
.entropyWordCount
.text(wordCount
);
870 DOM
.entropyBinary
.text(entropy
.binaryStr
);
871 DOM
.entropyBitsPerEvent
.text(bitsPerEvent
);
874 function getEntropyTypeStr(entropy
) {
875 var typeStr
= entropy
.base
.str
;
876 // Add some detail if these are cards
877 if (entropy
.base
.asInt
== 52) {
878 var cardDetail
= []; // array of message strings
881 var dupeTracker
= {};
882 for (var i
=0; i
<entropy
.base
.parts
.length
; i
++) {
883 var card
= entropy
.base
.parts
[i
];
884 var cardUpper
= card
.toUpperCase();
885 if (cardUpper
in dupeTracker
) {
888 dupeTracker
[cardUpper
] = true;
890 if (dupes
.length
> 0) {
891 var dupeWord
= "duplicates";
892 if (dupes
.length
== 1) {
893 dupeWord
= "duplicate";
895 var msg
= dupes
.length
+ " " + dupeWord
+ ": " + dupes
.slice(0,3).join(" ");
896 if (dupes
.length
> 3) {
899 cardDetail
.push(msg
);
902 var uniqueCards
= [];
903 for (var uniqueCard
in dupeTracker
) {
904 uniqueCards
.push(uniqueCard
);
906 if (uniqueCards
.length
== 52) {
907 cardDetail
.unshift("full deck");
909 // Detect missing cards
910 var values
= "A23456789TJQK";
912 var missingCards
= [];
913 for (var i
=0; i
<suits
.length
; i
++) {
914 for (var j
=0; j
<values
.length
; j
++) {
915 var card
= values
[j
] + suits
[i
];
916 if (!(card
in dupeTracker
)) {
917 missingCards
.push(card
);
921 // Display missing cards if six or less, ie clearly going for full deck
922 if (missingCards
.length
> 0 && missingCards
.length
<= 6) {
923 var msg
= missingCards
.length
+ " missing: " + missingCards
.slice(0,3).join(" ");
924 if (missingCards
.length
> 3) {
927 cardDetail
.push(msg
);
929 // Add card details to typeStr
930 if (cardDetail
.length
> 0) {
931 typeStr
+= " (" + cardDetail
.join(", ") + ")";
937 function setQrEvents(els
) {
938 els
.on("mouseenter", createQr
);
939 els
.on("mouseleave", destroyQr
);
940 els
.on("click", toggleQr
);
943 function createQr(e
) {
944 var content
= e
.target
.textContent
|| e
.target
.value
;
947 DOM
.qrImage
.qrcode({width: size
, height: size
, text: content
});
949 DOM
.qrImage
.addClass("hidden");
951 DOM
.qrContainer
.removeClass("hidden");
955 function destroyQr() {
956 DOM
.qrImage
.text("");
957 DOM
.qrContainer
.addClass("hidden");
960 function toggleQr() {
962 DOM
.qrImage
.toggleClass("hidden");
963 DOM
.qrHint
.toggleClass("hidden");
966 function bip44TabSelected() {
967 return DOM
.bip44tab
.hasClass("active");
970 function bip32TabSelected() {
971 return DOM
.bip32tab
.hasClass("active");
977 onSelect: function() {
978 network
= bitcoin
.networks
.bitcoin
;
979 DOM
.bip44coin
.val(0);
983 name: "Bitcoin Testnet",
984 onSelect: function() {
985 network
= bitcoin
.networks
.testnet
;
986 DOM
.bip44coin
.val(1);
991 onSelect: function() {
992 network
= bitcoin
.networks
.litecoin
;
993 DOM
.bip44coin
.val(2);
998 onSelect: function() {
999 network
= bitcoin
.networks
.dogecoin
;
1000 DOM
.bip44coin
.val(3);
1005 onSelect: function() {
1006 network
= bitcoin
.networks
.shadow
;
1007 DOM
.bip44coin
.val(35);
1011 name: "ShadowCash Testnet",
1012 onSelect: function() {
1013 network
= bitcoin
.networks
.shadowtn
;
1014 DOM
.bip44coin
.val(1);
1019 onSelect: function() {
1020 network
= bitcoin
.networks
.viacoin
;
1021 DOM
.bip44coin
.val(14);
1025 name: "Viacoin Testnet",
1026 onSelect: function() {
1027 network
= bitcoin
.networks
.viacointestnet
;
1028 DOM
.bip44coin
.val(1);
1033 onSelect: function() {
1034 network
= bitcoin
.networks
.jumbucks
;
1035 DOM
.bip44coin
.val(26);
1040 onSelect: function() {
1041 network
= bitcoin
.networks
.clam
;
1042 DOM
.bip44coin
.val(23);
1047 onSelect: function() {
1048 network
= bitcoin
.networks
.dash
;
1049 DOM
.bip44coin
.val(5);
1054 onSelect: function() {
1055 network
= bitcoin
.networks
.namecoin
;
1056 DOM
.bip44coin
.val(7);
1061 onSelect: function() {
1062 network
= bitcoin
.networks
.peercoin
;
1063 DOM
.bip44coin
.val(6);