密码学(一):异或密码与 AES
注:曾经我使用别的语言(Julia)写过类似的内容,但没有良好的解耦。因此改用 Haskell 进行更清晰的实现,读者也可使用自己熟悉的语言实现。
本文实现 The Cryptopals Crypto Challenges 的基础练习集 Set 1.
格式转换
Always operate on raw bytes, never on encoded strings. Only use hex and base64 for pretty-printing.
type Raw = [Bool]
readSized sz raw = sum [if raw !! i then 2 ^ (sz - i - 1) else 0 | i <- [0 .. sz - 1]]
writeSized sz int
| sz == 0 = []
| otherwise = writeSized (sz - 1) (int `div` 2) ++ [int `mod` 2 == 1]
解耦
一些常用的功能。
以指定的方式填充:
pad len gen list
| pos == 0 = list
| otherwise = list ++ map gen [(pos + 1) .. len]
where
分段:
splits len = unfoldr split
where
split [] = Nothing
split xs = Just (splitAt len xs)
Hex
这里实际上存在大小端的约定问题。按照 Challenge 1 的结果,对 $4=(0100)_2$,按从左至右顺序排布。
readHex = concatMap read1
where
read1 c = writeSized 4 $ fromJust $ lookup c (zip table [0 .. 15])
where
table = "0123456789abcdef"
writeHex raw = map write4 (splits 4 padded)
where
padded = pad 4 (const False) raw
write4 seq = table !! readSized 4 seq
where
table = "0123456789abcdef"
我们已经可以实现 Challenge 2
Base64
writeBase64 raw = map write6 (splits 6 padded)
where
padded = pad 6 (const False) raw
write6 seq = table !! readSized 6 seq
where
table = ['A' .. 'Z'] ++ ['a' .. 'z'] ++ ['0' .. '9'] ++ "+/"
有了这些东西我们已经可以实现 Challenge 1
Ascii
readAscii = concatMap (writeSized 8 . ord)
writeAscii raw = map write8 (splits 8 padded)
where
padded = pad 8 (const False) raw
write8 seq = chr $ readSized 8 seq
异或密码
单位异或
Challenge 3 要求 do this by hand
观察十六进制编码,分为两个一段,即
1b 37 37 33 31 36 3f 78 15 1b 7f 2b 78 34 31 33 3d 78 39 78 28 37 2d 36 3c 78 37 3e 78 3a 39 3b 37 36
由于 78 大量出现,猜测它是空格,再猜测 39 是 a 发现是一致的。
英语词频分析的方法似乎有很多,包括单/双/三/四字母组词频、常用单词词频、首/末字母词频、双重字母词频。这里我们先使用单字母词频。
随着 AI 生成内容的大量出现,英文词频发生了较大的失真。
这里的思路是直接计算赋值的和。
tableSpaceFrequency = 0.1918182
tableAsciiFrequency =
[ 0.0651738,
0.0124248,
0.0217339,
0.0349835,
0.1041442,
0.0197881,
0.0158610,
0.0492888,
0.0558094,
0.0009033,
0.0050529,
0.0331490,
0.0202124,
0.0564513,
0.0596302,
0.0137645,
0.0008606,
0.0497563,
0.0515760,
0.0729357,
0.0225134,
0.0082903,
0.0171272,
0.0013692,
0.0145984,
0.0007836
]
evalPhrase str = sum $ map eval1 str
where
eval1 c
| c == ' ' = tableSpaceFrequency
| isAsciiLower c = tableAsciiFrequency !! (ord c - ord 'a')
| isAsciiUpper c = tableAsciiFrequency !! (ord c - ord 'A')
| otherwise = 0.0
另一种计算思路是比较频率的分布与英文词频的相似程度。
evalPhrase str = sum $ map (\i -> (freqs !! i - tableAsciiFrequency !! i) ^ 2 / (tableAsciiFrequency !! i)) [0 .. 25]
where
freqs = [num i / fromIntegral (length str) | i <- [0 .. 25]]
where
num i = foldr (\chr cnt -> if ord chr - ord 'a' == i || ord chr - ord 'A' == i then cnt + 1 else cnt) 0 str
不过它在 Haskell 中就太慢了,以下还是使用前者。
现在来实现 Challenge 4
检测的思路是:看所有解密可能中评分最高的。
performScXor n = map perform
where
perform c = chr $ xor (ord c) n
detectScXor str = maximum (map resultDecrypt [1 .. 127])
where
resultDecrypt x = (\y -> (evalPhrase y, y)) $ performScXor x str
challenge_4 list = (snd $ fst best, snd best)
where
best = maximum $ map (\str -> (detectScXor str, str)) strings
strings = map (writeAscii . readHex . pad 60 (const '0')) list
得到的结果为:
("Now that the party is jumping\n","{ZB\NAKA]TA\NAKA]P\NAKETGAL\NAK\\F\NAK_@XE\\[R?")
多位异或
Challenge 5 也是易做的:
performRpXor key str = map (\(id, c) -> chr $ xor (ord c) (ord $ key !! mod id (length key))) indexed
where
indexed = zip [0 .. length str - 1] str
Challenge 6 比较有挑战,文本中指出的工作流程如下:
- 令 KEYSIZE 为猜测的密钥长,不妨取 2 ~ 40
- 写一个计算 edit distance/Hamming distance 的函数
- 对每个 KEYSIZE,从文本中依次取出长度为它的两段,计算 edit distance 并除以 KEYSIZE
- 上述的约化结果中最小的可能是正确的 KEYSIZE(为保证正确性,可能保留多个小的、计算多个片段)
- 现在把原文本切成 KEYSIZE 大小的块
- 对这些块作转置,即,考虑每个块的第 k 位,组成新块
- 每个块的做法即单位异或解密的方法(如果没有解密是没有考虑字符之间的关系)
- 现在把它们拼在一起
evalEditDistance x y = foldr (\i c -> if x !! i == y !! i then c else c + 1) 0 [0 .. length x - 1]
-- 其余内容略去
AES
有密钥解密
Challenge 7 部分略过。
检测
Challenge 8 中,用到 ECB 是无状态且确定的,相同的 16 字节段的加密结果一致,我们去检测是否存在这样的情况。