aboutsummaryrefslogtreecommitdiff
path: root/src/js/entropy.js
blob: 590034609daaf867641ad662fe9c5fa3f237f7f0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/*
 * Detects entropy from a string.
 *
 * Formats include:
 * binary [0-1]
 * base 6 [0-5]
 * dice 6 [1-6]
 * decimal [0-9]
 * hexadecimal [0-9A-F]
 *
 * Automatically uses lowest entropy to avoid issues such as interpretting 0101
 * as hexadecimal which would be 16 bits when really it's only 4 bits of binary
 * entropy.
 */

window.Entropy = new (function() {

    // matchers returns an array of the matched events for each type of entropy.
    // eg
    // matchers.binary("010") returns ["0", "1", "0"]
    // matchers.binary("a10") returns ["1", "0"]
    // matchers.hex("a10") returns ["a", "1", "0"]
    var matchers = {
        binary: function(str) {
            return str.match(/[0-1]/gi) || [];
        },
        base6: function(str) {
            return str.match(/[0-5]/gi) || [];
        },
        dice: function(str) {
            return str.match(/[1-6]/gi) || []; // ie dice numbers
        },
        base10: function(str) {
            return str.match(/[0-9]/gi) || [];
        },
        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<cards.length; i++) {
            var card = cards[i].toLowerCase();
            var value = card[0];
            var suit = card[1];
            var asInt = 13 * suits.indexOf(suit) + values.indexOf(value);
            ints.push(asInt);
        }
        return ints;
    }

    this.fromString = function(rawEntropyStr) {
        // Find type of entropy being used (binary, hex, dice etc)
        var base = getBase(rawEntropyStr);
        // Convert dice to base6 entropy (ie 1-6 to 0-5)
        // This is done by changing all 6s to 0s
        if (base.str == "dice") {
            var newParts = [];
            var newInts = [];
            for (var i=0; i<base.parts.length; i++) {
                var c = base.parts[i];
                if ("12345".indexOf(c) > -1) {
                    newParts[i] = base.parts[i];
                    newInts[i] = base.ints[i];
                }
                else {
                    newParts[i] = "0";
                    newInts[i] = 0;
                }
            }
            base.str = "base 6 (dice)";
            base.ints = newInts;
            base.parts = newParts;
            base.matcher = matchers.base6;
        }
        // Detect empty entropy
        if (base.parts.length == 0) {
            return {
                binaryStr: "",
                cleanStr: "",
                cleanHtml: "",
                base: base,
            };
        }
        // Convert base.ints to BigInteger.
        // Due to using unusual bases, eg cards of base52, this is not as simple as
        // using BigInteger.parse()
        var entropyInt = BigInteger.ZERO;
        for (var i=base.ints.length-1; 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 binary
        var entropyBin = entropyInt.toString(2);
        // 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 non-2^n bases.
        var expectedBits = Math.floor(base.parts.length * Math.log2(base.asInt));
        while (entropyBin.length < expectedBits) {
            entropyBin = "0" + entropyBin;
        }
        // Assume cards are NOT replaced.
        // Additional entropy decreases as more cards are used. This means
        // entropy is measured using n!, not base^n.
        // eg the second last card can be only one of two, not one of fifty two
        // so the added entropy for that card is only one bit at most
        if (base.asInt == 52) {
            // Get the maximum value without replacement
            var totalDecks = Math.ceil(base.parts.length / 52);
            var totalCards = totalDecks * 52;
            var totalCombos = factorial(52).pow(totalDecks);
            var totalRemainingCards = totalCards - base.parts.length;
            var remainingDecks = Math.floor(totalRemainingCards / 52);
            var remainingCards = totalRemainingCards % 52;
            var remainingCombos = factorial(52).pow(remainingDecks).multiply(factorial(remainingCards));
            var currentCombos = totalCombos.divide(remainingCombos);
            var numberOfBits = Math.log2(currentCombos);
            var maxWithoutReplace = BigInteger.pow(2, numberOfBits);
            // aggresive flooring of numberOfBits by BigInteger.pow means a
            // more accurate result can be had for small numbers using the
            // built-in Math.pow function.
            if (numberOfBits < 32) {
                maxWithoutReplace = BigInteger(Math.round(Math.pow(2, numberOfBits)));
            }
            // Get the maximum value with replacement
            var maxWithReplace = BigInteger.pow(52, base.parts.length);
            // Calculate the new value by scaling the original value down
            var withoutReplace = entropyInt.multiply(maxWithoutReplace).divide(maxWithReplace);
            // Left pad with zeros based on number of bits
            var entropyBin = withoutReplace.toString(2);
            var numberOfBitsInt = Math.floor(numberOfBits);
            while (entropyBin.length < numberOfBitsInt) {
                entropyBin = "0" + entropyBin;
            }
        }
        // Supply a 'filtered' entropy string for display purposes
        var entropyClean = base.parts.join("");
        var entropyHtml = base.parts.join("");
        if (base.asInt == 52) {
            entropyClean = base.parts.join(" ").toUpperCase();
            entropyClean = entropyClean.replace(/C/g, "\u2663");
            entropyClean = entropyClean.replace(/D/g, "\u2666");
            entropyClean = entropyClean.replace(/H/g, "\u2665");
            entropyClean = entropyClean.replace(/S/g, "\u2660");
            entropyHtml = base.parts.join(" ").toUpperCase();
            entropyHtml = entropyHtml.replace(/C/g, "<span class='card-suit club'>\u2663</span>");
            entropyHtml = entropyHtml.replace(/D/g, "<span class='card-suit diamond'>\u2666</span>");
            entropyHtml = entropyHtml.replace(/H/g, "<span class='card-suit heart'>\u2665</span>");
            entropyHtml = entropyHtml.replace(/S/g, "<span class='card-suit spade'>\u2660</span>");
        }
        var e = {
            binaryStr: entropyBin,
            cleanStr: entropyClean,
            cleanHtml: entropyHtml,
            base: base,
        }
        return e;
    }

    function getBase(str) {
        // Need to get the lowest base for the supplied entropy.
        // This prevents interpreting, say, dice rolls as hexadecimal.
        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 && 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 && hexMatches.length > 0) {
            var ints = diceMatches.map(function(i) { return parseInt(i) });
            return {
                ints: ints,
                parts: diceMatches,
                matcher: matchers.dice,
                asInt: 6,
                str: "dice",
            }
        }
        var base6Matches = matchers.base6(str);
        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,
                str: "base 6",
            }
        }
        var base10Matches = matchers.base10(str);
        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,
            str: "hexadecimal",
        }
    }

    // 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) {
        // 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);
    };

    // Depends on BigInteger
    function factorial(n) {
        if (n == 0) {
            return 1;
        }
        f = BigInteger.ONE;
        for (var i=1; i<=n; i++) {
            f = f.multiply(new BigInteger(i));
        }
        return f;
    }

})();