From adc8ce127d4f8ea0d7e5ede6a82c2791d60ff4d2 Mon Sep 17 00:00:00 2001 From: Ian Coleman Date: Mon, 7 Nov 2016 16:01:21 +1100 Subject: [PATCH] Cards can be used for entropy Format is [A2-9TJQK][CDHS] --- src/js/entropy.js | 123 ++++++++++++++++-------- src/js/index.js | 21 +++-- tests.js | 231 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 278 insertions(+), 97 deletions(-) diff --git a/src/js/entropy.js b/src/js/entropy.js index 5b687d8..92300af 100644 --- a/src/js/entropy.js +++ b/src/js/entropy.js @@ -36,6 +36,32 @@ window.Entropy = new (function() { hex: function(str) { return str.match(/[0-9A-F]/gi) || []; }, + card: function(str) { + // Format is NumberSuit, eg + // AH ace of hearts + // 8C eight of clubs + // TD ten of diamonds + // JS jack of spades + // QH queen of hearts + // KC king of clubs + return str.match(/([A2-9TJQK][CDHS])/gi) || []; + } + } + + // Convert array of cards from ["ac", "4d", "ks"] + // to numbers between 0 and 51 [0, 16, 51] + function convertCardsToInts(cards) { + var ints = []; + var values = "a23456789tjqk"; + var suits = "cdhs"; + for (var i=0; i= 4 && firstDigit < 8) { - binLeadingZeros += "0"; - } - else if (firstDigit >= 2 && firstDigit < 4) { - binLeadingZeros += "00"; - } - else if (firstDigit >= 1 && firstDigit < 2) { - binLeadingZeros += "000"; - } + // If the first integer is small, it must be padded with zeros. + // Otherwise the chance of the first bit being 1 is 100%, which is + // obviously incorrect. + // This is not perfect for unusual bases, eg base 6 has 2.6 bits, so is + // slightly biased toward having leading zeros, but it's still better + // than ignoring it completely. + // TODO: revise this, it seems very fishy. For example, in base 10, there are + // 8 opportunities to start with 0 but only 2 to start with 1 + var firstInt = base.ints[0]; + var firstIntBits = Math.floor(Math.log2(firstInt))+1; + var maxFirstIntBits = Math.floor(Math.log2(base.asInt-1))+1; + var missingFirstIntBits = maxFirstIntBits - firstIntBits; + var firstIntLeadingZeros = ""; + for (var i=0; i=0; i--) { + var thisInt = BigInteger.parse(base.ints[i]); + var power = (base.ints.length - 1) - i; + var additionalEntropy = BigInteger.parse(base.asInt).pow(power).multiply(thisInt); + entropyInt = entropyInt.add(additionalEntropy); } - // Convert entropy to different foramts - var entropyInt = BigInteger.parse(base.parts.join(""), base.asInt); + // Convert entropy to different formats var entropyBin = binLeadingZeros + entropyInt.toString(2); - var entropyHex = hexLeadingZeros + entropyInt.toString(16); - var entropyClean = leadingZeros.join("") + base.parts.join(""); + var entropyClean = base.parts.join(""); var e = { binaryStr: entropyBin, - hexStr: entropyHex, cleanStr: entropyClean, base: base, } @@ -129,17 +155,32 @@ window.Entropy = new (function() { var binaryMatches = matchers.binary(str); var hexMatches = matchers.hex(str); // Find the lowest base that can be used, whilst ignoring any irrelevant chars - if (binaryMatches.length == hexMatches.length) { + if (binaryMatches.length == hexMatches.length && hexMatches.length > 0) { + var ints = binaryMatches.map(function(i) { return parseInt(i, 2) }); return { + ints: ints, parts: binaryMatches, matcher: matchers.binary, asInt: 2, str: "binary", } } + var cardMatches = matchers.card(str); + if (cardMatches.length >= hexMatches.length / 2) { + var ints = convertCardsToInts(cardMatches); + return { + ints: ints, + parts: cardMatches, + matcher: matchers.card, + asInt: 52, + str: "card", + } + } var diceMatches = matchers.dice(str); - if (diceMatches.length == hexMatches.length) { + if (diceMatches.length == hexMatches.length && hexMatches.length > 0) { + var ints = diceMatches.map(function(i) { return parseInt(i) }); return { + ints: ints, parts: diceMatches, matcher: matchers.dice, asInt: 6, @@ -147,8 +188,10 @@ window.Entropy = new (function() { } } var base6Matches = matchers.base6(str); - if (base6Matches.length == hexMatches.length) { + if (base6Matches.length == hexMatches.length && hexMatches.length > 0) { + var ints = base6Matches.map(function(i) { return parseInt(i) }); return { + ints: ints, parts: base6Matches, matcher: matchers.base6, asInt: 6, @@ -156,15 +199,19 @@ window.Entropy = new (function() { } } var base10Matches = matchers.base10(str); - if (base10Matches.length == hexMatches.length) { + if (base10Matches.length == hexMatches.length && hexMatches.length > 0) { + var ints = base10Matches.map(function(i) { return parseInt(i) }); return { + ints: ints, parts: base10Matches, matcher: matchers.base10, asInt: 10, str: "base 10", } } + var ints = hexMatches.map(function(i) { return parseInt(i, 16) }); return { + ints: ints, parts: hexMatches, matcher: matchers.hex, asInt: 16, @@ -175,7 +222,11 @@ window.Entropy = new (function() { // Polyfill for Math.log2 // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2#Polyfill Math.log2 = Math.log2 || function(x) { - return Math.log(x) * Math.LOG2E; + // The polyfill isn't good enough because of the poor accuracy of + // Math.LOG2E + // log2(8) gave 2.9999999999999996 which when floored causes issues. + // So instead use the BigInteger library to get it right. + return BigInteger.log(x) / BigInteger.log(2); }; })(); diff --git a/src/js/index.js b/src/js/index.js index 1cc77d3..a717a9e 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -738,33 +738,36 @@ // Show entropy details var extraBits = 32 - (entropy.binaryStr.length % 32); var extraChars = Math.ceil(extraBits * Math.log(2) / Math.log(entropy.base.asInt)); + var words = Math.floor(entropy.binaryStr.length / 32) * 3; var strength = "an extremely weak"; - if (entropy.hexStr.length >= 8) { + if (words >= 3) { strength = "a very weak"; } - if (entropy.hexStr.length >= 12) { + if (words >= 6) { strength = "a weak"; } - if (entropy.hexStr.length >= 24) { + if (words >= 9) { strength = "a strong"; } - if (entropy.hexStr.length >= 32) { + if (words >= 12) { strength = "a very strong"; } - if (entropy.hexStr.length >= 40) { + if (words >= 15) { strength = "an extremely strong"; } - if (entropy.hexStr.length >=48) { + if (words >= 18) { 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); + var bitsToUse = Math.floor(entropy.binaryStr.length / 32) * 32; + var binaryStr = entropy.binaryStr.substring(0, bitsToUse); // Convert entropy string to numeric array var entropyArr = []; - for (var i=0; i= 30) { - return "Keyboard mashing should produce weak entropy"; + try { + e = Entropy.fromString("aj;se ifj; ask,dfv js;ifj"); + if (e.binaryStr.length >= 30) { + return "Keyboard mashing should produce weak entropy"; + } } - return false; + catch (e) { + return e.message; + } + // Card entropy is used if every pair could be a card + try { + e = Entropy.fromString("4c3c2c"); + if (e.base.str != "card") { + return "Card entropy not used if all pairs are cards"; + } + } + catch (e) { + return e.message; + } + // Card entropy uses base 52 + // [ cards, binary ] + try { + var cards = [ + [ "ac", "00000" ], + [ "acac", "00000000000" ], + [ "acac2c", "00000000000000001" ], + [ "acks", "00000110011" ], + [ "acacks", "00000000000110011" ], + [ "2c", "000001" ], + [ "3d", "001111" ], + [ "4h", "011101" ], + [ "5s", "101011" ], + [ "6c", "000101" ], + [ "7d", "010011" ], + [ "8h", "100001" ], + [ "9s", "101111" ], + [ "tc", "001001" ], + [ "jd", "010111" ], + [ "qh", "100101" ], + [ "ks", "110011" ], + [ "ks2c", "101001011101" ], + [ "KS2C", "101001011101" ], + ]; + for (var i=0; i