From 7e745a6911015a501cd2953b1340796c237a4dee Mon Sep 17 00:00:00 2001 From: Tyler Bettilyon Date: Mon, 21 Nov 2016 17:08:09 -0800 Subject: [PATCH 1/9] started the exercise description and added 2 hash colliders to the solution --- exercises/03-checksum.md | 2 +- exercises/04-collision-attack.md | 5 +++ src/collision-attack.js | 16 ++++++++ src/solutions/collision-attack.js | 62 +++++++++++++++++++++++++++++++ src/tests/collision-attack.js | 42 +++++++++++++++++++++ 5 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 exercises/04-collision-attack.md create mode 100644 src/collision-attack.js create mode 100644 src/solutions/collision-attack.js create mode 100644 src/tests/collision-attack.js diff --git a/exercises/03-checksum.md b/exercises/03-checksum.md index 987ad5d..6a3716d 100644 --- a/exercises/03-checksum.md +++ b/exercises/03-checksum.md @@ -1,4 +1,4 @@ -# Finally: Writing Checksums +# Writing Checksums Now that our code can both generate and corrupt random test data, it's time to make some checksum algorithms! diff --git a/exercises/04-collision-attack.md b/exercises/04-collision-attack.md new file mode 100644 index 0000000..776ed65 --- /dev/null +++ b/exercises/04-collision-attack.md @@ -0,0 +1,5 @@ +# Collision Attack + +A __collision attack__ is an way to fool a checksum. Previously, we were fooling our checksum by sending random data, or by randomly altering the data. This random testing framework is powerful and often helps programmers find vulnerabilities we did not know about, but hackers want to *exploit* a vulnerability. Random corruption might cause our data to behave strangely, but it's highly unlikely that random corruption would cause the data to suddenly become a virus or other malicious code. + +In this section, we're going to create a series of collision attacks, that ultimately will lead us to a better understanding of how to "break" a digital signature. Breaking such a signature could result in being able to spoof encrypted cookies, authentication tokens, and more. This is the same type of attack that allowed the Flame virus creators to fake SSL certificates. __MD5__ and __SHA-1__ are both now considered "broken" due to the efficacy of such __collision attacks__. At the time of this writing it appears that __SHA-256__ remains unbroken. diff --git a/src/collision-attack.js b/src/collision-attack.js new file mode 100644 index 0000000..b49b3f0 --- /dev/null +++ b/src/collision-attack.js @@ -0,0 +1,16 @@ +'use strict' +const checksums = require('./solutions/checksum'); + +module.exports = { + + /** + Given a checksum value produced by the charcodeSum function + return a value that collides with the provided checksum when + the charcodeSum function is used to hash the return value. + + This starting solution is tempting... but doesn't work. Why not? + */ + collideWithSimpleSum: function(checksum) { + return String.fromCharCode(checksum); + } +} diff --git a/src/solutions/collision-attack.js b/src/solutions/collision-attack.js new file mode 100644 index 0000000..48a7e99 --- /dev/null +++ b/src/solutions/collision-attack.js @@ -0,0 +1,62 @@ +'use strict' +const checksums = require('./checksum'); + +module.exports = { + + /** + Given a checksum value produced by the charcodeSum function + return a value that collides with the provided checksum when + the charcodeSum function is used to hash the return value. + + It's tempting to simply try: return String.fromCharCode(checksum); + give it a shot and find out why it doesn't work... + + @param {integer} checksum : the hash value that our returned string must + hash to using the simple sum method + */ + collideWithSimpleSum: function(checksum) { + let currentSum = 0; + let collision = ''; + + while(currentSum !== checksum) { + let addAmount = Math.min(32768, checksum - currentSum); + collision += String.fromCharCode(addAmount); + currentSum += addAmount; + } + + return collision; + }, + + /** + Given a checksum value produced by the charcodeSum function + return a value that collides with the provided checksum when + the charcodeSum function is used to hash the return value. + + This time, however, we are restricted to using a specific set + of characers. It's common that our output space would be limited, + for example, consider password strings which often have a restricted + character set. + + @param {integer} checksum : the hash value that our returned string must + hash to using the simple sum method + */ + collideWithSimpleSumRestricted: function(checksum, characterSet) { + + return generateCollision(0, ''); + + // I'm using a recursive solution to exhaustively test many solutions + function generateCollision(currentSum, currentCollision) { + if(currentSum > checksum) return false; + if(currentSum === checksum) return currentCollision; + + for(let character of characterSet) { + let newSum = currentSum + Number(character.charCodeAt(0)); + let collision = generateCollision(newSum, currentCollision + character); + + if(collision !== false) return collision; + } + + return false; + } + } +} diff --git a/src/tests/collision-attack.js b/src/tests/collision-attack.js new file mode 100644 index 0000000..ac71a88 --- /dev/null +++ b/src/tests/collision-attack.js @@ -0,0 +1,42 @@ +'use strict' +const collide = require('../solutions/collision-attack'); +const checksums = require('../solutions/checksum'); +const generate = require('../solutions/string-generator'); +const assert = require('chai').assert + +const sampleLengths = [256, 512]; +const testStrings = generate.generateRandomSamples(500, sampleLengths); + +describe("collision-attack", function() { + + describe("collideWithSimpleSum", function() { + it("Should always produce a value that matches the input checksum", function(){ + for(let testCase of testStrings) { + let checksum = checksums.charcodeSum(testCase); + let collisionData = collide.collideWithSimpleSum(checksum); + let collision = checksums.charcodeSum(collisionData); + + assert.equal(collision, checksum, `checksum didn't match for test case ${testCase}, returned ${collisionData}`) + } + }); + }); + + describe("collideWithSimpleSumRestricted", function() { + it("Should always produce a value that matches the input checksum using only characters from a restricted set", function(){ + // To speed up the tests + let charSet = new Set(generate.alphaChars.split('')); + + for(let testCase of testStrings) { + let checksum = checksums.charcodeSum(testCase); + let collisionData = collide.collideWithSimpleSumRestricted(checksum, generate.alphaChars); + let collision = checksums.charcodeSum(collisionData); + assert.equal(collision, checksum, `checksum didn't match for test case ${testCase}, returned ${collisionData}`) + + for(let character of collisionData) { + assert.isOk(charSet.has(character), `Restricted character in collision: ${character}`); + } + + } + }); + }); +}); From 60d0ebdddeed335ee8d7ecc69bb318434a80b378 Mon Sep 17 00:00:00 2001 From: Tyler Bettilyon Date: Tue, 22 Nov 2016 13:53:51 -0700 Subject: [PATCH 2/9] strong progress, pretty sure I have enough to consistenly break the indexTimes checksum. I still need to generate a string based on the sum partials --- exercises/04-collision-attack.md | 22 +++- src/solutions/checksum.js | 4 +- src/solutions/collision-attack.js | 182 +++++++++++++++++++++++++++++- src/tests/collision-attack.js | 32 +++++- 4 files changed, 228 insertions(+), 12 deletions(-) diff --git a/exercises/04-collision-attack.md b/exercises/04-collision-attack.md index 776ed65..f14e46a 100644 --- a/exercises/04-collision-attack.md +++ b/exercises/04-collision-attack.md @@ -1,5 +1,23 @@ -# Collision Attack +# Collision Attacks A __collision attack__ is an way to fool a checksum. Previously, we were fooling our checksum by sending random data, or by randomly altering the data. This random testing framework is powerful and often helps programmers find vulnerabilities we did not know about, but hackers want to *exploit* a vulnerability. Random corruption might cause our data to behave strangely, but it's highly unlikely that random corruption would cause the data to suddenly become a virus or other malicious code. -In this section, we're going to create a series of collision attacks, that ultimately will lead us to a better understanding of how to "break" a digital signature. Breaking such a signature could result in being able to spoof encrypted cookies, authentication tokens, and more. This is the same type of attack that allowed the Flame virus creators to fake SSL certificates. __MD5__ and __SHA-1__ are both now considered "broken" due to the efficacy of such __collision attacks__. At the time of this writing it appears that __SHA-256__ remains unbroken. +In this section, we're going to create a series of collision attacks, that ultimately will lead us to a better understanding of how to "break" a digital signature. Breaking such a signature could result in being able to spoof encrypted cookies, authentication tokens, and more. This is the same type of attack that allowed the Flame virus creators to fake SSL certificates. __MD5__ and __SHA-1__ are both now considered "broken" due to the efficacy of such __collision attacks__. At the time of this writing it appears that __SHA-256__ remains unbroken. + +## Your Tasks + +You're going to create several hash collision attacks with increasing difficulty. + +First we're going to create an attack with very no restrictions on output, against an exceptionally weak hash algorithm. Then we're going to restrict the output to alpha characters. Then we're going to make the hash function a little better and continue to use the restricted character set. These first 3 exercises are to get you thinking about collisions in general. + +Once we've successfully crafted a few collision attacks using arbitrary data, we're going to put a further restriction on the data: you must send something __useful__. For example, checksums are frequently used as a digital signature on secure content. We're going to simulate "breaking" the signature of something sent to JSON.stringify. Specifically, you're going to attempt to change the userId in cookies like this one: + +```js +let data = { + userId: 1234 +}; + +let payload = JSON.stringify(data); +``` + +We're trying to create a collision attack against this "payload", but where the userId has been changed to 42. In this case, we're pretending to be user number 42 without the receiver realizing that they actually authenticated user 1234. Lucky for us, the receiver only looks in the `userId` property of the data. diff --git a/src/solutions/checksum.js b/src/solutions/checksum.js index 8f9f49c..2504f84 100644 --- a/src/solutions/checksum.js +++ b/src/solutions/checksum.js @@ -16,7 +16,7 @@ module.exports = { */ charcodeSum: function(inputString){ var sum = 0; - for(var i = 0; i < inputString.length; i++) { + for(let i = 0; i < inputString.length; i++) { sum += inputString.charCodeAt(i); } return sum; @@ -28,7 +28,7 @@ module.exports = { */ charcodeTimesIndex: function(inputString){ var sum = 0; - for(var i = 0; i < inputString.length; i++) { + for(let i = 0; i < inputString.length; i++) { sum += inputString.charCodeAt(i) * (i+1); } diff --git a/src/solutions/collision-attack.js b/src/solutions/collision-attack.js index 48a7e99..892ba9e 100644 --- a/src/solutions/collision-attack.js +++ b/src/solutions/collision-attack.js @@ -41,22 +41,196 @@ module.exports = { hash to using the simple sum method */ collideWithSimpleSumRestricted: function(checksum, characterSet) { + // Performance optimization, greedily pick bigger charCodes first + let charSetOrdered = characterSet.split('').sort(function(a, b) { + return b.charCodeAt(0) - a.charCodeAt(0); + }); + // See recursive inner function for how this works return generateCollision(0, ''); - // I'm using a recursive solution to exhaustively test many solutions + /** + I'm using a recursive solution to exhaustively test many solutions. + A more clever solution could be faster, but this is good enough for + our test cases. + */ function generateCollision(currentSum, currentCollision) { if(currentSum > checksum) return false; if(currentSum === checksum) return currentCollision; - for(let character of characterSet) { - let newSum = currentSum + Number(character.charCodeAt(0)); + for(let character of charSetOrdered) { + let newSum = currentSum + character.charCodeAt(0); let collision = generateCollision(newSum, currentCollision + character); - if(collision !== false) return collision; + if(collision) return collision; } return false; } + }, + + /** + Given a checksum value produced by the charcodeTimesIndex function + return a value that collides with the provided checksum when + the charcodeSum function is used to hash the return value. + + Once again, we're restricted to a specific character set. + + @param {integer} checksum : the hash value that our returned string must + hash to using the simple sum method + */ + collideWithIndex: function(checksum, characterSet) { + + const numbers = createIndexTimesNumberMap(checksum, characterSet); + let collisionObj = generateAllSums(0); + return 'aa'; + // return collisionObj.reduce(function(accum, charOb) { + // return accum + charOb.character; + // }, ''); + + + /** + Now we're using brute force again, but finding the numbers that + sum to our checksum instead of searching for strings that break + the checksum. This problem is actually well studied, and called + "The subset sum problem". It's also "NP-Complete", which means + many things, but also means we won't be able to generate all + such sums. So we have to limit the search space. + + Luckily, we have some very practical limits in the position + and character set. Each option in the numberMap can only + be used once, for example, since they each represent a + character at an individual position in the string. + */ + function generateAllSums(currentSum, partial = [], usedInfo = undefined) { + // Default for the usedInfo is complex + if(usedInfo === undefined) { + usedInfo = { + numbers: {}, + positions: {} + }; + } + // console.log(usedInfo.positions); + + if(currentSum > checksum) return []; + if(currentSum === checksum){ + console.log(partial.join(',')); + tryPartial(numbers, partial); + return partial; + } + + let sumsFoundBelow = []; + for(let number in numbers) { + let newUsedInfo = cloneUsedInfo(usedInfo); + let usedNumbers = newUsedInfo.numbers; + let usedPositions = newUsedInfo.positions; + + // Ignore numbers we have already exhausted + let numberUsedCount = newUsedInfo.numbers[number]; + if(numberUsedCount >= numbers[number].length) { + continue; + } + + // Increment this number's used count + if(usedNumbers[number] === undefined) usedNumbers[number] = 1; + else usedNumbers[number] += 1; + + // Ignore numbers that can only be represented in positions we've used + // And mark this position as used + let openPosition = false; + for(let {position} of numbers[number]) { + + if(!usedInfo.positions[position]) { + openPosition = true; + usedPositions[position] = true; + break; + } + } + + if(!openPosition) { + continue; + } + + let newSum = currentSum + Number(number); + let newCollision = partial.slice(); + newCollision.push(number); + + sumsFoundBelow.concat(generateAllSums(newSum, newCollision, newUsedInfo)); + } + + return sumsFoundBelow; + } + } +} + + +// Quick way to make a deep clone of the usedInfo obj +function cloneUsedInfo(usedInfo) { + let newObj = { + numbers: {}, + positions: {} + }; + + for(let number in usedInfo.numbers) { + newObj.numbers[number] = usedInfo.numbers[number]; } + + for(let position in usedInfo.positions) { + newObj.positions[position] = usedInfo.positions[position]; + } + + return newObj; +} + +/** + Given a number map, and the subset sum solution, see if we can use this subset + as our collision given the constraints of the numberMap. We use another exhaustive + recursive search to generate all the strings represented. +*/ +function tryPartial(numberMap, partial) { + let collisions = []; + for(let number of partial) { + let options = numberMap[number]; + for(let option in options) { + console.log(option); + } + } +} + +/** + This subroutine generates all the single character values + based on their position in the output string. We use this to + shrink the space of the brute force attack. +*/ +function createIndexTimesNumberMap(checksum, characteSet) { + // Compute the maximum length of the string given the characterSet + // and using what we know about the checksum algorithm. + let min = characteSet[characteSet.length - 1].charCodeAt(0); + let maxLength = 1; + let tmpSum = 0; + while(tmpSum < checksum) { + tmpSum += (maxLength * min); + maxLength++; + } + maxLength -= 1; + + let numbers = {} + for(let char of characteSet) { + for(let i = 0; i < maxLength; i++) { + let charWithVal = { + character: char, + position: i + } + let value = char.charCodeAt(0) * (i+1); + + if(numbers[value] === undefined) { + numbers[value] = [charWithVal]; + } + else { + numbers[value].push(charWithVal); + } + } + } + + return numbers; } diff --git a/src/tests/collision-attack.js b/src/tests/collision-attack.js index ac71a88..3ebba02 100644 --- a/src/tests/collision-attack.js +++ b/src/tests/collision-attack.js @@ -2,14 +2,14 @@ const collide = require('../solutions/collision-attack'); const checksums = require('../solutions/checksum'); const generate = require('../solutions/string-generator'); -const assert = require('chai').assert - -const sampleLengths = [256, 512]; -const testStrings = generate.generateRandomSamples(500, sampleLengths); +const assert = require('chai').assert; describe("collision-attack", function() { describe("collideWithSimpleSum", function() { + const sampleLengths = [256, 512]; + const testStrings = generate.generateRandomSamples(500, sampleLengths); + it("Should always produce a value that matches the input checksum", function(){ for(let testCase of testStrings) { let checksum = checksums.charcodeSum(testCase); @@ -22,6 +22,9 @@ describe("collision-attack", function() { }); describe("collideWithSimpleSumRestricted", function() { + const sampleLengths = [256, 512]; + const testStrings = generate.generateRandomSamples(500, sampleLengths); + it("Should always produce a value that matches the input checksum using only characters from a restricted set", function(){ // To speed up the tests let charSet = new Set(generate.alphaChars.split('')); @@ -35,7 +38,28 @@ describe("collision-attack", function() { for(let character of collisionData) { assert.isOk(charSet.has(character), `Restricted character in collision: ${character}`); } + } + }); + }); + + describe("collideWidthIndex", function() { + it("Should always produce a value that matches the input checksum using only characters from a restricted set", function(){ + const sampleLengths = [4, 8, 16]; + const testStrings = generate.generateRandomSamples(20, sampleLengths); + // To speed up the tests + let charSet = new Set(generate.alphaChars.split('')); + this.timeout(30000); // 30 second limit... + + for(let testCase of testStrings) { + let checksum = checksums.charcodeTimesIndex(testCase); + let collisionData = collide.collideWithIndex(checksum, generate.alphaChars); + let collision = checksums.charcodeTimesIndex(collisionData); + assert.equal(collision, checksum, `checksum didn't match for test case ${testCase}, returned ${collisionData}`) + + for(let character of collisionData) { + assert.isOk(charSet.has(character), `Restricted character in collision: ${character}`); + } } }); }); From 1a227650cb9f93d477f4edfe8a4634cd36a30d99 Mon Sep 17 00:00:00 2001 From: Tyler Bettilyon Date: Tue, 22 Nov 2016 16:13:02 -0700 Subject: [PATCH 3/9] wooowzers this solution is slow, but I can definitly create arbitrary collisions against the charcodeTimesIndex function --- src/solutions/collision-attack.js | 64 ++++++++++++++++++++++++------- src/tests/collision-attack.js | 6 +-- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/solutions/collision-attack.js b/src/solutions/collision-attack.js index 892ba9e..ca82926 100644 --- a/src/solutions/collision-attack.js +++ b/src/solutions/collision-attack.js @@ -83,11 +83,7 @@ module.exports = { const numbers = createIndexTimesNumberMap(checksum, characterSet); let collisionObj = generateAllSums(0); - return 'aa'; - // return collisionObj.reduce(function(accum, charOb) { - // return accum + charOb.character; - // }, ''); - + return collisionObj; /** Now we're using brute force again, but finding the numbers that @@ -110,13 +106,20 @@ module.exports = { positions: {} }; } - // console.log(usedInfo.positions); if(currentSum > checksum) return []; + if(currentSum === checksum){ - console.log(partial.join(',')); - tryPartial(numbers, partial); - return partial; + let collisions = computePotentialCollisions(numbers, partial); + if(collisions === false) { + return []; + } + + for(let c of collisions) { + if(checksums.charcodeTimesIndex(c) === checksum) { + return [c] + } + } } let sumsFoundBelow = []; @@ -155,7 +158,10 @@ module.exports = { let newCollision = partial.slice(); newCollision.push(number); - sumsFoundBelow.concat(generateAllSums(newSum, newCollision, newUsedInfo)); + sumsFoundBelow = sumsFoundBelow.concat(generateAllSums(newSum, newCollision, newUsedInfo)); + if(sumsFoundBelow.length > 0) { + return sumsFoundBelow; + } } return sumsFoundBelow; @@ -187,14 +193,44 @@ function cloneUsedInfo(usedInfo) { as our collision given the constraints of the numberMap. We use another exhaustive recursive search to generate all the strings represented. */ -function tryPartial(numberMap, partial) { - let collisions = []; +function computePotentialCollisions(numberMap, partial, partialCollision = []) { + let positionCharacterMap = {} for(let number of partial) { let options = numberMap[number]; - for(let option in options) { - console.log(option); + for(let option of options) { + if(positionCharacterMap[option.position] === undefined) { + positionCharacterMap[option.position] = []; + } + positionCharacterMap[option.position].push(option.character); } } + + // Check that it's possible + for(let i = 0; i < partial.length; i++) { + if(positionCharacterMap[i] === undefined) { + return false; + } + } + + return stringsFromPositionMap(positionCharacterMap); +} + +function stringsFromPositionMap(positionCharacterMap, position = 0, prefix = '') { + let stringsBelow = []; + if(prefix.length >= Object.keys(positionCharacterMap).length){ + stringsBelow.push(prefix); + return stringsBelow; + } + + for(let i = position; i < Object.keys(positionCharacterMap).length; i++) { + let characters = positionCharacterMap[i]; + + for(let char of characters) { + stringsBelow = stringsBelow.concat(stringsFromPositionMap(positionCharacterMap, i+1, prefix + char)); + } + } + + return stringsBelow; } /** diff --git a/src/tests/collision-attack.js b/src/tests/collision-attack.js index 3ebba02..d930d72 100644 --- a/src/tests/collision-attack.js +++ b/src/tests/collision-attack.js @@ -44,8 +44,8 @@ describe("collision-attack", function() { describe("collideWidthIndex", function() { it("Should always produce a value that matches the input checksum using only characters from a restricted set", function(){ - const sampleLengths = [4, 8, 16]; - const testStrings = generate.generateRandomSamples(20, sampleLengths); + const sampleLengths = [4]; + const testStrings = generate.generateRandomSamples(10, sampleLengths); // To speed up the tests let charSet = new Set(generate.alphaChars.split('')); @@ -53,7 +53,7 @@ describe("collision-attack", function() { for(let testCase of testStrings) { let checksum = checksums.charcodeTimesIndex(testCase); - let collisionData = collide.collideWithIndex(checksum, generate.alphaChars); + let collisionData = collide.collideWithIndex(checksum, generate.alphaChars)[0]; let collision = checksums.charcodeTimesIndex(collisionData); assert.equal(collision, checksum, `checksum didn't match for test case ${testCase}, returned ${collisionData}`) From 26e806c327e7d83e4c36792dd32509a3f21adb84 Mon Sep 17 00:00:00 2001 From: Tyler Bettilyon Date: Wed, 23 Nov 2016 13:39:35 -0700 Subject: [PATCH 4/9] speedup by pre-processing valid number set instead of continuing inside the numbers loop --- src/solutions/collision-attack.js | 37 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/solutions/collision-attack.js b/src/solutions/collision-attack.js index ca82926..645bcae 100644 --- a/src/solutions/collision-attack.js +++ b/src/solutions/collision-attack.js @@ -122,38 +122,41 @@ module.exports = { } } + // Performance: Compute the numbers we need to work on BEFORE we clone the info + let validNumbers = Object.keys(numbers).filter(function(number) { + + let openPosition = false; + for(let {position} of numbers[number]) { + // console.log(position, usedInfo.positions[position]); + if(!usedInfo.positions[position]) { + openPosition = true; + break; + } + } + + return openPosition && (!usedInfo.numbers[number] || usedInfo.numbers[number] < numbers[number].length); + }); + let sumsFoundBelow = []; - for(let number in numbers) { + for(let number of validNumbers) { let newUsedInfo = cloneUsedInfo(usedInfo); let usedNumbers = newUsedInfo.numbers; let usedPositions = newUsedInfo.positions; - // Ignore numbers we have already exhausted - let numberUsedCount = newUsedInfo.numbers[number]; - if(numberUsedCount >= numbers[number].length) { - continue; - } - // Increment this number's used count if(usedNumbers[number] === undefined) usedNumbers[number] = 1; else usedNumbers[number] += 1; - // Ignore numbers that can only be represented in positions we've used - // And mark this position as used - let openPosition = false; - for(let {position} of numbers[number]) { + // Mark one of the possible positions at this location as used + let n = numbers[number]; + for(let {position} of n) { - if(!usedInfo.positions[position]) { - openPosition = true; + if(!usedPositions[position]) { usedPositions[position] = true; break; } } - if(!openPosition) { - continue; - } - let newSum = currentSum + Number(number); let newCollision = partial.slice(); newCollision.push(number); From 3217e0107c6570e2bd2712b7a6a868ed57462fe0 Mon Sep 17 00:00:00 2001 From: Tyler Bettilyon Date: Wed, 23 Nov 2016 14:45:24 -0700 Subject: [PATCH 5/9] some speed gains with a padding strategy, but its looking like I should try a different tactic --- src/solutions/collision-attack.js | 37 +++++++++++++++++++++++++++---- src/tests/collision-attack.js | 4 ++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/solutions/collision-attack.js b/src/solutions/collision-attack.js index 645bcae..00932d7 100644 --- a/src/solutions/collision-attack.js +++ b/src/solutions/collision-attack.js @@ -82,8 +82,32 @@ module.exports = { collideWithIndex: function(checksum, characterSet) { const numbers = createIndexTimesNumberMap(checksum, characterSet); - let collisionObj = generateAllSums(0); - return collisionObj; + + let charSetOrdered = characterSet.split('').sort(function(a, b) { + return a.charCodeAt(0) - b.charCodeAt(0); + }); + let maxCharCode = charSetOrdered[0].charCodeAt(0); + let fourDigitsOfMax = 10 * maxCharCode; + + // Instead of exhaustively searching, we're "padding" until we've reduced our + // actual search space to about 8 digits. + let searchSum = checksum; + let mockStaringIndex = 9; + while(searchSum > fourDigitsOfMax) { + searchSum -= mockStaringIndex * maxCharCode; + mockStaringIndex += 1; + } + + // "unpad" if we take it too far + if(searchSum < 0) { + searchSum += mockStaringIndex * maxCharCode; + mockStaringIndex == 1; + } + + console.log(searchSum, checksum, mockStaringIndex, fourDigitsOfMax); + let collisionObj = generateAllSums(searchSum, 0); + console.log(collisionObj[0], collisionObj[0] + charSetOrdered[0].repeat(mockStaringIndex - 9)); + return collisionObj[0] + charSetOrdered[0].repeat(mockStaringIndex - 9); /** Now we're using brute force again, but finding the numbers that @@ -98,7 +122,7 @@ module.exports = { be used once, for example, since they each represent a character at an individual position in the string. */ - function generateAllSums(currentSum, partial = [], usedInfo = undefined) { + function generateAllSums(checksum, currentSum, partial = [], usedInfo = undefined) { // Default for the usedInfo is complex if(usedInfo === undefined) { usedInfo = { @@ -161,7 +185,7 @@ module.exports = { let newCollision = partial.slice(); newCollision.push(number); - sumsFoundBelow = sumsFoundBelow.concat(generateAllSums(newSum, newCollision, newUsedInfo)); + sumsFoundBelow = sumsFoundBelow.concat(generateAllSums(checksum, newSum, newCollision, newUsedInfo)); if(sumsFoundBelow.length > 0) { return sumsFoundBelow; } @@ -228,6 +252,11 @@ function stringsFromPositionMap(positionCharacterMap, position = 0, prefix = '') for(let i = position; i < Object.keys(positionCharacterMap).length; i++) { let characters = positionCharacterMap[i]; + // Impossible -- don't continue, no possible strings below. + if(!characters) { + return []; + } + for(let char of characters) { stringsBelow = stringsBelow.concat(stringsFromPositionMap(positionCharacterMap, i+1, prefix + char)); } diff --git a/src/tests/collision-attack.js b/src/tests/collision-attack.js index d930d72..2df4fb8 100644 --- a/src/tests/collision-attack.js +++ b/src/tests/collision-attack.js @@ -44,7 +44,7 @@ describe("collision-attack", function() { describe("collideWidthIndex", function() { it("Should always produce a value that matches the input checksum using only characters from a restricted set", function(){ - const sampleLengths = [4]; + const sampleLengths = [4, 8, 16, 32, 64]; const testStrings = generate.generateRandomSamples(10, sampleLengths); // To speed up the tests @@ -53,7 +53,7 @@ describe("collision-attack", function() { for(let testCase of testStrings) { let checksum = checksums.charcodeTimesIndex(testCase); - let collisionData = collide.collideWithIndex(checksum, generate.alphaChars)[0]; + let collisionData = collide.collideWithIndex(checksum, generate.alphaChars); let collision = checksums.charcodeTimesIndex(collisionData); assert.equal(collision, checksum, `checksum didn't match for test case ${testCase}, returned ${collisionData}`) From 40a0b634135530327c5dee8daaf00c912d163087 Mon Sep 17 00:00:00 2001 From: Tyler Bettilyon Date: Wed, 23 Nov 2016 15:24:51 -0700 Subject: [PATCH 6/9] delete a bunch of stuff for a better life --- src/solutions/collision-attack.js | 200 ++++-------------------------- 1 file changed, 24 insertions(+), 176 deletions(-) diff --git a/src/solutions/collision-attack.js b/src/solutions/collision-attack.js index 00932d7..c809bb2 100644 --- a/src/solutions/collision-attack.js +++ b/src/solutions/collision-attack.js @@ -39,6 +39,8 @@ module.exports = { @param {integer} checksum : the hash value that our returned string must hash to using the simple sum method + + @param {string} characterSet : A string with at least one of each allowed character */ collideWithSimpleSumRestricted: function(checksum, characterSet) { // Performance optimization, greedily pick bigger charCodes first @@ -74,195 +76,41 @@ module.exports = { return a value that collides with the provided checksum when the charcodeSum function is used to hash the return value. - Once again, we're restricted to a specific character set. + Once again, we're restricted to a specific character set. Unfortnuately, + this checksum can't realistically be broken with a brute force search. Doing + so requires solving the "subset-sum" problem, which is NP-Complete. Using a + similar strategy to the above becomes quite slow as soon as we need 16-32 digit + strings. + + Instead we're [doing something that takes advantage of the checksum]. @param {integer} checksum : the hash value that our returned string must hash to using the simple sum method + + @param {string} characterSet : A string with at least one of each allowed character */ collideWithIndex: function(checksum, characterSet) { - const numbers = createIndexTimesNumberMap(checksum, characterSet); + let [maxLength, numbers] = createIndexTimesNumberMap(checksum, characterSet); let charSetOrdered = characterSet.split('').sort(function(a, b) { return a.charCodeAt(0) - b.charCodeAt(0); }); - let maxCharCode = charSetOrdered[0].charCodeAt(0); - let fourDigitsOfMax = 10 * maxCharCode; - - // Instead of exhaustively searching, we're "padding" until we've reduced our - // actual search space to about 8 digits. - let searchSum = checksum; - let mockStaringIndex = 9; - while(searchSum > fourDigitsOfMax) { - searchSum -= mockStaringIndex * maxCharCode; - mockStaringIndex += 1; - } - - // "unpad" if we take it too far - if(searchSum < 0) { - searchSum += mockStaringIndex * maxCharCode; - mockStaringIndex == 1; - } - - console.log(searchSum, checksum, mockStaringIndex, fourDigitsOfMax); - let collisionObj = generateAllSums(searchSum, 0); - console.log(collisionObj[0], collisionObj[0] + charSetOrdered[0].repeat(mockStaringIndex - 9)); - return collisionObj[0] + charSetOrdered[0].repeat(mockStaringIndex - 9); - - /** - Now we're using brute force again, but finding the numbers that - sum to our checksum instead of searching for strings that break - the checksum. This problem is actually well studied, and called - "The subset sum problem". It's also "NP-Complete", which means - many things, but also means we won't be able to generate all - such sums. So we have to limit the search space. - - Luckily, we have some very practical limits in the position - and character set. Each option in the numberMap can only - be used once, for example, since they each represent a - character at an individual position in the string. - */ - function generateAllSums(checksum, currentSum, partial = [], usedInfo = undefined) { - // Default for the usedInfo is complex - if(usedInfo === undefined) { - usedInfo = { - numbers: {}, - positions: {} - }; - } - - if(currentSum > checksum) return []; - - if(currentSum === checksum){ - let collisions = computePotentialCollisions(numbers, partial); - if(collisions === false) { - return []; - } - - for(let c of collisions) { - if(checksums.charcodeTimesIndex(c) === checksum) { - return [c] - } - } - } - - // Performance: Compute the numbers we need to work on BEFORE we clone the info - let validNumbers = Object.keys(numbers).filter(function(number) { - - let openPosition = false; - for(let {position} of numbers[number]) { - // console.log(position, usedInfo.positions[position]); - if(!usedInfo.positions[position]) { - openPosition = true; - break; - } - } - - return openPosition && (!usedInfo.numbers[number] || usedInfo.numbers[number] < numbers[number].length); - }); - - let sumsFoundBelow = []; - for(let number of validNumbers) { - let newUsedInfo = cloneUsedInfo(usedInfo); - let usedNumbers = newUsedInfo.numbers; - let usedPositions = newUsedInfo.positions; - - // Increment this number's used count - if(usedNumbers[number] === undefined) usedNumbers[number] = 1; - else usedNumbers[number] += 1; - - // Mark one of the possible positions at this location as used - let n = numbers[number]; - for(let {position} of n) { - - if(!usedPositions[position]) { - usedPositions[position] = true; - break; - } - } - - let newSum = currentSum + Number(number); - let newCollision = partial.slice(); - newCollision.push(number); - - sumsFoundBelow = sumsFoundBelow.concat(generateAllSums(checksum, newSum, newCollision, newUsedInfo)); - if(sumsFoundBelow.length > 0) { - return sumsFoundBelow; - } - } - - return sumsFoundBelow; + let minCharCode = charSetOrdered[0].charCodeAt(0); + + let startingPoint = 0; + let valueAtPosition = []; + for(let i = 1; i < maxLength; i++) { + let value = i * minCharCode; + valueAtPosition[i-1] = value; + startingPoint += value; } - } -} - - -// Quick way to make a deep clone of the usedInfo obj -function cloneUsedInfo(usedInfo) { - let newObj = { - numbers: {}, - positions: {} - }; - - for(let number in usedInfo.numbers) { - newObj.numbers[number] = usedInfo.numbers[number]; - } - - for(let position in usedInfo.positions) { - newObj.positions[position] = usedInfo.positions[position]; - } - - return newObj; -} - -/** - Given a number map, and the subset sum solution, see if we can use this subset - as our collision given the constraints of the numberMap. We use another exhaustive - recursive search to generate all the strings represented. -*/ -function computePotentialCollisions(numberMap, partial, partialCollision = []) { - let positionCharacterMap = {} - for(let number of partial) { - let options = numberMap[number]; - for(let option of options) { - if(positionCharacterMap[option.position] === undefined) { - positionCharacterMap[option.position] = []; - } - positionCharacterMap[option.position].push(option.character); - } - } - - // Check that it's possible - for(let i = 0; i < partial.length; i++) { - if(positionCharacterMap[i] === undefined) { - return false; - } - } - - return stringsFromPositionMap(positionCharacterMap); -} - -function stringsFromPositionMap(positionCharacterMap, position = 0, prefix = '') { - let stringsBelow = []; - if(prefix.length >= Object.keys(positionCharacterMap).length){ - stringsBelow.push(prefix); - return stringsBelow; - } - for(let i = position; i < Object.keys(positionCharacterMap).length; i++) { - let characters = positionCharacterMap[i]; + let offBy = checksum - startingPoint; + console.log(valueAtPosition); - // Impossible -- don't continue, no possible strings below. - if(!characters) { - return []; - } - - for(let char of characters) { - stringsBelow = stringsBelow.concat(stringsFromPositionMap(positionCharacterMap, i+1, prefix + char)); - } + console.log(startingPoint, checksum, offBy); } - - return stringsBelow; } /** @@ -300,5 +148,5 @@ function createIndexTimesNumberMap(checksum, characteSet) { } } - return numbers; + return [maxLength, numbers]; } From 79820ea3582fcacd5aa1c5be4915232da3cd6094 Mon Sep 17 00:00:00 2001 From: Tyler Bettilyon Date: Mon, 5 Dec 2016 22:04:03 -0800 Subject: [PATCH 7/9] save because i am changing focus... consider squashing this commit --- src/solutions/collision-attack.js | 77 +++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/src/solutions/collision-attack.js b/src/solutions/collision-attack.js index c809bb2..8e64deb 100644 --- a/src/solutions/collision-attack.js +++ b/src/solutions/collision-attack.js @@ -90,27 +90,86 @@ module.exports = { @param {string} characterSet : A string with at least one of each allowed character */ collideWithIndex: function(checksum, characterSet) { - - let [maxLength, numbers] = createIndexTimesNumberMap(checksum, characterSet); - let charSetOrdered = characterSet.split('').sort(function(a, b) { return a.charCodeAt(0) - b.charCodeAt(0); }); + + let [maxLength, numbers] = createIndexTimesNumberMap(checksum, charSetOrdered); + let positionMap = createPositionMap(numbers); + + let minCharCode = charSetOrdered[0].charCodeAt(0); let startingPoint = 0; let valueAtPosition = []; - for(let i = 1; i < maxLength; i++) { - let value = i * minCharCode; - valueAtPosition[i-1] = value; - startingPoint += value; + + for(let i = 0; i < maxLength; i++) { + valueAtPosition[i] = positionMap[i][0]; + startingPoint += positionMap[i][0].value; } + console.log(valueAtPosition) let offBy = checksum - startingPoint; - console.log(valueAtPosition); - console.log(startingPoint, checksum, offBy); + + // Now let "hill climb" by changing valueAtPosition towards offBy is 0... + // There is a risk that this loops forever, which happens all the time. + while(offBy !== 0) { + console.log('================='); + console.log(valueAtPosition.map(function(a){return a.value}).join(',')) + console.log(offBy); + + + + for(let i = 0; i < valueAtPosition.length - 1; i++) { + let currentValue = valueAtPosition[i]; + let valuesForPosition = positionMap[i]; + let newChoice = moveOffsetTowardsZero(offBy, currentValue, valuesForPosition); + offBy = offBy - (newChoice.value - currentValue.value); + valueAtPosition[i] = newChoice; + } + } + + return valueAtPosition.map(function(val, idx){ + return String.fromCharCode(val / (idx + 1)); + }).join(''); + } +} + + +function moveOffsetTowardsZero(offBy, currentValue, possibleValues) { + let bestItem = currentValue; + let bestNewOffset = Math.abs(offBy); + + for(let i = 0; i < possibleValues.length; i++) { + let newValue = possibleValues[i].value; + let newOffBy = offBy - (newValue - currentValue.value); + + if(Math.abs(newOffBy) < bestNewOffset) { + bestItem = possibleValues[i]; + bestNewOffset = newOffBy; + } } + + return bestItem; +} + +function createPositionMap(numbers) { + let positionCharacterMap = {} + for(let number in numbers) { + let options = numbers[number]; + for(let option of options) { + if(positionCharacterMap[option.position] === undefined) { + positionCharacterMap[option.position] = []; + } + positionCharacterMap[option.position].push({ + character: option.character, + value: (option.position+1) * option.character.charCodeAt(0) + }); + } + } + + return positionCharacterMap; } /** From 4870383d9621fd155dee85c36f1ec80d70b3b713 Mon Sep 17 00:00:00 2001 From: Tyler Bettilyon Date: Fri, 23 Dec 2016 13:48:34 -0700 Subject: [PATCH 8/9] final polish and solution for collision-attack --- src/collision-attack.js | 47 +++++++++- src/solutions/collision-attack.js | 147 ++++++++++++++++++++---------- src/tests/collision-attack.js | 6 +- 3 files changed, 147 insertions(+), 53 deletions(-) diff --git a/src/collision-attack.js b/src/collision-attack.js index b49b3f0..40aa1ed 100644 --- a/src/collision-attack.js +++ b/src/collision-attack.js @@ -1,5 +1,5 @@ 'use strict' -const checksums = require('./solutions/checksum'); +const checksums = require('./checksum'); module.exports = { @@ -8,9 +8,50 @@ module.exports = { return a value that collides with the provided checksum when the charcodeSum function is used to hash the return value. - This starting solution is tempting... but doesn't work. Why not? + It's tempting to simply try: return String.fromCharCode(checksum); + give it a shot and find out why it doesn't work... + + @param {integer} checksum : the hash value that our returned string must + hash to using the simple sum method */ collideWithSimpleSum: function(checksum) { - return String.fromCharCode(checksum); + + }, + + /** + Given a checksum value produced by the charcodeSum function + return a value that collides with the provided checksum when + the charcodeSum function is used to hash the return value. + + @param {integer} checksum : the hash value that our returned string must + hash to using the simple sum method + + @param {string} characterSet : A string with at least one of each allowed character + */ + collideWithSimpleSumRestricted: function(checksum, characterSet) { + + }, + + /** + Given a checksum value produced by the charcodeTimesIndex function + return a value that collides with the provided checksum when + the charcodeTimesIndex function is used to hash the return value. + + Once again, we're restricted to a specific character set. Unfortnuately, + this checksum can't realistically be broken with a brute force search. Doing + so requires solving the "subset-sum" problem, which is NP-Complete. Using a + similar strategy to the above becomes quite slow as soon as we need 16-32 digit + strings. + + Instead I've used a hill-clibming technique that explores randomly when anytime + the hashcode is off by the same amount twice in a row. + + @param {integer} checksum : the hash value that our returned string must + hash to using the simple sum method + + @param {string} characterSet : A string with at least one of each allowed character + */ + collideWithCharCodeTimesIndex: function(checksum, characterSet) { + } } diff --git a/src/solutions/collision-attack.js b/src/solutions/collision-attack.js index 8e64deb..a412108 100644 --- a/src/solutions/collision-attack.js +++ b/src/solutions/collision-attack.js @@ -54,7 +54,8 @@ module.exports = { /** I'm using a recursive solution to exhaustively test many solutions. A more clever solution could be faster, but this is good enough for - our test cases. + our test cases. In fact, you'll see the more challenging hash-function + is broken more quickly than this hash function with a "clever" strategy. */ function generateCollision(currentSum, currentCollision) { if(currentSum > checksum) return false; @@ -74,7 +75,7 @@ module.exports = { /** Given a checksum value produced by the charcodeTimesIndex function return a value that collides with the provided checksum when - the charcodeSum function is used to hash the return value. + the charcodeTimesIndex function is used to hash the return value. Once again, we're restricted to a specific character set. Unfortnuately, this checksum can't realistically be broken with a brute force search. Doing @@ -82,61 +83,83 @@ module.exports = { similar strategy to the above becomes quite slow as soon as we need 16-32 digit strings. - Instead we're [doing something that takes advantage of the checksum]. + Instead I've used a hill-clibming technique that explores randomly when anytime + the hashcode is off by the same amount twice in a row. @param {integer} checksum : the hash value that our returned string must hash to using the simple sum method @param {string} characterSet : A string with at least one of each allowed character */ - collideWithIndex: function(checksum, characterSet) { + collideWithCharCodeTimesIndex: function(checksum, characterSet) { let charSetOrdered = characterSet.split('').sort(function(a, b) { return a.charCodeAt(0) - b.charCodeAt(0); }); - let [maxLength, numbers] = createIndexTimesNumberMap(checksum, charSetOrdered); - let positionMap = createPositionMap(numbers); - + let maxLength = computeMaxLength(checksum, charSetOrdered); + let hashContributionMap = createIndexTimesNumberMap(checksum, charSetOrdered); + let hashContributionsByPosition = createHashContributionsByPosition(hashContributionMap); - let minCharCode = charSetOrdered[0].charCodeAt(0); - - let startingPoint = 0; + // Initialize the collision with each index as the smallest-char-code character + let offBy = checksum; let valueAtPosition = []; - for(let i = 0; i < maxLength; i++) { - valueAtPosition[i] = positionMap[i][0]; - startingPoint += positionMap[i][0].value; + valueAtPosition[i] = hashContributionsByPosition[i][0]; + offBy -= hashContributionsByPosition[i][0].value; } - console.log(valueAtPosition) - let offBy = checksum - startingPoint; - console.log(startingPoint, checksum, offBy); - // Now let "hill climb" by changing valueAtPosition towards offBy is 0... // There is a risk that this loops forever, which happens all the time. + let previousOffBy; while(offBy !== 0) { - console.log('================='); - console.log(valueAtPosition.map(function(a){return a.value}).join(',')) - console.log(offBy); - - - - for(let i = 0; i < valueAtPosition.length - 1; i++) { - let currentValue = valueAtPosition[i]; - let valuesForPosition = positionMap[i]; - let newChoice = moveOffsetTowardsZero(offBy, currentValue, valuesForPosition); - offBy = offBy - (newChoice.value - currentValue.value); - valueAtPosition[i] = newChoice; + + // This means we've reached a "shoulder", explore randomly + if(previousOffBy === offBy) { + let randomPosition = Math.floor(Math.random() * valueAtPosition.length); + let randomChoice = Math.floor(Math.random() * hashContributionsByPosition[randomPosition].length); + + let newValue = hashContributionsByPosition[randomPosition][randomChoice]; + let currentValue = valueAtPosition[randomPosition]; + valueAtPosition[randomPosition] = newValue; + + previousOffBy = offBy; + offBy = offBy - (newValue.value - currentValue.value); + } + else { + for(let i = 0; i < valueAtPosition.length - 1; i++) { + let currentValue = valueAtPosition[i]; + let valuesForPosition = hashContributionsByPosition[i]; + let newChoice = moveOffsetTowardsZero(offBy, currentValue, valuesForPosition); + + offBy = offBy - (newChoice.value - currentValue.value); + valueAtPosition[i] = newChoice; + } + + previousOffBy = offBy; } } return valueAtPosition.map(function(val, idx){ - return String.fromCharCode(val / (idx + 1)); + return val.character; }).join(''); } } +//** Private Helper Functions Below This Point **// + +/** + Given an amount by which the current collision attempt is off from the checksum + as well as a value for a particular index, and the possible values for that index + return a new character that moves our offBy amount closest to 0. + @param offBy {integer} -- the amount the current collision is off from the checksum + + @param currentValue {Object} -- an object with keys value and character that represent + the character in a specific position in our collision + + @param possibleValues {Array} -- an array containing Objects each of the same format of + currentValue, representing all the choices for this index +*/ function moveOffsetTowardsZero(offBy, currentValue, possibleValues) { let bestItem = currentValue; let bestNewOffset = Math.abs(offBy); @@ -154,10 +177,20 @@ function moveOffsetTowardsZero(offBy, currentValue, possibleValues) { return bestItem; } -function createPositionMap(numbers) { +/** + Given an object of the format generated by createIndexTimesNumberMap + return a reformatted version of the same data. Specifically in the + format where each key is an index for the collision string, and the + values are all the possible character/checksum-contribution-value + combinations for that index in the collision-string. + + @param hashContributionMap {Object} -- an Object of the format returned from + createIndexTimesNumberMap. +*/ +function createHashContributionsByPosition(hashContributionMap) { let positionCharacterMap = {} - for(let number in numbers) { - let options = numbers[number]; + for(let number in hashContributionMap) { + let options = hashContributionMap[number]; for(let option of options) { if(positionCharacterMap[option.position] === undefined) { positionCharacterMap[option.position] = []; @@ -173,24 +206,44 @@ function createPositionMap(numbers) { } /** - This subroutine generates all the single character values - based on their position in the output string. We use this to - shrink the space of the brute force attack. + Given a checksum value created by the indexTimesCharCode hash + compute the maximum length that a colliding string can possibly + be. We do this by calculating the sum of using the smallest charCode + value in the provided characterSet until that sum exceeds the checksum + value. + + @param checksum {integer} -- a checksum value produced by the indexTimesCharCode + hash function. + + @param characterSet {string} -- a string representing the set of allowed characters + in the collision we wish to create. + */ -function createIndexTimesNumberMap(checksum, characteSet) { - // Compute the maximum length of the string given the characterSet - // and using what we know about the checksum algorithm. - let min = characteSet[characteSet.length - 1].charCodeAt(0); +function computeMaxLength(checksum, characterSet) { + let min = characterSet[characterSet.length - 1].charCodeAt(0); let maxLength = 1; let tmpSum = 0; while(tmpSum < checksum) { tmpSum += (maxLength * min); maxLength++; } - maxLength -= 1; - let numbers = {} - for(let char of characteSet) { + return maxLength - 1; +} + +/** + This subroutine generates all integer values that can be generated + given a checksum and a character set. The output maps those integer + values to the position and character used to produce that integer. + + For example, 97 maps to {character: a, position: 0}, because the + charCode for a is 97, and when it's at the 0th position the indexTimesCharCode + strategy multiplies 97 by 1 to produce the value. +*/ +function createIndexTimesNumberMap(checksum, characterSet) { + let maxLength = computeMaxLength(checksum, characterSet); + let hashContributionMap = {}; + for(let char of characterSet) { for(let i = 0; i < maxLength; i++) { let charWithVal = { character: char, @@ -198,14 +251,14 @@ function createIndexTimesNumberMap(checksum, characteSet) { } let value = char.charCodeAt(0) * (i+1); - if(numbers[value] === undefined) { - numbers[value] = [charWithVal]; + if(hashContributionMap[value] === undefined) { + hashContributionMap[value] = [charWithVal]; } else { - numbers[value].push(charWithVal); + hashContributionMap[value].push(charWithVal); } } } - return [maxLength, numbers]; + return hashContributionMap; } diff --git a/src/tests/collision-attack.js b/src/tests/collision-attack.js index 2df4fb8..4f777a0 100644 --- a/src/tests/collision-attack.js +++ b/src/tests/collision-attack.js @@ -1,5 +1,5 @@ 'use strict' -const collide = require('../solutions/collision-attack'); +const collide = require('../collision-attack'); const checksums = require('../solutions/checksum'); const generate = require('../solutions/string-generator'); const assert = require('chai').assert; @@ -42,7 +42,7 @@ describe("collision-attack", function() { }); }); - describe("collideWidthIndex", function() { + describe("collideWithCharCodeTimesIndex", function() { it("Should always produce a value that matches the input checksum using only characters from a restricted set", function(){ const sampleLengths = [4, 8, 16, 32, 64]; const testStrings = generate.generateRandomSamples(10, sampleLengths); @@ -53,7 +53,7 @@ describe("collision-attack", function() { for(let testCase of testStrings) { let checksum = checksums.charcodeTimesIndex(testCase); - let collisionData = collide.collideWithIndex(checksum, generate.alphaChars); + let collisionData = collide.collideWithCharCodeTimesIndex(checksum, generate.alphaChars); let collision = checksums.charcodeTimesIndex(collisionData); assert.equal(collision, checksum, `checksum didn't match for test case ${testCase}, returned ${collisionData}`) From b1dc1f25d904e356db9f117854c6d56bf9038d9b Mon Sep 17 00:00:00 2001 From: Tyler Bettilyon Date: Fri, 23 Dec 2016 13:48:46 -0700 Subject: [PATCH 9/9] final polish and solution for collision-attack --- .../{04-collision-attack.md => 05-collision-attack.md} | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) rename exercises/{04-collision-attack.md => 05-collision-attack.md} (73%) diff --git a/exercises/04-collision-attack.md b/exercises/05-collision-attack.md similarity index 73% rename from exercises/04-collision-attack.md rename to exercises/05-collision-attack.md index f14e46a..5ac3743 100644 --- a/exercises/04-collision-attack.md +++ b/exercises/05-collision-attack.md @@ -6,11 +6,11 @@ In this section, we're going to create a series of collision attacks, that ultim ## Your Tasks -You're going to create several hash collision attacks with increasing difficulty. +You're going to create 3 hash collision attacks with increasing difficulty. -First we're going to create an attack with very no restrictions on output, against an exceptionally weak hash algorithm. Then we're going to restrict the output to alpha characters. Then we're going to make the hash function a little better and continue to use the restricted character set. These first 3 exercises are to get you thinking about collisions in general. +First we're going to create an attack with very no restrictions on output, against an exceptionally weak hash algorithm. Then we're going to restrict the output to alpha characters. Then we're going to make the hash function a little better and continue to use the restricted character set. -Once we've successfully crafted a few collision attacks using arbitrary data, we're going to put a further restriction on the data: you must send something __useful__. For example, checksums are frequently used as a digital signature on secure content. We're going to simulate "breaking" the signature of something sent to JSON.stringify. Specifically, you're going to attempt to change the userId in cookies like this one: +For a bonus challenge, consider this further restriction on the data: you must send something __useful__. For example, checksums are frequently used as a digital signature on secure content. We're going to simulate "breaking" the signature of something sent to JSON.stringify. Specifically, you're going to attempt to change the userId in cookies like this one: ```js let data = { @@ -21,3 +21,5 @@ let payload = JSON.stringify(data); ``` We're trying to create a collision attack against this "payload", but where the userId has been changed to 42. In this case, we're pretending to be user number 42 without the receiver realizing that they actually authenticated user 1234. Lucky for us, the receiver only looks in the `userId` property of the data. + +How could you write tests and a function that accomplishes this more difficult task?