]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/BIP39.git/blob - src/js/entropy.js
Entropy library assumes cards are discarded
[perso/Immae/Projets/Cryptomonnaies/BIP39.git] / src / js / entropy.js
1 /*
2 * Detects entropy from a string.
3 *
4 * Formats include:
5 * binary [0-1]
6 * base 6 [0-5]
7 * dice 6 [1-6]
8 * decimal [0-9]
9 * hexadecimal [0-9A-F]
10 *
11 * Automatically uses lowest entropy to avoid issues such as interpretting 0101
12 * as hexadecimal which would be 16 bits when really it's only 4 bits of binary
13 * entropy.
14 */
15
16 window.Entropy = new (function() {
17
18 // matchers returns an array of the matched events for each type of entropy.
19 // eg
20 // matchers.binary("010") returns ["0", "1", "0"]
21 // matchers.binary("a10") returns ["1", "0"]
22 // matchers.hex("a10") returns ["a", "1", "0"]
23 var matchers = {
24 binary: function(str) {
25 return str.match(/[0-1]/gi) || [];
26 },
27 base6: function(str) {
28 return str.match(/[0-5]/gi) || [];
29 },
30 dice: function(str) {
31 return str.match(/[1-6]/gi) || []; // ie dice numbers
32 },
33 base10: function(str) {
34 return str.match(/[0-9]/gi) || [];
35 },
36 hex: function(str) {
37 return str.match(/[0-9A-F]/gi) || [];
38 },
39 card: function(str) {
40 // Format is NumberSuit, eg
41 // AH ace of hearts
42 // 8C eight of clubs
43 // TD ten of diamonds
44 // JS jack of spades
45 // QH queen of hearts
46 // KC king of clubs
47 return str.match(/([A2-9TJQK][CDHS])/gi) || [];
48 }
49 }
50
51 // Convert array of cards from ["ac", "4d", "ks"]
52 // to numbers between 0 and 51 [0, 16, 51]
53 function convertCardsToInts(cards) {
54 var ints = [];
55 var values = "a23456789tjqk";
56 var suits = "cdhs";
57 for (var i=0; i<cards.length; i++) {
58 var card = cards[i].toLowerCase();
59 var value = card[0];
60 var suit = card[1];
61 var asInt = 13 * suits.indexOf(suit) + values.indexOf(value);
62 ints.push(asInt);
63 }
64 return ints;
65 }
66
67 this.fromString = function(rawEntropyStr) {
68 // Find type of entropy being used (binary, hex, dice etc)
69 var base = getBase(rawEntropyStr);
70 // Convert dice to base6 entropy (ie 1-6 to 0-5)
71 // This is done by changing all 6s to 0s
72 if (base.str == "dice") {
73 var newParts = [];
74 var newInts = [];
75 for (var i=0; i<base.parts.length; i++) {
76 var c = base.parts[i];
77 if ("12345".indexOf(c) > -1) {
78 newParts[i] = base.parts[i];
79 newInts[i] = base.ints[i];
80 }
81 else {
82 newParts[i] = "0";
83 newInts[i] = 0;
84 }
85 }
86 base.str = "base 6 (dice)";
87 base.ints = newInts;
88 base.parts = newParts;
89 base.matcher = matchers.base6;
90 }
91 // Detect empty entropy
92 if (base.parts.length == 0) {
93 return {
94 binaryStr: "",
95 cleanStr: "",
96 cleanHtml: "",
97 base: base,
98 };
99 }
100 // Convert base.ints to BigInteger.
101 // Due to using unusual bases, eg cards of base52, this is not as simple as
102 // using BigInteger.parse()
103 var entropyInt = BigInteger.ZERO;
104 for (var i=base.ints.length-1; i>=0; i--) {
105 var thisInt = BigInteger.parse(base.ints[i]);
106 var power = (base.ints.length - 1) - i;
107 var additionalEntropy = BigInteger.parse(base.asInt).pow(power).multiply(thisInt);
108 entropyInt = entropyInt.add(additionalEntropy);
109 }
110 // Convert entropy to binary
111 var entropyBin = entropyInt.toString(2);
112 // If the first integer is small, it must be padded with zeros.
113 // Otherwise the chance of the first bit being 1 is 100%, which is
114 // obviously incorrect.
115 // This is not perfect for non-2^n bases.
116 var expectedBits = Math.floor(base.parts.length * Math.log2(base.asInt));
117 while (entropyBin.length < expectedBits) {
118 entropyBin = "0" + entropyBin;
119 }
120 // Assume cards are NOT replaced.
121 // Additional entropy decreases as more cards are used. This means
122 // entropy is measured using n!, not base^n.
123 // eg the second last card can be only one of two, not one of fifty two
124 // so the added entropy for that card is only one bit at most
125 if (base.asInt == 52) {
126 // Get the maximum value without replacement
127 var totalDecks = Math.ceil(base.parts.length / 52);
128 var totalCards = totalDecks * 52;
129 var totalCombos = factorial(52).pow(totalDecks);
130 var totalRemainingCards = totalCards - base.parts.length;
131 var remainingDecks = Math.floor(totalRemainingCards / 52);
132 var remainingCards = totalRemainingCards % 52;
133 var remainingCombos = factorial(52).pow(remainingDecks).multiply(factorial(remainingCards));
134 var currentCombos = totalCombos.divide(remainingCombos);
135 var numberOfBits = Math.log2(currentCombos);
136 var maxWithoutReplace = BigInteger.pow(2, numberOfBits);
137 // aggresive flooring of numberOfBits by BigInteger.pow means a
138 // more accurate result can be had for small numbers using the
139 // built-in Math.pow function.
140 if (numberOfBits < 32) {
141 maxWithoutReplace = BigInteger(Math.round(Math.pow(2, numberOfBits)));
142 }
143 // Get the maximum value with replacement
144 var maxWithReplace = BigInteger.pow(52, base.parts.length);
145 // Calculate the new value by scaling the original value down
146 var withoutReplace = entropyInt.multiply(maxWithoutReplace).divide(maxWithReplace);
147 // Left pad with zeros based on number of bits
148 var entropyBin = withoutReplace.toString(2);
149 var numberOfBitsInt = Math.floor(numberOfBits);
150 while (entropyBin.length < numberOfBitsInt) {
151 entropyBin = "0" + entropyBin;
152 }
153 }
154 // Supply a 'filtered' entropy string for display purposes
155 var entropyClean = base.parts.join("");
156 var entropyHtml = base.parts.join("");
157 if (base.asInt == 52) {
158 entropyClean = base.parts.join(" ").toUpperCase();
159 entropyClean = entropyClean.replace(/C/g, "\u2663");
160 entropyClean = entropyClean.replace(/D/g, "\u2666");
161 entropyClean = entropyClean.replace(/H/g, "\u2665");
162 entropyClean = entropyClean.replace(/S/g, "\u2660");
163 entropyHtml = base.parts.join(" ").toUpperCase();
164 entropyHtml = entropyHtml.replace(/C/g, "<span class='card-suit club'>\u2663</span>");
165 entropyHtml = entropyHtml.replace(/D/g, "<span class='card-suit diamond'>\u2666</span>");
166 entropyHtml = entropyHtml.replace(/H/g, "<span class='card-suit heart'>\u2665</span>");
167 entropyHtml = entropyHtml.replace(/S/g, "<span class='card-suit spade'>\u2660</span>");
168 }
169 var e = {
170 binaryStr: entropyBin,
171 cleanStr: entropyClean,
172 cleanHtml: entropyHtml,
173 base: base,
174 }
175 return e;
176 }
177
178 function getBase(str) {
179 // Need to get the lowest base for the supplied entropy.
180 // This prevents interpreting, say, dice rolls as hexadecimal.
181 var binaryMatches = matchers.binary(str);
182 var hexMatches = matchers.hex(str);
183 // Find the lowest base that can be used, whilst ignoring any irrelevant chars
184 if (binaryMatches.length == hexMatches.length && hexMatches.length > 0) {
185 var ints = binaryMatches.map(function(i) { return parseInt(i, 2) });
186 return {
187 ints: ints,
188 parts: binaryMatches,
189 matcher: matchers.binary,
190 asInt: 2,
191 str: "binary",
192 }
193 }
194 var cardMatches = matchers.card(str);
195 if (cardMatches.length >= hexMatches.length / 2) {
196 var ints = convertCardsToInts(cardMatches);
197 return {
198 ints: ints,
199 parts: cardMatches,
200 matcher: matchers.card,
201 asInt: 52,
202 str: "card",
203 }
204 }
205 var diceMatches = matchers.dice(str);
206 if (diceMatches.length == hexMatches.length && hexMatches.length > 0) {
207 var ints = diceMatches.map(function(i) { return parseInt(i) });
208 return {
209 ints: ints,
210 parts: diceMatches,
211 matcher: matchers.dice,
212 asInt: 6,
213 str: "dice",
214 }
215 }
216 var base6Matches = matchers.base6(str);
217 if (base6Matches.length == hexMatches.length && hexMatches.length > 0) {
218 var ints = base6Matches.map(function(i) { return parseInt(i) });
219 return {
220 ints: ints,
221 parts: base6Matches,
222 matcher: matchers.base6,
223 asInt: 6,
224 str: "base 6",
225 }
226 }
227 var base10Matches = matchers.base10(str);
228 if (base10Matches.length == hexMatches.length && hexMatches.length > 0) {
229 var ints = base10Matches.map(function(i) { return parseInt(i) });
230 return {
231 ints: ints,
232 parts: base10Matches,
233 matcher: matchers.base10,
234 asInt: 10,
235 str: "base 10",
236 }
237 }
238 var ints = hexMatches.map(function(i) { return parseInt(i, 16) });
239 return {
240 ints: ints,
241 parts: hexMatches,
242 matcher: matchers.hex,
243 asInt: 16,
244 str: "hexadecimal",
245 }
246 }
247
248 // Polyfill for Math.log2
249 // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2#Polyfill
250 Math.log2 = Math.log2 || function(x) {
251 // The polyfill isn't good enough because of the poor accuracy of
252 // Math.LOG2E
253 // log2(8) gave 2.9999999999999996 which when floored causes issues.
254 // So instead use the BigInteger library to get it right.
255 return BigInteger.log(x) / BigInteger.log(2);
256 };
257
258 // Depends on BigInteger
259 function factorial(n) {
260 if (n == 0) {
261 return 1;
262 }
263 f = BigInteger.ONE;
264 for (var i=1; i<=n; i++) {
265 f = f.multiply(new BigInteger(i));
266 }
267 return f;
268 }
269
270 })();