commit 9f663beda8bacccc4d138aa67543ff7d3069e73c
parent 67c029356b1fe359bc143a1ecb61e190d805012a
Author: Jared Tobin <jared@jtobin.io>
Date: Sun, 6 Aug 2023 20:45:10 -0230
Add 4.27.
Diffstat:
2 files changed, 81 insertions(+), 9 deletions(-)
diff --git a/docs/s4.md b/docs/s4.md
@@ -78,4 +78,48 @@ the desired plaintext:
#### 4.27
+This one works exactly as advertised. The ivl{encrypt, decrypt}CbcAES128
+functions in Cryptopals.Block.Attacks will {en, de}crypt inputs in CBC
+mode using identical key and IV's, and ivlVerifier serves as the desired
+oracle.
+
+First, assembling the nasty ciphertext:
+
+ > let b = "YELLOW SUBMARINE"
+ > B16.encodeBase16 consistentKey
+ "d18a7e96a50f45cb9b928e502c2b310d"
+ > let cip = ivlEncryptCbcAES128 consistentKey (b <> b <> b)
+ > let cs = CU.chunks 16 cip
+ > let mcip = cs !! 0 <> BS.replicate 16 0 <> cs !! 0
+
+And now recovering the key:
+
+ > let Left mpay = bfcIvVerifier mcip
+ > let ps = CU.chunks 16 mpay
+ > B16.encodeBase16 $ (ps !! 0) `CU.fixedXor` (ps !! 2)
+ "d18a7e96a50f45cb9b928e502c2b310d"
+
+As for how this works: refer back to the omnipresent CBC-mode decryption
+scheme from 2.16 (here modified):
+
+ for ciphertext c = (c_1, c_2, c_3)
+ block decryption w/key k dec_k
+ xor operator +
+
+ let p_1 = dec_k(c_1) + k
+ p_2 = dec_k(c_2) + c_1
+ p_3 = dec_k(c_3) + c_2
+
+ in plaintext p = (p_1, p_2, p_3)
+
+So if we provide the modified `c = (c_1, 0, c_1)`, decryption will give us:
+
+ p_1' = dec_k(c_1) + k
+ p_2' = dec_k(0) + c_1
+ p_3' = dec_k(c_1) + 0
+
+such that, trivially:
+
+ p_1' + p_3' = dec_k(c_1) + k + dec_k(c_1) + 0
+ = k.
diff --git a/lib/Cryptopals/Block/Attacks.hs b/lib/Cryptopals/Block/Attacks.hs
@@ -362,13 +362,41 @@ rnBest s = loop (0, 1 / 0, s) 0 where
-- CBC key recovery w/IV=key
-bfcIvEncrypter :: BS.ByteString -> BS.ByteString
-bfcIvEncrypter input =
- AES.encryptCbcAES128 consistentKey consistentKey padded
- where
- filtered = BS.filter (`notElem` (BS.unpack ";=")) input
- plaintext = "comment1=cooking%20MCs;userdata=" <> filtered <>
- ";comment2=%20like%20a%20pound%20of%20bacon"
- padded = CU.lpkcs7 plaintext
-
+-- Usually we include the IV with the ciphertext, but that won't fly here
+-- as it would very obviously expose the key. Instead let's omit the IV
+-- in the ciphertext:
+ivlEncryptCbcAES128
+ :: BS.ByteString -> BS.ByteString -> BS.ByteString
+ivlEncryptCbcAES128 key plaintext = loop key mempty (BS.splitAt 16 plaintext)
+ where
+ loop las !acc (b, bs) =
+ let xed = CU.fixedXor las b
+ enc = AES.encryptEcbAES128 key xed
+ nacc = acc <> enc
+ in if BS.null bs
+ then nacc
+ else loop enc nacc (BS.splitAt 16 bs)
+
+ivlDecryptCbcAES128
+ :: BS.ByteString -> BS.ByteString -> BS.ByteString
+ivlDecryptCbcAES128 key ciphertext =
+ let (iv, cip) = BS.splitAt 16 (key <> ciphertext)
+ in loop iv mempty (BS.splitAt 16 cip)
+ where
+ loop !las !acc (b, bs) =
+ let dec = AES.decryptEcbAES128 key b
+ nacc = acc <> CU.fixedXor dec las
+ niv = b
+ in if BS.null bs
+ then nacc
+ else loop b nacc (BS.splitAt 16 bs)
+
+ivlVerifier :: BS.ByteString -> Either BS.ByteString Bool
+ivlVerifier cip = loop pay where
+ pay = ivlDecryptCbcAES128 consistentKey cip
+ loop p = case BS.uncons p of
+ Nothing -> pure True
+ Just (b, bs)
+ | b > 127 -> Left pay
+ | otherwise -> loop bs