Last active
February 23, 2023 03:28
-
-
Save justingreenberg/2f3a3a8b90ab63a043d5949e688cd9ca to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as assert from 'assert' | |
import * as crypto from 'crypto' | |
import * as data from './data' | |
function ascendingByProp(prop) { | |
return (a, b) => b[prop] - a[prop] | |
} | |
function descendingByProp(prop) { | |
return (a, b) => a[prop] - b[prop] | |
} | |
// Set 1 Challenge 1 Convert hex to base64 | |
export function stringToBase64(hexString) { | |
return Buffer.from(hexString, 'hex').toString('base64') | |
} | |
// Set 1 Challenge 2 Fixed XOR | |
export function xorByteArray(byteArray1, byteArray2) { | |
return byteArray1.map((byte, byteIndex) => byte ^ byteArray2.at(byteIndex)) | |
} | |
export function hexStringToByteArray(hexString) { | |
return Buffer.from(hexString, 'hex') | |
} | |
export function xorHexString(hexString1, hexString2) { | |
return xorByteArray( | |
hexStringToByteArray(hexString1), | |
hexStringToByteArray(hexString2), | |
).toString('hex') | |
} | |
// Set 1 Challenge 3 Single-byte XOR cipher | |
export function calculateEnglishScore(rawString: string): number { | |
const getEnglighScore = char => | |
data.englishCharFrequencies[char.toLowerCase()] || 0 | |
return Array.from(rawString).reduce( | |
(totalScore, currentChar) => (totalScore += getEnglighScore(currentChar)), | |
0, | |
) | |
} | |
type BruteforceScoredResults = { | |
key: number | |
plaintext: string | |
score: number | |
} | |
export function bruteforceDecryptSingleCharXOR( | |
byteArray: Uint8Array, | |
): BruteforceScoredResults | |
export function bruteforceDecryptSingleCharXOR(byteArray) { | |
const xorByteArrayWithKeyIndex = (_, keyIndex) => ({ | |
key: keyIndex, | |
plaintext: byteArray | |
.map(byteCharCode => byteCharCode ^ keyIndex) | |
.toString(), | |
}) | |
const addScoreForPlaintext = ({ key, plaintext }) => ({ | |
key, | |
plaintext, | |
score: calculateEnglishScore(plaintext), | |
}) | |
return Array.from({ length: 255 }) // ascii char codes 0-255 | |
.map(xorByteArrayWithKeyIndex) | |
.map(addScoreForPlaintext) | |
.sort(ascendingByProp('score')) | |
.at(0) | |
} | |
// Set 1 Challenge 4 Detect single-character XOR | |
export function detectSingleCharXOR(ciphertextsArray): BruteforceScoredResults { | |
return ciphertextsArray | |
.map(hexStringToByteArray) | |
.map(bruteforceDecryptSingleCharXOR) | |
.sort(ascendingByProp('score')) | |
.at(0) | |
} | |
// Set 1 Challenge 5 Implement repeating-key XOR | |
function createGetNextCharCode(keyString): () => number { | |
const getKeyChar = (function* (keyCharIndex = 0) { | |
while (true) | |
if (keyCharIndex === keyString.length) keyCharIndex = 0 | |
else yield keyString.charAt(keyCharIndex++) | |
})() | |
return () => getKeyChar.next().value.codePointAt(0) | |
} | |
export function applyRepeatingKeyXOR(byteArray, key): Uint8Array { | |
const getNextCharCode = createGetNextCharCode(key) | |
const xorWithNextCharCode = byteCharCode => byteCharCode ^ getNextCharCode() | |
return byteArray.map(xorWithNextCharCode) | |
} | |
// Set 1 Challenge 6 Break repeating-key XOR | |
export function makeBlocks(byteArray, blockSize = 8) { | |
let result = [] | |
for (let i = 0; i < byteArray.length; i += blockSize) | |
result.push(byteArray.slice(i, i + blockSize)) | |
return result | |
} | |
export function computeHammingSize(str1, str2) { | |
const byteArray2 = Buffer.from(str2) | |
return Buffer.from(str1).reduce((distance, currentByte, byteIndex) => { | |
return ( | |
distance + | |
--(currentByte ^ byteArray2[byteIndex]).toString(2).split('1').length | |
) | |
}, 0) | |
} | |
export function estimateKeysize( | |
byteArray, | |
minimumKeysize = 2, | |
maximumKeysize = 40, | |
) { | |
let editDistances = [] | |
for ( | |
let currentKeysize = minimumKeysize; | |
currentKeysize <= maximumKeysize; | |
currentKeysize++ | |
) { | |
let keysizeEditDistances = [] | |
const blocks = makeBlocks(byteArray, currentKeysize) | |
while (blocks.length >= 2) | |
keysizeEditDistances.push( | |
computeHammingSize(blocks.shift(), blocks.shift()) / currentKeysize, | |
) | |
editDistances.push({ | |
keysize: currentKeysize, | |
distance: | |
keysizeEditDistances.reduce((a, b) => a + b, 0) / | |
keysizeEditDistances.length, | |
}) | |
} | |
return editDistances.sort(descendingByProp('distance')).at(0).keysize | |
} | |
export function transposeBlocksAndBruteforceKey(byteArray, keysize) { | |
const blocks = makeBlocks(byteArray, keysize) | |
const getBlockAtKeyIndex = keyIndex => | |
blocks.reduce( | |
(nextByteArray, block, offsetIndex) => | |
nextByteArray.fill(block.at(keyIndex), offsetIndex), | |
Buffer.alloc(keysize), | |
) | |
return Buffer.alloc(keysize) | |
.map((_, i) => bruteforceDecryptSingleCharXOR(getBlockAtKeyIndex(i)).key) | |
.toString() | |
} | |
export function bruteforceDecryptRepeatingKeyXOR(byteArray) { | |
const keysize = estimateKeysize(byteArray) | |
const key = transposeBlocksAndBruteforceKey(byteArray, keysize) | |
const plaintext = applyRepeatingKeyXOR(byteArray, key).toString() | |
return { key, plaintext } | |
} | |
// Set 1 Challenge 7 AES in ECB mode | |
export function decryptAES128ECB(byteArray: Buffer, key, autoPadding = false) { | |
const decipher = crypto | |
.createDecipheriv('aes-128-ecb', key, null) | |
.setAutoPadding(autoPadding) | |
return Buffer.concat([decipher.update(byteArray), decipher.final()]) | |
} | |
// Set 1 Challenge 8 Detect AES in ECB mode | |
export function countDuplicates(blocks) { | |
return blocks | |
.map(currentBlock => blocks.filter(block => block === currentBlock).length) | |
.sort() | |
.at(-1) | |
} | |
export function detectAES128ECB(ciphertextsArray, keysize = 16) { | |
const getCount = (ciphertext, ciphertextsArrayIndex) => ({ | |
index: ciphertextsArrayIndex, | |
ciphertext, | |
count: countDuplicates(makeBlocks(ciphertext, keysize)), | |
}) | |
return ciphertextsArray.map(getCount).sort(ascendingByProp('count')).at(0) | |
} | |
// Set 2 Challenge 9 Implement PKCS#7 padding | |
export function padPKCS7(plaintext, blocksize) { | |
if (plaintext.length === blocksize) return plaintext | |
else if (plaintext.length < blocksize) | |
return plaintext.padEnd( | |
blocksize, | |
Buffer.from([blocksize % plaintext.length]).toString(), | |
) | |
const paddedLength = | |
(Math.floor(plaintext.length / blocksize) + 1) * blocksize | |
return plaintext.padEnd( | |
paddedLength, | |
Buffer.from([paddedLength - plaintext.length]).toString(), | |
) | |
} | |
export function unpadPKCS7(plaintext) { | |
const lastChar = plaintext.split('').at(-1) | |
const [paddingCount] = Buffer.from(lastChar) | |
for (let i = plaintext.length - paddingCount; i < plaintext.length; i++) | |
if (Buffer.from(plaintext).at(i) !== paddingCount) return plaintext | |
return plaintext.slice(0, plaintext.length - paddingCount) | |
} | |
// Set 2 Challenge 10 Implement CBC mode | |
export const BLOCKSIZE = 16 // 16 bytes | |
export function encryptAES128ECB(byteArray, key, autoPadding = true) { | |
const cipher = crypto | |
.createCipheriv('aes-128-ecb', key, null) | |
.setAutoPadding(autoPadding) | |
return Buffer.concat([cipher.update(byteArray), cipher.final()]) | |
} | |
export function padPKCS7b(byteArray, blocksize) { | |
const paddingCount = blocksize % byteArray.byteLength | |
const padding = Buffer.alloc(paddingCount, paddingCount) | |
// TODO: handle bytearray > blocksize | |
return Buffer.concat([byteArray, padding]) | |
} | |
export function encryptAES128CBC(byteArray, keyString) { | |
const ciphertextBlocks = [] | |
let previousBlock = Buffer.alloc(BLOCKSIZE, 0) | |
const paddedByteArray = padPKCS7b(byteArray, BLOCKSIZE) | |
for (let i = 0; i < paddedByteArray.length; i += BLOCKSIZE) { | |
const currentBlock = paddedByteArray.slice(i, i + BLOCKSIZE) | |
const currentCiphertext = encryptAES128ECB( | |
xorByteArray(currentBlock, previousBlock), | |
keyString, | |
) | |
ciphertextBlocks.push(currentCiphertext) | |
previousBlock = currentCiphertext | |
} | |
return Buffer.concat(ciphertextBlocks) | |
} | |
export function decryptAES128CBC(byteArray, keyString) { | |
const plaintextBlocks = [] | |
let previousBlock = Buffer.alloc(BLOCKSIZE, 0) | |
for (let i = 0; i < byteArray.length; i += BLOCKSIZE) { | |
const currentBlock = byteArray.slice(i, i + BLOCKSIZE) | |
const currentDecipher = decryptAES128ECB(currentBlock, keyString, false) | |
const plaintextBlock = xorByteArray(currentDecipher, previousBlock) | |
plaintextBlocks.push(plaintextBlock) | |
previousBlock = currentBlock | |
} | |
const plaintextString = Buffer.concat(plaintextBlocks).toString() | |
return unpadPKCS7(plaintextString) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment