Entropy can be supplied by user

This commit is contained in:
Ian Coleman
2016-11-03 16:34:56 +11:00
parent d737abf680
commit c6624d51f4
5 changed files with 4465 additions and 14 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -65,12 +65,14 @@
<div class="col-md-12">
<h2>Mnemonic</h2>
<form class="form-horizontal" role="form">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<p>You can enter an existing BIP39 mnemonic, or generate a new random one. Typing your own twelve words will probably not work how you expect, since the words require a particular structure (the last word is a checksum)</p>
<p>For more info see the <a href="https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki" target="_blank">BIP39 spec</a></p>
</div>
<div class="form-group">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<p>You can enter an existing BIP39 mnemonic, or generate a new random one. Typing your own twelve words will probably not work how you expect, since the words require a particular structure (the last word is a checksum)</p>
<p>For more info see the <a href="https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki" target="_blank">BIP39 spec</a></p>
</div>
</div>
<div class="form-group generate-container">
<label class="col-sm-2 control-label"></label>
<div class="col-sm-10">
<div class="form-inline">
@@ -92,7 +94,30 @@
</div>
</div>
</div>
<div class="form-group">
<div class="entropy-container hidden">
<label for="entropy" class="col-sm-2 control-label">Entropy</label>
<div class="col-sm-10">
<input id="entropy" class="entropy form-control" placeholder="Accepts binary, base 6, 6-sided dice, base 10, hexadecimal">
<span class="help-block">
<div class="text-danger">
This is an advanced feature.
Your mnemonic may be insecure if this feature is used incorrectly.
<a href="#entropy-notes">Read more</a>
</div>
<div class="text-danger entropy-error"></div>
</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-2"></div>
<div class="col-sm-10 checkbox">
<label>
<input type="checkbox" class="use-entropy">
Supply my own source of entropy
</label>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label"></label>
<div class="col-sm-10 languages">
<a href="#english">English</a>
@@ -353,6 +378,24 @@
but be careful - it can be easy to make mistakes if you
don't know what you're doing
</p>
<h3 id="entropy-notes">Entropy</h3>
<p>
Entropy values must be sourced from a
<a href="https://en.wikipedia.org/wiki/Random_number_generation" target="_blank">strong source of randomness</a>.
This means flipping a fair coin, rolling a fair dice, noise measurements etc. Do <strong>NOT</strong> use
phrases from books, lyrics from songs, your birthday or steet address, keyboard mashing, or anything you <i>think</i>
is random, because chances are <em>overwhelming</em> that it isn't random enough for the needs of this tool.
</p>
<p>
The random mnemonic generator on this page uses a
<a href="https://developer.mozilla.org/en-US/docs/Web/API/RandomSource/getRandomValues" target="_blank">cryptographically secure random number generator</a>,
and can generally be trusted more than your own intuition about randomness.
If cryptographic randomness isn't available in your browser, this page will show a warning and <i>will not generate
random mnemonics</i>.
</p>
<p>
<a href="https://bitcointalk.org/index.php?topic=311000.msg3345309#msg3345309" target="_blank">You are not a good source of entropy.</a>
</p>
</div>
</div>
@@ -465,6 +508,7 @@
<script src="js/wordlist_french.js"></script>
<script src="js/wordlist_italian.js"></script>
<script src="js/jsbip39.js"></script>
<script src="js/entropy.js"></script>
<script src="js/index.js"></script>
</body>
</html>

