Skip to content

Instantly share code, notes, and snippets.

@justingreenberg
Last active February 23, 2023 03:28
Show Gist options
  • Save justingreenberg/2f3a3a8b90ab63a043d5949e688cd9ca to your computer and use it in GitHub Desktop.
Save justingreenberg/2f3a3a8b90ab63a043d5949e688cd9ca to your computer and use it in GitHub Desktop.
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