cryptopals

Matasano's cryptopals challenges (cryptopals.com).
Log | Files | Refs | README | LICENSE

commit 75e926d4f7badeeb7a0bcd6924721ca4770aba78
parent 5d9609ffc97c122cc23fc80cae24ce9de5f5fb89
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 11 Aug 2023 19:33:37 -0230

Add 4.31.

Diffstat:
M.gitignore | 2++
Mdocs/s4.md | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Aetc/crackhmac.sh | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aetc/hmac/hmac.js | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aetc/hmac/index.js | 44++++++++++++++++++++++++++++++++++++++++++++
Aetc/hmac/package.json | 12++++++++++++
6 files changed, 294 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore @@ -1,4 +1,6 @@ .cabal-sandbox cabal.sandbox.config dist-newstyle +node_modules +package-lock.json *swp diff --git a/docs/s4.md b/docs/s4.md @@ -295,7 +295,8 @@ access to a verifying oracle: k <- R.ask pure $ CM.verifymd4mac k mac msg -and let's give it a whirl: +Again, 'md4' is a modified implementation allowing us to resume from an +arbitrary state and use devious padding. Let's give it a whirl: > k <- key > let mac = CM.md4mac k raw @@ -305,3 +306,82 @@ and let's give it a whirl: > CM.verifymd4mac k forged evil True +#### 4.31 + +For this exercise we'll switch it up and write some JavaScript. etc/hmac +contains some JS HMAC utilities (in hmac.js) and a tiny express.js app +(in index.js) that will verify messages and MACs it receives via query +parameters. + +HMAC (Hash-based MAC) is not vulnerable to length-extension attacks +due the presence of multiple (at least two, and potentially more) +enveloping calls to the hash function when creating the MAC. Ferguson et +al. recommended a similar approach in *Cryptography Engineering* when +dealing with Merkle-Damgård hash functions, in which, for hash function +h and message m, instead of using h(m) one uses h(h(m) || m). The HMAC +construction, which goes: + + hmac(k, m) = h((k' + 0x5c) || h((k' + 0x36) || m)) + +for: + + k' = { h(k) for k > block size + { k otherwise + +where + is single-byte XOR and || is concatenation, is more or less the +same idea, except.. moreso. + +Anyway. The idea here is that we have a webserver that does +byte-at-a-time comparison with the supplied MAC, sleeping a little +per byte verified and bailing out early if equality isn't found. So, +we start guessing MACs by working on one byte at a time and checking +whether the server takes appreciably longer to reject what we supply. +If it does, then we probably know that we've matched the value for that +byte, and so we move onto the next one, repeating the same procedure. + +The server does what is expected of us. etc/crackhmac.sh is a +particularly-hacky bash script that calls curl in the requisite loop, +hammering the server with different MACs until it reports verification. + +So, boot up the server via 'node index.js' and elsewhere do: + + $ .etc/crackhmac.sh "secrets.csv" + present MAC guess: 0000000000000000000000000000000000000000 + working on next byte (hexstring index 0).. + found byte b0 + present MAC guess: b000000000000000000000000000000000000000 + working on next byte (hexstring index 2).. + found byte 94 + present MAC guess: b094000000000000000000000000000000000000 + working on next byte (hexstring index 4).. + found byte e8 + present MAC guess: b094e80000000000000000000000000000000000 + working on next byte (hexstring index 6).. + [..] + +N.b., the script is sort of janky, so there are facilities for resuming +the loop in case the next byte can't be found due to timing weirdness. +Append as arguments the index we were working on, the time in seconds +the last comparison took, and the key we've extracted thus far, e.g.: + + $ ./etc/crackhmac.sh "secrets.csv" 20 "0.57" "b094e8b74021e638ca4a" + +I probably shouldn't have chosen to do this with the shell, in +hindsight, and I can immediately think of a better way to handle the +timing variation. But in any case, after a while we should recover the +secret MAC: + + working on next byte (hexstring index 38).. + succeeded + file: secrets.csv + hmac: b094e8b74021e638ca4a65a9ac466cc7fde35c03 + +And we can check our result manually to see HTTP status code 200 (the +'safe' query parameter determines whether we do a sane comparison or +this insane bytewise sleep comparison): + + $ hmac="b094e8b74021e638ca4a65a9ac466cc7fde35c03" + $ curl -o /dev/null --silent -Iw "%{http_code}\n" \ + "localhost:3000/hmac?safe=false&file=secrets.csv&signature=$hmac" + 200 + diff --git a/etc/crackhmac.sh b/etc/crackhmac.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +fil=$1 +lidx=$2 +llas=$3 +lgot=$4 + +if [[ -z "$fil" ]]; then + echo "no file specified. bailing out.." + exit 1 +fi + +if [[ -z "$lidx" ]]; then + lidx=0 + lgot="" + llas=0.049 +fi + +# resume broken loop +sup=$((39 - $lidx)) +sig="$lgot""$(printf '0%.0s' $(seq 0 $sup))" + +hos="localhost:3000" +got="$lgot" + +attempt() { + local res=$(curl -o /dev/null --silent -Iw "%{http_code}\n" "$1") + echo "$res" +} + +weld() { + echo "$hos""/hmac?safe=false&file=""$fil""&signature=""$1" +} + +las="$llas" + +for j in $(seq $lidx 2 38); do + etc="${sig:$((j+2))}" + + echo "present MAC guess: $sig" + echo "working on next byte (hexstring index $j).." + + for b in {0..255}; do + byt=$(printf "%02x" $b) + + can="$got""$byt""$etc" + url=$(weld $can) + + org=$(date +%s.%N) + try=$(attempt $url) + end=$(date +%s.%N) + + tim=$(echo "$end - $org" | bc -l) + dif=$(echo "$tim - $las" | bc -l) + + if (($try == 500)); then + lon=$(echo "$dif > 0.05" | bc -l) # XX need more reliable condition + if (( $lon == 1 )); then + echo "dif: $dif" + got="$got""$byt" + sig="$got""$etc" + las=$tim + echo "found byte $byt" + break + elif (($b == 255)); then + echo "couldn't find byte value. bailing out.." + echo "got: $got" + echo "tim: $tim" + exit 1 + fi + elif (($try == 200)); then + echo "succeeded" + echo "file: $fil" + echo "hmac: $sig" + exit 0 + else + echo "something really weird happened.." + fi + done +done + diff --git a/etc/hmac/hmac.js b/etc/hmac/hmac.js @@ -0,0 +1,74 @@ +const sha1 = require('js-sha1') +const crypto = require('crypto') + +// hmac utils ///////////////////////////////////////////////////////////////// + +const buf2hex = buf => + Buffer.from(buf).toString('hex') + +const hex2buf = hex => + Buffer.from(hex, 'hex') + +const bufs2buf = (a, b) => + Buffer.concat([a, b]) + +// returns hex +const gen_key = () => { + const arr = crypto.getRandomValues(new Uint8Array(16)) + return buf2hex(arr) +} + +// expects hex, returns hex +const pad = (hex, siz) => + hex.padEnd(siz, '0') + +// expects buffer, returns hex +const compute_sized_key = (key, hash, siz) => + key.length > siz ? + hash(key) : + key.length < siz ? + pad(buf2hex(key), siz * 2) : // bytes, not length + buf2hex(key) + +// expects hex, returns hex +const hmac_sha1 = (k, m) => { + const bk = hex2buf(k) + const bm = hex2buf(m) + + const sized_key = hex2buf(compute_sized_key(bk, sha1, 64)) + + const okey_pad = sized_key.map(x => x ^ 0x5c) + const ikey_pad = sized_key.map(x => x ^ 0x36) + + const inner = bufs2buf(ikey_pad, bm) + const hinner = hex2buf(sha1(inner)) + const outer = bufs2buf(okey_pad, hinner) + + return sha1(outer) +} + +const verify_hmac_sha1 = (key, msg, mac) => + hmac_sha1(key, msg) == mac + +async function insecure_compare(key, msg, mac) { + const bcal = Buffer.from(hmac_sha1(key, msg), 'hex') + const bmac = Buffer.from(mac, 'hex') + + const idxs = Array(bcal.length).fill().map((el, idx) => idx) + + for await (const idx of idxs) { + if (bcal[idx] != bmac[idx]) { + return false + } + await new Promise(r => setTimeout(r, 50)) + } + + return true +} + +module.exports = { + gen_key, + hmac_sha1, + verify_hmac_sha1, + insecure_compare +} diff --git a/etc/hmac/index.js b/etc/hmac/index.js @@ -0,0 +1,44 @@ +const express = require('express') +const hmac = require('./hmac') + +// web app //////////////////////////////////////////////////////////////////// + +const sec = hmac.gen_key() + +const app = express() + +app.get('/', (req, res) => { + res.send('waiting to do something cool..') +}) + +app.get('/hmac', async (req, res) => { + const saf = req.query.safe + const fil = req.query.file + const sig = req.query.signature + + const msg = Buffer.from(fil).toString('hex') + const safe = saf == "true" + + const valid = safe + ? hmac.verify_hmac_sha1(sec, msg, sig) + : await hmac.insecure_compare(sec, msg, sig) + + if (valid) { + res.status(200).send({ 'HTTP': 200 }) + } else { + res.status(500).send({ 'HTTP': 500 }) + } + +}) + +const port = 3000 + +app.listen(port, () => { + console.log(`server listening on ${port}`) +}) + +console.log('server generated the following key:') +console.log(`${sec}`) +console.log('for dev convenience, hmac for \'secrets.csv\' is:') +console.log(`${hmac.hmac_sha1(sec, Buffer.from('secrets.csv').toString('hex'))}`) + diff --git a/etc/hmac/package.json b/etc/hmac/package.json @@ -0,0 +1,12 @@ +{ + "name": "hmac", + "version": "0.1.0", + "description": "cryptopals challenge 4.30", + "main": "index.js", + "author": "jtobin", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "js-sha1": "^0.6.0" + } +}