1774
src/js/entropy.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,14 +14,20 @@
var showPubKey = true;
var showPrivKey = true;
var entropyChangeTimeoutEvent = null;
var phraseChangeTimeoutEvent = null;
var rootKeyChangedTimeoutEvent = null;
var DOM = {};
DOM.network = $(".network");
DOM.phraseNetwork = $("#network-phrase");
DOM.useEntropy = $(".use-entropy");
DOM.entropyContainer = $(".entropy-container");
DOM.entropy = $(".entropy");
DOM.entropyError = $(".entropy-error");
DOM.phrase = $(".phrase");
DOM.passphrase = $(".passphrase");
DOM.generateContainer = $(".generate-container");
DOM.generate = $(".generate");
DOM.seed = $(".seed");
DOM.rootKey = $(".root-key");
@@ -53,6 +59,8 @@
function init() {
// Events
DOM.network.on("change", networkChanged);
DOM.useEntropy.on("change", setEntropyVisibility);
DOM.entropy.on("input", delayedEntropyChanged);
DOM.phrase.on("input", delayedPhraseChanged);
DOM.passphrase.on("input", delayedPhraseChanged);
DOM.generate.on("click", generateClicked);
@@ -89,6 +97,21 @@
}
}
function setEntropyVisibility() {
if (isUsingOwnEntropy()) {
DOM.entropyContainer.removeClass("hidden");
DOM.generateContainer.addClass("hidden");
DOM.phrase.prop("readonly", true);
DOM.entropy.focus();
entropyChanged();
}
else {
DOM.entropyContainer.addClass("hidden");
DOM.generateContainer.removeClass("hidden");
DOM.phrase.prop("readonly", false);
}
}
function delayedPhraseChanged() {
hideValidationError();
showPending();
@@ -116,6 +139,20 @@
hidePending();
}
function delayedEntropyChanged() {
hideValidationError();
showPending();
if (entropyChangeTimeoutEvent != null) {
clearTimeout(entropyChangeTimeoutEvent);
}
entropyChangeTimeoutEvent = setTimeout(entropyChanged, 400);
}
function entropyChanged() {
setMnemonicFromEntropy();
phraseChanged();
}
function delayedRootKeyChanged() {
// Warn if there is an existing mnemonic or passphrase.
if (DOM.phrase.val().length > 0 || DOM.passphrase.val().length > 0) {
@@ -168,6 +205,9 @@
}
function generateClicked() {
if (isUsingOwnEntropy()) {
return;
}
clearDisplay();
showPending();
setTimeout(function() {
@@ -599,7 +639,12 @@
}
function getLanguageFromUrl() {
return window.location.hash.substring(1);
for (var language in WORDLISTS) {
if (window.location.hash.indexOf(language) > -1) {
return language;
}
}
return "";
}
function setMnemonicLanguage() {
@@ -650,6 +695,65 @@
return phrase;
}
function isUsingOwnEntropy() {
return DOM.useEntropy.prop("checked");
}
function setMnemonicFromEntropy() {
hideEntropyError();
// Work out minimum base for entropy
var entropyStr = DOM.entropy.val();
var entropy = Entropy.fromString(entropyStr);
if (entropy.hexStr.length == 0) {
return;
}
// Show entropy details
var extraBits = 32 - (entropy.binaryStr.length % 32);
var extraChars = Math.ceil(extraBits * Math.log(2) / Math.log(entropy.base.asInt));
var strength = "an extremely weak";
if (entropy.hexStr.length >= 8) {
strength = "a very weak";
}
if (entropy.hexStr.length >= 12) {
strength = "a weak";
}
if (entropy.hexStr.length >= 24) {
strength = "a strong";
}
if (entropy.hexStr.length >= 32) {
strength = "a very strong";
}
if (entropy.hexStr.length >= 40) {
strength = "an extremely strong";
}
if (entropy.hexStr.length >=48) {
strength = "an even stronger"
}
var msg = "Have " + entropy.binaryStr.length + " bits of entropy, " + extraChars + " more " + entropy.base.str + " chars required to generate " + strength + " mnemonic: " + entropy.cleanStr;
showEntropyError(msg);
// Discard trailing entropy
var hexStr = entropy.hexStr.substring(0, Math.floor(entropy.hexStr.length / 8) * 8);
// Convert entropy string to numeric array
var entropyArr = [];
for (var i=0; i<hexStr.length / 2; i++) {
var entropyByte = parseInt(hexStr[i*2].concat(hexStr[i*2+1]), 16);
entropyArr.push(entropyByte)
}
// Convert entropy array to mnemonic
var phrase = mnemonic.toMnemonic(entropyArr);
// Set the mnemonic in the UI
DOM.phrase.val(phrase);
}
function hideEntropyError() {
DOM.entropyError.addClass("hidden");
}
function showEntropyError(msg) {
DOM.entropyError.text(msg);
DOM.entropyError.removeClass("hidden");
}
var networks = [
{
name: "Bitcoin",

607
tests.js
View File

@@ -1960,6 +1960,613 @@ page.open(url, function(status) {
});
},
// Entropy unit tests
function() {
page.open(url, function(status) {
var error = page.evaluate(function() {
var e;
// binary entropy is detected
e = Entropy.fromString("01010101");
if (e.base.str != "binary") {
return "Binary entropy not detected correctly";
}
// base6 entropy is detected
e = Entropy.fromString("012345012345");
if (e.base.str != "base 6") {
return "base6 entropy not detected correctly";
}
// dice entropy is detected
e = Entropy.fromString("123456123456");
if (e.base.str != "base 6 (dice)") {
return "dice entropy not detected correctly";
}
// base10 entropy is detected
e = Entropy.fromString("0123456789");
if (e.base.str != "base 10") {
return "base10 entropy not detected correctly";
}
// hex entropy is detected
e = Entropy.fromString("0123456789ABCDEF");
if (e.base.str != "hexadecimal") {
return "hexadecimal entropy not detected correctly";
}
// entropy is case insensitive
e = Entropy.fromString("aBcDeF");
if (e.cleanStr != "aBcDeF") {
return "Entropy should not be case sensitive";
}
// dice entropy is converted to base6
e = Entropy.fromString("123456");
if (e.cleanStr != "012345") {
return "Dice entropy is not automatically converted to base6";
}
// dice entropy is preferred to base6 if ambiguous
e = Entropy.fromString("12345");
if (e.base.str != "base 6 (dice)") {
return "dice not used as default over base 6";
}
// unused characters are ignored
e = Entropy.fromString("fghijkl");
if (e.cleanStr != "f") {
return "additional characters are not ignored";
}
// the lowest base is used by default
// 7 could be decimal or hexadecimal, but should be detected as decimal
e = Entropy.fromString("7");
if (e.base.str != "base 10") {
return "lowest base is not used";
}
// Hexadecimal representation is returned
e = Entropy.fromString("1010");
if (e.hexStr != "A") {
return "Hexadecimal representation not returned";
}
// Leading zeros are retained
e = Entropy.fromString("000A");
if (e.cleanStr != "000A") {
return "Leading zeros are not retained";
}
// Leading zeros are correctly preserved for hex in binary string
e = Entropy.fromString("2A");
if (e.binaryStr != "00101010") {
return "Hex leading zeros are not correct in binary";
}
// Keyboard mashing results in weak entropy
// Despite being a long string, it's less than 30 bits of entropy
e = Entropy.fromString("aj;se ifj; ask,dfv js;ifj");
if (e.binaryStr.length >= 30) {
return "Keyboard mashing should produce weak entropy";
}
return false;
});
if (error) {
console.log("Entropy unit tests");
console.log(error);
fail();
};
next();
});
},
// Entropy can be entered by the user
function() {
page.open(url, function(status) {
expected = {
mnemonic: "abandon abandon ability",
address: "1Di3Vp7tBWtyQaDABLAjfWtF6V7hYKJtug",
}
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
$(".entropy").val("00000000 00000000 00000000 00000000").trigger("input");
});
// check the mnemonic is set and address is correct
waitForGenerate(function() {
var actual = page.evaluate(function() {
return {
address: $(".address:first").text(),
mnemonic: $(".phrase").val(),
}
});
if (actual.mnemonic != expected.mnemonic) {
console.log("Entropy does not generate correct mnemonic");
console.log("Expected: " + expected.mnemonic);
console.log("Got: " + actual.mnemonic);
fail();
}
if (actual.address != expected.address) {
console.log("Entropy does not generate correct address");
console.log("Expected: " + expected.address);
console.log("Got: " + actual.address);
fail();
}
next();
});
});
},
// A warning about entropy is shown to the user, with additional information
function() {
page.open(url, function(status) {
// get text content from entropy sections of page
var hasWarning = page.evaluate(function() {
var entropyText = $(".entropy-container").text();
var warning = "mnemonic may be insecure";
if (entropyText.indexOf(warning) == -1) {
return false;
}
var readMoreText = $("#entropy-notes").parent().text();
var goodSources = "flipping a fair coin, rolling a fair dice, noise measurements etc";
if (readMoreText.indexOf(goodSources) == -1) {
return false;
}
return true;
});
// check the warnings and information are shown
if (!hasWarning) {
console.log("Page does not contain warning about using own entropy");
fail();
}
next();
});
},
// The types of entropy available are described to the user
function() {
page.open(url, function(status) {
// get placeholder text for entropy field
var placeholder = page.evaluate(function() {
return $(".entropy").attr("placeholder");
});
var options = [
"binary",
"base 6",
"dice",
"base 10",
"hexadecimal",
];
for (var i=0; i<options.length; i++) {
var option = options[i];
if (placeholder.indexOf(option) == -1) {
console.log("Available entropy type is not shown to user: " + option);
fail();
}
}
next();
});
},
// The actual entropy used is shown to the user
function() {
page.open(url, function(status) {
// use entropy
var badEntropySource = page.evaluate(function() {
var entropy = "Not A Very Good Entropy Source At All";
$(".use-entropy").prop("checked", true).trigger("change");
$(".entropy").val(entropy).trigger("input");
});
// check the actual entropy being used is shown
waitForGenerate(function() {
var expectedText = "AedEceAA";
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf(expectedText) == -1) {
console.log("Actual entropy used is not shown");
fail();
}
next();
});
});
},
// Binary entropy can be entered
function() {
page.open(url, function(status) {
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
$(".entropy").val("01").trigger("input");
});
// check the entropy is shown to be the correct type
waitForGenerate(function() {
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf("binary") == -1) {
console.log("Binary entropy is not detected and presented to user");
fail();
}
next();
});
});
},
// Base 6 entropy can be entered
function() {
page.open(url, function(status) {
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
$(".entropy").val("012345").trigger("input");
});
// check the entropy is shown to be the correct type
waitForGenerate(function() {
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf("base 6") == -1) {
console.log("Base 6 entropy is not detected and presented to user");
fail();
}
next();
});
});
},
// Base 6 dice entropy can be entered
function() {
page.open(url, function(status) {
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
$(".entropy").val("123456").trigger("input");
});
// check the entropy is shown to be the correct type
waitForGenerate(function() {
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf("dice") == -1) {
console.log("Dice entropy is not detected and presented to user");
fail();
}
next();
});
});
},
// Base 10 entropy can be entered
function() {
page.open(url, function(status) {
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
$(".entropy").val("789").trigger("input");
});
// check the entropy is shown to be the correct type
waitForGenerate(function() {
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf("base 10") == -1) {
console.log("Base 10 entropy is not detected and presented to user");
fail();
}
next();
});
});
},
// Hexadecimal entropy can be entered
function() {
page.open(url, function(status) {
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
$(".entropy").val("abcdef").trigger("input");
});
// check the entropy is shown to be the correct type
waitForGenerate(function() {
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf("hexadecimal") == -1) {
console.log("Hexadecimal entropy is not detected and presented to user");
fail();
}
next();
});
});
},
// Dice entropy value is shown as the converted base 6 value
function() {
page.open(url, function(status) {
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
$(".entropy").val("123456").trigger("input");
});
// check the entropy is shown as base 6, not as the original dice value
waitForGenerate(function() {
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf("012345") == -1) {
console.log("Dice entropy is not shown to user as base 6 value");
fail();
}
if (entropyText.indexOf("123456") > -1) {
console.log("Dice entropy value is shown instead of true base 6 value");
fail();
}
next();
});
});
},
// The number of bits of entropy accumulated is shown
function() {
page.open(url, function(status) {
var tests = {
"0000 0000 0000 0000 0000": "20",
"0": "1",
"0000": "4",
"6": "3",
"7": "3",
"8": "4",
"F": "4",
"29": "5",
"0A": "8",
"1A": "8", // hex is always multiple of 4 bits of entropy
"2A": "8",
"4A": "8",
"8A": "8",
"FA": "8",
"000A": "16",
"2220": "10",
"2221": "9", // uses dice, so entropy is actually 1110
"2227": "12",
"222F": "16",
"FFFF": "16",
}
// Arrange tests in array so last one can be easily detected
var entropys = [];
var results = [];
for (var entropy in tests) {
entropys.push(entropy);
results.push(tests[entropy]);
}
// use entropy
page.evaluate(function(e) {
$(".use-entropy").prop("checked", true).trigger("change");
});
// Run each test
var nextTest = function runNextTest(i) {
var entropy = entropys[i];
var expected = results[i];
// set entropy
page.evaluate(function(e) {
$(".addresses").empty(); // bit of a hack, but needed for waitForGenerate
$(".entropy").val(e).trigger("input");
}, entropy);
// check the number of bits of entropy is shown
waitForGenerate(function() {
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf("Have " + expected + " bits of entropy") == -1) {
console.log("Accumulated entropy is not shown correctly for " + entropy);
fail();
}
var isLastTest = i == results.length - 1;
if (isLastTest) {
next();
}
else {
runNextTest(i+1);
}
});
}
nextTest(0);
});
},
// The number of bits of entropy to reach the next mnemonic strength is shown
function() {
page.open(url, function(status) {
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
$(".entropy").val("7654321").trigger("input");
});
// check the amount of additional entropy required is shown
waitForGenerate(function() {
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf("3 more base 10 chars required") == -1) {
console.log("Additional entropy requirement is not shown");
fail();
}
next();
});
});
},
// The next strength above 0-word mnemonics is considered extremely weak
// The next strength above 3-word mnemonics is considered very weak
// The next strength above 6-word mnemonics is considered weak
// The next strength above 9-word mnemonics is considered strong
// The next strength above 12-word mnemonics is considered very strong
// The next strength above 15-word mnemonics is considered extremely strong
function() {
page.open(url, function(status) {
var tests = [
{
entropy: "A",
words: 0,
nextStrength: "an extremely weak",
},
{
entropy: "AAAAAAAA",
words: 3,
nextStrength: "a very weak",
},
{
entropy: "AAAAAAAA B",
words: 3,
nextStrength: "a very weak",
},
{
entropy: "AAAAAAAA BBBBBBBB",
words: 6,
nextStrength: "a weak",
},
{
entropy: "AAAAAAAA BBBBBBBB CCCCCCCC",
words: 9,
nextStrength: "a strong",
},
{
entropy: "AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD",
words: 12,
nextStrength: "a very strong",
},
{
entropy: "AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD EEEEEEEE",
words: 15,
nextStrength: "an extremely strong",
}
];
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
});
var nextTest = function runNextTest(i) {
test = tests[i];
page.evaluate(function(e) {
$(".addresses").empty();
$(".entropy").val(e).trigger("input");
}, test.entropy);
waitForGenerate(function() {
// check the strength of the current mnemonic
var mnemonic = page.evaluate(function() {
return $(".phrase").val();
});
if (test.words == 0) {
if (mnemonic.length > 0) {
console.log("Mnemonic length for " + test.nextStrength + " strength is not " + test.words);
console.log("Mnemonic: " + mnemonic);
fail();
}
}
else {
if (mnemonic.split(" ").length != test.words) {
console.log("Mnemonic length for " + test.nextStrength + " strength is not " + test.words);
console.log("Mnemonic: " + mnemonic);
fail();
}
}
// check the strength of the next mnemonic is shown
var entropyText = page.evaluate(function() {
return $(".entropy-container").text();
});
if (entropyText.indexOf("required to generate " + test.nextStrength + " mnemonic") == -1) {
console.log("Strength indicator for " + test.nextStrength + " mnemonic is incorrect");
fail();
}
var isLastTest = i == tests.length - 1;
if (isLastTest) {
next();
}
else {
runNextTest(i+1);
}
});
}
nextTest(0);
});
},
// Entropy is truncated from the right
function() {
page.open(url, function(status) {
var expected = "abandon abandon ability";
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
var entropy = "00000000 00000000 00000000 00000000";
entropy += "11111111 11111111 11111111 1111"; // Missing last byte, only first 8 bytes are used
$(".entropy").val(entropy).trigger("input");
});
// check the entropy is truncated from the right
waitForGenerate(function() {
var actual = page.evaluate(function() {
return $(".phrase").val();
});
if (actual != expected) {
console.log("Entropy is not truncated from the right");
console.log("Expected: " + expected);
console.log("Got: " + actual);
fail();
}
next();
});
});
},
// Very large entropy results in very long mnemonics
function() {
page.open(url, function(status) {
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
var entropy = "";
// Generate a very long entropy string
for (var i=0; i<33; i++) {
entropy += "AAAAAAAA"; // 3 words * 33 iterations = 99 words
}
$(".entropy").val(entropy).trigger("input");
});
// check the mnemonic is very long
waitForGenerate(function() {
var wordCount = page.evaluate(function() {
return $(".phrase").val().split(" ").length;
});
if (wordCount != 99) {
console.log("Large entropy does not generate long mnemonic");
console.log("Expected 99 words, got " + wordCount);
fail();
}
next();
});
});
},
// Is compatible with bip32jp entropy
// https://bip32jp.github.io/english/index.html
// NOTES:
// Is incompatible with:
// base 6 with leading zeros
// base 6 wth 12 words / 53 chars
// base 20
function() {
page.open(url, function(status) {
var expected = "defy trip fatal jaguar mean rack rifle survey satisfy drift twist champion steel wife state furnace night consider glove olympic oblige donor novel left";
// use entropy
page.evaluate(function() {
$(".use-entropy").prop("checked", true).trigger("change");
var entropy = "123450123450123450123450123450123450123450123450123450123450123450123450123450123450123450123450123";
$(".entropy").val(entropy).trigger("input");
});
// check the mnemonic matches the expected value from bip32jp
waitForGenerate(function() {
var actual = page.evaluate(function() {
return $(".phrase").val();
});
if (actual != expected) {
console.log("Mnemonic does not match bip32jp for base 6 entropy");
console.log("Expected: " + expected);
console.log("Got: " + actual);
fail();
}
next();
});
});
},
// If you wish to add more tests, do so here...
// Here is a blank test template