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