diff options
author | Ian Coleman <coleman.ian@gmail.com> | 2016-11-30 15:30:19 +1100 |
---|---|---|
committer | Ian Coleman <coleman.ian@gmail.com> | 2016-11-30 15:30:19 +1100 |
commit | 87ad2c6e4c7987320d8ce147fe9310b702c5deea (patch) | |
tree | 6dc821981e0fcb86371b52d4a8b1b1ba528346b3 /src | |
parent | c3c3df473e0e5114b75dbe835ada59a0e8066543 (diff) | |
download | BIP39-87ad2c6e4c7987320d8ce147fe9310b702c5deea.tar.gz BIP39-87ad2c6e4c7987320d8ce147fe9310b702c5deea.tar.zst BIP39-87ad2c6e4c7987320d8ce147fe9310b702c5deea.zip |
Card entropy has improved conversion to binary
See http://crypto.stackexchange.com/q/41886
and https://github.com/iancoleman/bip39/issues/33#issuecomment-263021856
Diffstat (limited to 'src')
-rw-r--r-- | src/js/entropy.js | 169 |
1 files changed, 79 insertions, 90 deletions
diff --git a/src/js/entropy.js b/src/js/entropy.js index 8a0c799..d04d861 100644 --- a/src/js/entropy.js +++ b/src/js/entropy.js | |||
@@ -120,97 +120,9 @@ window.Entropy = new (function() { | |||
120 | while (entropyBin.length < expectedBits) { | 120 | while (entropyBin.length < expectedBits) { |
121 | entropyBin = "0" + entropyBin; | 121 | entropyBin = "0" + entropyBin; |
122 | } | 122 | } |
123 | // Assume cards are NOT replaced. | 123 | // Cards binary must be handled differently, since they're not replaced |
124 | // Additional entropy decreases as more cards are used. This means | ||
125 | // total possible entropy is measured using n!, not base^n. | ||
126 | // eg the second last card can be only one of two, not one of fifty two | ||
127 | // so the added entropy for that card is only one bit at most | ||
128 | if (base.asInt == 52) { | 124 | if (base.asInt == 52) { |
129 | var totalDecks = Math.ceil(base.parts.length / 52); | 125 | entropyBin = getCardBinary(base.parts); |
130 | var totalCards = totalDecks * 52; | ||
131 | var totalCombos = factorial(52).pow(totalDecks); | ||
132 | var totalRemainingCards = totalCards - base.parts.length; | ||
133 | var remainingDecks = Math.floor(totalRemainingCards / 52); | ||
134 | var remainingCards = totalRemainingCards % 52; | ||
135 | var remainingCombos = factorial(52).pow(remainingDecks).multiply(factorial(remainingCards)); | ||
136 | var currentCombos = totalCombos.divide(remainingCombos); | ||
137 | var numberOfBits = Math.log2(currentCombos); | ||
138 | var maxWithoutReplace = BigInteger.pow(2, numberOfBits); | ||
139 | // Use a bunch of sorted decks to measure entropy from, populated | ||
140 | // as needed. | ||
141 | var sortedDecks = []; | ||
142 | // Initialize the final entropy value for these cards | ||
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; | ||
207 | } | ||
208 | // Convert to binary | ||
209 | var entropyBin = entropyInt.toString(2); | ||
210 | var numberOfBitsInt = Math.floor(numberOfBits); | ||
211 | while (entropyBin.length < numberOfBitsInt) { | ||
212 | entropyBin = "0" + entropyBin; | ||
213 | } | ||
214 | } | 126 | } |
215 | // Supply a 'filtered' entropy string for display purposes | 127 | // Supply a 'filtered' entropy string for display purposes |
216 | var entropyClean = base.parts.join(""); | 128 | var entropyClean = base.parts.join(""); |
@@ -319,6 +231,83 @@ window.Entropy = new (function() { | |||
319 | } | 231 | } |
320 | } | 232 | } |
321 | 233 | ||
234 | // Assume cards are NOT replaced. | ||
235 | // Additional entropy decreases as more cards are used. This means | ||
236 | // total possible entropy is measured using n!, not base^n. | ||
237 | // eg the second last card can be only one of two, not one of fifty two | ||
238 | // so the added entropy for that card is only one bit at most | ||
239 | function getCardBinary(cards) { | ||
240 | // Track how many instances of each card have been used, and thus | ||
241 | // how many decks are in use. | ||
242 | var cardCounts = {}; | ||
243 | var numberOfDecks = 0; | ||
244 | // Work out number of decks by max(duplicates) | ||
245 | for (var i=0; i<cards.length; i++) { | ||
246 | // Get the card that was drawn | ||
247 | var cardLower = cards[i]; | ||
248 | var card = cardLower.toUpperCase(); | ||
249 | // Initialize the count for this card if needed | ||
250 | if (!(card in cardCounts)) { | ||
251 | cardCounts[card] = 0; | ||
252 | } | ||
253 | cardCounts[card] += 1; | ||
254 | // See if this is max(duplicates) | ||
255 | if (cardCounts[card] > numberOfDecks) { | ||
256 | numberOfDecks = cardCounts[card]; | ||
257 | } | ||
258 | } | ||
259 | // Work out the total number of bits for this many decks | ||
260 | // See http://crypto.stackexchange.com/q/41886 | ||
261 | var gainedBits = Math.log2(factorial(52 * numberOfDecks)); | ||
262 | var lostBits = 52 * Math.log2(factorial(numberOfDecks)); | ||
263 | var maxBits = gainedBits - lostBits; | ||
264 | // Convert the drawn cards to a binary representation. | ||
265 | // The exact technique for doing this is unclear. | ||
266 | // See | ||
267 | // http://crypto.stackexchange.com/a/41896 | ||
268 | // "I even doubt that this is well defined (only the average entropy | ||
269 | // is, I believe)." | ||
270 | // See | ||
271 | // https://github.com/iancoleman/bip39/issues/33#issuecomment-263021856 | ||
272 | // "The binary representation can be the first log(permutations,2) bits | ||
273 | // of the sha-2 hash of the normalized deck string." | ||
274 | // | ||
275 | // In this specific implementation, the first N bits of the hash of the | ||
276 | // normalized cards string is being used. Uppercase, no spaces; eg | ||
277 | // sha256("AH8DQSTC2H") | ||
278 | var totalCards = numberOfDecks * 52; | ||
279 | var percentUsed = cards.length / totalCards; | ||
280 | // Calculate the average number of bits of entropy for the number of | ||
281 | // cards drawn. | ||
282 | var numberOfBits = Math.floor(maxBits * percentUsed); | ||
283 | // Create a normalized string of the selected cards | ||
284 | var normalizedCards = cards.join("").toUpperCase(); | ||
285 | // Convert to binary using the SHA256 hash of the normalized cards. | ||
286 | // If the number of bits is more than 256, multiple rounds of hashing | ||
287 | // are used until the required number of bits is reached. | ||
288 | var entropyBin = ""; | ||
289 | var iterations = 0; | ||
290 | while (entropyBin.length < numberOfBits) { | ||
291 | var hashedCards = sjcl.hash.sha256.hash(normalizedCards); | ||
292 | for (var j=0; j<iterations; j++) { | ||
293 | hashedCards = sjcl.hash.sha256.hash(hashedCards); | ||
294 | } | ||
295 | var hashHex = sjcl.codec.hex.fromBits(hashedCards); | ||
296 | for (var i=0; i<hashHex.length; i++) { | ||
297 | var decimal = parseInt(hashHex[i], 16); | ||
298 | var binary = decimal.toString(2); | ||
299 | while (binary.length < 4) { | ||
300 | binary = "0" + binary; | ||
301 | } | ||
302 | entropyBin = entropyBin + binary; | ||
303 | } | ||
304 | iterations = iterations + 1; | ||
305 | } | ||
306 | // Truncate to the appropriate number of bits. | ||
307 | entropyBin = entropyBin.substring(0, numberOfBits); | ||
308 | return entropyBin; | ||
309 | } | ||
310 | |||
322 | // Polyfill for Math.log2 | 311 | // Polyfill for Math.log2 |
323 | // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2#Polyfill | 312 | // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2#Polyfill |
324 | Math.log2 = Math.log2 || function(x) { | 313 | Math.log2 = Math.log2 || function(x) { |