aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan Coleman <coleman.ian@gmail.com>2016-11-25 14:24:58 +1100
committerIan Coleman <coleman.ian@gmail.com>2016-11-25 14:24:58 +1100
commit886f06ee6bbca2e6e05109dbd7e264bdea35238f (patch)
tree89d7d36e913c497516347b20e7818ad9dd804f25
parent9e97eb7684bf2c2d9c5276561f29fe50caf31f82 (diff)
downloadBIP39-886f06ee6bbca2e6e05109dbd7e264bdea35238f.tar.gz
BIP39-886f06ee6bbca2e6e05109dbd7e264bdea35238f.tar.zst
BIP39-886f06ee6bbca2e6e05109dbd7e264bdea35238f.zip
Card entropy calculation bugfix
See https://github.com/iancoleman/bip39/issues/33
-rw-r--r--src/js/entropy.js96
-rw-r--r--tests.js182
2 files changed, 260 insertions, 18 deletions
diff --git a/src/js/entropy.js b/src/js/entropy.js
index f41cf80..8a0c799 100644
--- a/src/js/entropy.js
+++ b/src/js/entropy.js
@@ -16,6 +16,8 @@
16 16
17window.Entropy = new (function() { 17window.Entropy = new (function() {
18 18
19 var TWO = new BigInteger(2);
20
19 // matchers returns an array of the matched events for each type of entropy. 21 // matchers returns an array of the matched events for each type of entropy.
20 // eg 22 // eg
21 // matchers.binary("010") returns ["0", "1", "0"] 23 // matchers.binary("010") returns ["0", "1", "0"]
@@ -124,7 +126,6 @@ window.Entropy = new (function() {
124 // eg the second last card can be only one of two, not one of fifty two 126 // eg the second last card can be only one of two, not one of fifty two
125 // so the added entropy for that card is only one bit at most 127 // so the added entropy for that card is only one bit at most
126 if (base.asInt == 52) { 128 if (base.asInt == 52) {
127 // Get the maximum value WITHOUT replacement
128 var totalDecks = Math.ceil(base.parts.length / 52); 129 var totalDecks = Math.ceil(base.parts.length / 52);
129 var totalCards = totalDecks * 52; 130 var totalCards = totalDecks * 52;
130 var totalCombos = factorial(52).pow(totalDecks); 131 var totalCombos = factorial(52).pow(totalDecks);
@@ -135,18 +136,77 @@ window.Entropy = new (function() {
135 var currentCombos = totalCombos.divide(remainingCombos); 136 var currentCombos = totalCombos.divide(remainingCombos);
136 var numberOfBits = Math.log2(currentCombos); 137 var numberOfBits = Math.log2(currentCombos);
137 var maxWithoutReplace = BigInteger.pow(2, numberOfBits); 138 var maxWithoutReplace = BigInteger.pow(2, numberOfBits);
138 // aggresive flooring of numberOfBits by BigInteger.pow means a 139 // Use a bunch of sorted decks to measure entropy from, populated
139 // more accurate result can be had for small numbers using the 140 // as needed.
140 // built-in Math.pow function. 141 var sortedDecks = [];
141 if (numberOfBits < 32) { 142 // Initialize the final entropy value for these cards
142 maxWithoutReplace = BigInteger(Math.round(Math.pow(2, numberOfBits))); 143 var entropyInt = BigInteger.ZERO;
144 // Track how many instances of each card have been used, and thus
145 // how many decks are in use.
146 var cardCounts = {};
147 // Track the total bits of entropy that remain, which diminishes as
148 // each card is drawn.
149 var totalBitsLeft = numberOfBits;
150 // Work out entropy contribution of each card drawn
151 for (var i=0; i<base.parts.length; i++) {
152 // Get the card that was drawn
153 var cardLower = base.parts[i];
154 var card = cardLower.toUpperCase();
155 // Initialize the deck for this card if needed, to track how
156 // much entropy it adds.
157 if (!(card in cardCounts)) {
158 cardCounts[card] = 0;
159 }
160 // Get the deck this card is from
161 var deckIndex = cardCounts[card];
162 while (deckIndex > sortedDecks.length-1) {
163 sortedDecks.push(getSortedDeck());
164 }
165 // See how many bits this card contributes (depends on how many
166 // are left in the deck it's from)
167 var deckForCard = sortedDecks[deckIndex];
168 var cardsLeftInDeck = deckForCard.length;
169 var additionalBits = Math.log2(cardsLeftInDeck);
170 // Work out the min and max value for this card
171 var nextTotalBitsLeft = totalBitsLeft - additionalBits;
172 var minPossibleNewEntropy = TWO.pow(nextTotalBitsLeft).subtract(1);
173 var maxPossibleNewEntropy = TWO.pow(totalBitsLeft).subtract(1);
174 var diff = maxPossibleNewEntropy.subtract(minPossibleNewEntropy);
175 // BigInteger aggresively floors numbers which greatly affects
176 // the small numbers. In that case, use native Math library
177 var useBigInt = totalBitsLeft >= 32;
178 if (!useBigInt) {
179 minPossibleNewEntropy = Math.round(Math.pow(2, nextTotalBitsLeft)-1);
180 maxPossibleNewEntropy = Math.round(Math.pow(2, totalBitsLeft)-1);
181 diff = maxPossibleNewEntropy - minPossibleNewEntropy;
182 }
183 // Scale the value between possible min and max depending on
184 // this card value
185 var thisCardIndex = deckForCard.indexOf(card);
186 var toAdd = BigInteger.ZERO;
187 if (cardsLeftInDeck > 1) {
188 if (useBigInt) {
189 toAdd = diff.multiply(thisCardIndex)
190 .divide(deckForCard.length - 1)
191 .add(minPossibleNewEntropy);
192 }
193 else {
194 var ratio = thisCardIndex / (deckForCard.length -1);
195 var f = diff * ratio;
196 toAdd = new BigInteger(f).add(minPossibleNewEntropy);
197 }
198 }
199 // Add this card entropy to existing entropy
200 entropyInt = entropyInt.add(toAdd);
201 // Remove this card from the deck it comes from
202 deckForCard.splice(thisCardIndex,1);
203 // Ensure the next insance of this card uses the next deck
204 cardCounts[card] = cardCounts[card] + 1;
205 // Next card drawn has less total remaining bits to work with
206 totalBitsLeft = nextTotalBitsLeft;
143 } 207 }
144 // Get the maximum value WITH replacement 208 // Convert to binary
145 var maxWithReplace = BigInteger.pow(52, base.parts.length); 209 var entropyBin = entropyInt.toString(2);
146 // Calculate the new value by scaling the original value down
147 var withoutReplace = entropyInt.multiply(maxWithoutReplace).divide(maxWithReplace);
148 // Left pad with zeros based on number of bits
149 var entropyBin = withoutReplace.toString(2);
150 var numberOfBitsInt = Math.floor(numberOfBits); 210 var numberOfBitsInt = Math.floor(numberOfBits);
151 while (entropyBin.length < numberOfBitsInt) { 211 while (entropyBin.length < numberOfBitsInt) {
152 entropyBin = "0" + entropyBin; 212 entropyBin = "0" + entropyBin;
@@ -177,6 +237,18 @@ window.Entropy = new (function() {
177 return e; 237 return e;
178 } 238 }
179 239
240 function getSortedDeck() {
241 var s = [];
242 var suits = "CDHS";
243 var values = "A23456789TJQK";
244 for (var i=0; i<suits.length; i++) {
245 for (var j=0; j<values.length; j++) {
246 s.push(values[j]+suits[i]);
247 }
248 }
249 return s;
250 }
251
180 function getBase(str) { 252 function getBase(str) {
181 // Need to get the lowest base for the supplied entropy. 253 // Need to get the lowest base for the supplied entropy.
182 // This prevents interpreting, say, dice rolls as hexadecimal. 254 // This prevents interpreting, say, dice rolls as hexadecimal.
diff --git a/tests.js b/tests.js
index 03ce9e1..0ce34bd 100644
--- a/tests.js
+++ b/tests.js
@@ -2185,9 +2185,9 @@ page.open(url, function(status) {
2185 try { 2185 try {
2186 var cards = [ 2186 var cards = [
2187 [ "ac", "00000" ], 2187 [ "ac", "00000" ],
2188 [ "acqs", "00000110001" ], 2188 [ "acqs", "00001100011" ],
2189 [ "acks", "00000110010" ], 2189 [ "acks", "00001100100" ],
2190 [ "2cac", "00000110011" ], 2190 [ "2cac", "00001100101" ],
2191 [ "2c", "00001" ], 2191 [ "2c", "00001" ],
2192 [ "3d", "01111" ], 2192 [ "3d", "01111" ],
2193 [ "4h", "11101" ], 2193 [ "4h", "11101" ],
@@ -2200,8 +2200,8 @@ page.open(url, function(status) {
2200 [ "jd", "10111" ], 2200 [ "jd", "10111" ],
2201 [ "qh", "100101" ], 2201 [ "qh", "100101" ],
2202 [ "ks", "110011" ], 2202 [ "ks", "110011" ],
2203 [ "ks2c", "101000101001" ], 2203 [ "ks2c", "101001011100" ],
2204 [ "KS2C", "101000101001" ], 2204 [ "KS2C", "101001011100" ],
2205 ]; 2205 ];
2206 for (var i=0; i<cards.length; i++) { 2206 for (var i=0; i<cards.length; i++) {
2207 var card = cards[i][0]; 2207 var card = cards[i][0];
@@ -2209,7 +2209,7 @@ page.open(url, function(status) {
2209 e = Entropy.fromString(card); 2209 e = Entropy.fromString(card);
2210 console.log(e.binary + " " + result); 2210 console.log(e.binary + " " + result);
2211 if (e.binaryStr !== result) { 2211 if (e.binaryStr !== result) {
2212 return "card entropy not parsed correctly: " + result + " != " + e.binaryStr; 2212 return "card entropy " + card + " not parsed correctly: " + result + " != " + e.binaryStr;
2213 } 2213 }
2214 } 2214 }
2215 } 2215 }
@@ -2724,6 +2724,136 @@ page.open(url, function(status) {
2724 words: 18, 2724 words: 18,
2725 strength: "extremely strong", 2725 strength: "extremely strong",
2726 }, 2726 },
2727 // Additional entropy from each card decreases as deck is depleted.
2728 // Check the boundaries of this depletion
2729 // See table at https://github.com/iancoleman/bip39/issues/33#issuecomment-262855862
2730 // for following values of 'events, bits, words'
2731 // 2 cards remaining = 21 words
2732 {
2733 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjs",
2734 type: "card (2 missing: QS KS)",
2735 events: 50,
2736 bits: 224,
2737 words: 21,
2738 strength: "extremely strong",
2739 },
2740 // 3 cards remaining = 18 words
2741 {
2742 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9sts",
2743 type: "card (3 missing: JS QS KS)",
2744 events: 49,
2745 bits: 222, // table uses different rounding - 222.99604
2746 words: 18,
2747 strength: "extremely strong",
2748 },
2749 // 13 cards remaining = 18 words
2750 {
2751 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkh",
2752 type: "card",
2753 events: 39,
2754 bits: 193,
2755 words: 18,
2756 strength: "extremely strong",
2757 },
2758 // 14 cards remaining = 15 words
2759 {
2760 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqh",
2761 type: "card",
2762 events: 38,
2763 bits: 189,
2764 words: 15,
2765 strength: "very strong",
2766 },
2767 // 21 cards remaining = 15 words
2768 {
2769 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h",
2770 type: "card",
2771 events: 31,
2772 bits: 160,
2773 words: 15,
2774 strength: "very strong",
2775 },
2776 // 22 cards remaining = 12 words
2777 {
2778 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h",
2779 type: "card",
2780 events: 30,
2781 bits: 155,
2782 words: 12,
2783 strength: "strong",
2784 },
2785 // 27 cards remaining = 12 words
2786 {
2787 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqd",
2788 type: "card",
2789 events: 25,
2790 bits: 132,
2791 words: 12,
2792 strength: "strong",
2793 },
2794 // 28 cards remaining = 9 words
2795 {
2796 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjd",
2797 type: "card",
2798 events: 24,
2799 bits: 127,
2800 words: 9,
2801 strength: "weak",
2802 },
2803 // 34 cards remaining = 9 words
2804 {
2805 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d",
2806 type: "card",
2807 events: 18,
2808 bits: 97,
2809 words: 9,
2810 strength: "weak",
2811 },
2812 // 35 cards remaining = 6 words
2813 {
2814 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d",
2815 type: "card",
2816 events: 17,
2817 bits: 92,
2818 words: 6,
2819 strength: "very weak",
2820 },
2821 // 40 cards remaining = 6 words
2822 {
2823 entropy: "ac2c3c4c5c6c7c8c9ctcjcqc",
2824 type: "card",
2825 events: 12,
2826 bits: 66,
2827 words: 6,
2828 strength: "very weak",
2829 },
2830 // 41 cards remaining = 3 words
2831 {
2832 entropy: "ac2c3c4c5c6c7c8c9ctcjc",
2833 type: "card",
2834 events: 11,
2835 bits: 61,
2836 words: 3,
2837 strength: "extremely weak",
2838 },
2839 // 46 cards remaining = 3 words
2840 {
2841 entropy: "ac2c3c4c5c6c",
2842 type: "card",
2843 events: 6,
2844 bits: 33,
2845 words: 3,
2846 strength: "extremely weak",
2847 },
2848 // 47 cards remaining = 0 words
2849 {
2850 entropy: "ac2c3c4c5c",
2851 type: "card",
2852 events: 5,
2853 bits: 28,
2854 words: 0,
2855 strength: "extremely weak",
2856 },
2727 ]; 2857 ];
2728 // use entropy 2858 // use entropy
2729 page.evaluate(function() { 2859 page.evaluate(function() {
@@ -2946,6 +3076,46 @@ page.open(url, function(status) {
2946}); 3076});
2947}, 3077},
2948 3078
3079// Github issue 33
3080// https://github.com/iancoleman/bip39/issues/33
3081// Final cards should contribute entropy
3082function() {
3083page.open(url, function(status) {
3084 // use entropy
3085 page.evaluate(function() {
3086 $(".use-entropy").prop("checked", true).trigger("change");
3087 $(".entropy").val("7S 9H 9S QH 8C KS AS 7D 7C QD 4S 4D TC 2D 5S JS 3D 8S 8H 4C 3C AC 3S QC 9C JC 7H AD TD JD 6D KH 5C QS 2S 6S 6H JH KD 9D-6C TS TH 4H KC 5H 2H AH 2C 8D 3H 5D").trigger("input");
3088 });
3089 // get the mnemonic
3090 waitForGenerate(function() {
3091 var originalPhrase = page.evaluate(function() {
3092 return $(".phrase").val();
3093 });
3094 // Set the last 12 cards to be AS
3095 page.evaluate(function() {
3096 $(".addresses").empty();
3097 $(".entropy").val("7S 9H 9S QH 8C KS AS 7D 7C QD 4S 4D TC 2D 5S JS 3D 8S 8H 4C 3C AC 3S QC 9C JC 7H AD TD JD 6D KH 5C QS 2S 6S 6H JH KD 9D-AS AS AS AS AS AS AS AS AS AS AS AS").trigger("input");
3098 });
3099 // get the new mnemonic
3100 waitForGenerate(function() {
3101 var newPhrase = page.evaluate(function() {
3102 return $(".phrase").val();
3103 });
3104 if (newPhrase == originalPhrase) {
3105 console.log("Changing last 12 cards does not change mnemonic");
3106 console.log("Original:");
3107 console.log(originalPhrase);
3108 console.log("New:");
3109 console.log(newPhrase);
3110 fail();
3111 }
3112 next();
3113 });
3114 });
3115});
3116},
3117
3118
2949// If you wish to add more tests, do so here... 3119// If you wish to add more tests, do so here...
2950 3120
2951// Here is a blank test template 3121// Here is a blank test template