cryptopals

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

commit 195bbec86afbc84ee3451e35a737eda795a9bb88
parent 6cbe4b901a02775ac0d39426cbd61c44eb86f271
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat,  3 Jun 2023 09:15:54 +0400

More stuff from set 1.

Diffstat:
Mcryptopals.cabal | 46+++++++++++++++++++++++++++++++++++++++++++++-
Mdefault.nix | 2+-
Mdocs/s1.md | 84+++++++++++++++++++++++++++----------------------------------------------------
Alib/Cryptopals/AES.hs | 19+++++++++++++++++++
Mlib/Cryptopals/Util.hs | 8+++++++-
Alib/Cryptopals/Util/ByteString.hs | 45+++++++++++++++++++++++++++++++++++++++++++++
Mlib/Cryptopals/Util/Similarity.hs | 2+-
Asrc/AES.hs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/BreakSingleByteXor.hs | 21++++++++++++++++++---
Asrc/DetectRepeatingKeyXorKeysize.hs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/DetectSingleByteXor.hs | 1-
Msrc/RepeatingKeyXor.hs | 23+++++++++++++++++++----
Asrc/Rotate.hs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 376 insertions(+), 67 deletions(-)

diff --git a/cryptopals.cabal b/cryptopals.cabal @@ -12,6 +12,7 @@ cabal-version: >= 1.10 library default-language: Haskell2010 default-extensions: + BangPatterns LambdaCase OverloadedStrings RecordWildCards @@ -19,7 +20,9 @@ library hs-source-dirs: lib exposed-modules: - Cryptopals.Util + Cryptopals.AES + , Cryptopals.Util + , Cryptopals.Util.ByteString , Cryptopals.Util.Similarity build-depends: base @@ -27,6 +30,7 @@ library , base64 , bytestring , containers + , cryptonite , text executable fixed-xor @@ -94,3 +98,43 @@ executable repeating-key-xor , optparse-applicative , text +executable detect-repeating-key-xor-keysize + main-is: DetectRepeatingKeyXorKeysize.hs + ghc-options: -Wall -O2 + default-language: Haskell2010 + hs-source-dirs: src + build-depends: + base + , base64 + , bytestring + , cryptopals + , optparse-applicative + , psqueues + , text + +executable rotate + main-is: Rotate.hs + ghc-options: -Wall -O2 + default-language: Haskell2010 + hs-source-dirs: src + build-depends: + base + , base16 + , bytestring + , cryptopals + , optparse-applicative + , text + +executable aes + main-is: AES.hs + ghc-options: -Wall -O2 + default-language: Haskell2010 + hs-source-dirs: src + build-depends: + base + , base16 + , bytestring + , cryptopals + , optparse-applicative + , text + diff --git a/default.nix b/default.nix @@ -1,5 +1,5 @@ let - revision = "813836d64"; + revision = "5717cbc076d996e6c07b16706073f09e021d0b99"; tarball = owner: repo: rev: builtins.fetchTarball { diff --git a/docs/s1.md b/docs/s1.md @@ -167,73 +167,47 @@ so if we want to decrypt the ciphertext we'll need to pipe it through #### 1.6 - $ INPUTB64=$(< data/s1/q6_input.txt) - $ INPUTHEX=$(echo $INPUTB64 | ./bin/b642hex) - $ echo $INPUTB64 | ./bin/score_keysizes 4 10 - -Top keysizes for average of 4+ groups are roughly 5, 29, 3. - -Five doesn't go far: - - $ KEYSIZE=5 - $ echo $INPUTHEX | ./bin/rotate $KEYSIZE | parallel -k ./bin/charfreq | less - -Twenty-nine does it: - - $ KEYSIZE=29 - $ echo $INPUTHEX | ./bin/rotate $KEYSIZE | \ - parallel -k 'echo -n {} | ./bin/break_single_byte_xor -r' - T (84) - e (101) - r (114) - m (109) - i (105) - n (110) - a (97) - t (116) - o (111) - r (114) - (32) - X (88) - : (58) - (32) - B (66) - r (114) - i (105) - n (110) - g (103) - (32) - t (116) - h (104) - e (101) - (32) - n (110) - o (111) - i (105) - s (115) - e (101) - - $ xxd -r -p <<< "$INPUTHEX" | \ - ./bin/repeating_key_xor "Terminator X: Bring the noise" | \ - xxd -r -p | tail -c +19 | head -2 +First, determining a keysize for a suspected repeating-key XOR'd +ciphertext; I use an average pairwise normalised Hamming distance: + + $ detect-repeating-key-xor-keysize "$(< data/s1/q6_input.txt)" + cryptopals: keysize of 29 yields minimum score of 2.7856894063790736 + +Then to guess the key itself, one chunks the input into blocks of the +appropriate size and transposes the result, so that every byte in a +transposed block has been XOR'd against a single byte. Doing that and +breaking each block individually yields the key: + + $ input_hex=$(cat data/s1/q6_input.txt | base64 -d | xxd -p | tr -d '\n') + $ key=$(rotate 29 $input_hex | \ + parallel -k 'break-single-byte-xor -l {}' 2> /dev/null | tr -d "\n'") + $ echo $key + Terminator X: Bring the noise + +then use `repeating-key-xor` with the key to recover the plaintext: + + $ repeating-key-xor "$key" --hex "$input_hex" | xxd -r -p | head -2 I'm back and I'm ringin' the bell A rockin' on the mike while the fly girls yell #### 1.7 -I like openssl, heck the rules: +Here we're doing AES-128 decryption in ECB mode. + +It's worth using the `openssl` tool just to get a feel for it: - $ KEY=$(echo -n 'YELLOW SUBMARINE' | xxd -p) + $ key=$(echo -n 'YELLOW SUBMARINE' | xxd -p) $ openssl enc -aes-128-ecb \ - -a -d -K $KEY -nosalt \ + -a -d -K "$key" -nosalt \ -in data/s1/q7_input.txt | head -2 I'm back and I'm ringin' the bell A rockin' on the mike while the fly girls yell -Alternatively, with code: +The `aes` binary will similarly do the trick: - $ cat data/s1/q7_input.txt | tr -d '\n' | ./bin/aes_ecb \ - --key "YELLOW SUBMARINE" | head -2 + $ key=$(echo -n "YELLOW SUBMARINE" | xxd -p) + $ ciphertext=$(cat data/s1/q7_input.txt | base64 -d | xxd -p | tr -d '\n') + $ aes decrypt "$key" "$ciphertext" | xxd -r -p | head -2 I'm back and I'm ringin' the bell A rockin' on the mike while the fly girls yell diff --git a/lib/Cryptopals/AES.hs b/lib/Cryptopals/AES.hs @@ -0,0 +1,19 @@ +module Cryptopals.AES ( + encryptEcbAES128 + , decryptEcbAES128 + ) where + +import qualified Data.ByteString as BS +import qualified Crypto.Cipher.AES as CAES +import qualified Crypto.Cipher.Types as CT +import qualified Crypto.Error as CE + +initAES128 :: BS.ByteString -> CAES.AES128 +initAES128 = CE.throwCryptoError . CT.cipherInit + +encryptEcbAES128 :: BS.ByteString -> BS.ByteString -> BS.ByteString +encryptEcbAES128 key = CT.ecbEncrypt (initAES128 key) + +decryptEcbAES128 :: BS.ByteString -> BS.ByteString -> BS.ByteString +decryptEcbAES128 key = CT.ecbDecrypt (initAES128 key) + diff --git a/lib/Cryptopals/Util.hs b/lib/Cryptopals/Util.hs @@ -2,15 +2,21 @@ module Cryptopals.Util ( Hex(..) , Base64(..) + , CUB.chunks + , CUB.hamming , hexToB64 , fixedXor + , CUB.nhamming , CUS.often - , CUS.score + , CUB.panhamming , repeatingKeyXor + , CUB.rotate + , CUS.score , singleByteXor , CUS.tally ) where +import qualified Cryptopals.Util.ByteString as CUB import qualified Cryptopals.Util.Similarity as CUS import qualified Data.Bits as B import qualified Data.ByteString as BS diff --git a/lib/Cryptopals/Util/ByteString.hs b/lib/Cryptopals/Util/ByteString.hs @@ -0,0 +1,45 @@ +module Cryptopals.Util.ByteString ( + hamming + , nhamming + , panhamming + , chunks + , rotate + ) where + +import qualified Data.Bits as B +import qualified Data.ByteString as BS +import qualified Data.List as L +import qualified Data.List.NonEmpty as NE + +-- | Hamming distance between bytestrings. +hamming :: BS.ByteString -> BS.ByteString -> Maybe Int +hamming l r + | BS.length l /= BS.length r = Nothing + | otherwise = Just (foldr alg 0 (BS.zip l r)) + where + ham a b = B.popCount (B.xor a b) + alg = (+) . uncurry ham + +-- | Normalized Hamming distance between bytestrings. +nhamming :: BS.ByteString -> BS.ByteString -> Maybe Double +nhamming a b = + let len = fromIntegral (BS.length a) + in fmap (\s -> fromIntegral s / len) (hamming a b) + +-- | Average pairwise normalized Hamming distance between bytestrings. +panhamming:: [BS.ByteString] -> Maybe Double +panhamming bs = case bs of + [] -> Nothing + _ -> do + ps <- sequence [nhamming h b | (h:t) <- L.tails bs, b <- t] + pure $ sum ps / fromIntegral (length ps) + +chunks :: Int -> BS.ByteString -> [BS.ByteString] +chunks size = loop mempty where + loop !acc bs + | BS.null bs = reverse acc + | otherwise = case BS.splitAt size bs of + (chunk, rest) -> loop (chunk : acc) rest + +rotate :: Int -> BS.ByteString -> [BS.ByteString] +rotate rows = BS.transpose . chunks rows diff --git a/lib/Cryptopals/Util/Similarity.hs b/lib/Cryptopals/Util/Similarity.hs @@ -9,7 +9,7 @@ import Data.Function (on) import qualified Data.IntMap.Strict as IMS import qualified Data.List as L --- | Similarity of the byte encoding to English plaintext. Smaller is better. +-- | Distance of the encoding from expected English plaintext. score :: BS.ByteString -> Double score = mse english . dist diff --git a/src/AES.hs b/src/AES.hs @@ -0,0 +1,72 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Main where + +import qualified Cryptopals.AES as AES +import qualified Data.ByteString.Base16 as B16 +import qualified Data.Char as C +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Text.IO as TIO +import qualified Options.Applicative as O +import qualified System.Exit as SE +import qualified System.IO as SIO + +data Operation = + Encrypt + | Decrypt + +data Args = Args { + argsOpr :: Operation + , argsKey :: T.Text + , argsInp :: T.Text + } + +ops :: O.Parser Args +ops = Args + <$> operationParser + <*> O.argument O.str (O.metavar "KEY") + <*> O.argument O.str (O.metavar "INPUT") + +operationParser :: O.Parser Operation +operationParser = O.argument op etc where + op = O.eitherReader $ \input -> case fmap C.toLower input of + "encrypt" -> pure Encrypt + "decrypt" -> pure Decrypt + _ -> Left ("invalid operation: " <> input) + + etc = O.metavar "OPERATION" + <> O.help "{encrypt, decrypt}" + +aes :: Args -> IO () +aes Args {..} = do + let args = do + k <- B16.decodeBase16 $ TE.encodeUtf8 argsKey + v <- B16.decodeBase16 $ TE.encodeUtf8 argsInp + pure (k, v) + + case args of + Left e -> do + TIO.hPutStrLn SIO.stderr ("cryptopals: " <> e) + SE.exitFailure + + Right (k, v) -> do + case argsOpr of + Encrypt -> TIO.putStrLn . TE.decodeUtf8 . B16.encodeBase16' $ + AES.encryptEcbAES128 k v + + Decrypt -> TIO.putStrLn . TE.decodeUtf8 . B16.encodeBase16' $ + AES.decryptEcbAES128 k v + +main :: IO () +main = do + let pars = O.info (O.helper <*> ops) $ + O.fullDesc + <> O.progDesc "AES encryption/decryption" + <> O.header "aes" + + args <- O.execParser pars + + aes args + diff --git a/src/BreakSingleByteXor.hs b/src/BreakSingleByteXor.hs @@ -6,6 +6,7 @@ module Main where import qualified Cryptopals.Util as CU import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 +import qualified Data.Char as C import Data.List (foldl') import qualified Data.Text as T import qualified Data.Text.IO as TIO @@ -15,10 +16,22 @@ import qualified Options.Applicative as O import qualified System.Exit as SE import qualified System.IO as SIO -data Args = Args { argsInp :: T.Text } +data Mode = + Decrypt + | Log + +data Args = Args { + argsInp :: T.Text + , argsMod :: Mode + } ops :: O.Parser Args -ops = Args <$> O.argument O.str (O.metavar "INPUT") +ops = Args + <$> O.argument O.str (O.metavar "INPUT") + <*> O.flag Decrypt Log ( + O.long "log" <> O.short 'l' <> + O.help "log the likely enciphering byte" + ) best :: BS.ByteString -> (Word8, Double, BS.ByteString) best s = foldl' alg (0, CU.score s, s) [32..126] where @@ -55,7 +68,9 @@ decipher Args {..} = do ) err $ "cryptopals: result" - out . TE.decodeUtf8 . B16.encodeBase16' $ b + case argsMod of + Decrypt -> out . TE.decodeUtf8 . B16.encodeBase16' $ b + Log -> out . render $ C.chr (fromIntegral byt) main :: IO () main = do diff --git a/src/DetectRepeatingKeyXorKeysize.hs b/src/DetectRepeatingKeyXorKeysize.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Main where + +import qualified Cryptopals.Util as CU +import qualified Data.ByteString as BS +import qualified Data.ByteString.Base64 as B64 +import qualified Data.Text as T +import qualified Data.Text.IO as TIO +import qualified Data.Text.Encoding as TE +import qualified Options.Applicative as O +import qualified System.Exit as SE +import qualified System.IO as SIO + +data Args = Args { argsInp :: T.Text } + +ops :: O.Parser Args +ops = Args <$> O.argument O.str (O.metavar "INPUT") + +score :: BS.ByteString -> Maybe (Double, Int) +score b = loop Nothing 2 where + loop acc siz + | siz == 40 = acc + | otherwise = + let sc = CU.panhamming + . filter (\s -> BS.length s == siz) + . CU.chunks siz + $ b + in case sc of + Nothing -> loop acc (succ siz) + Just s -> + let nacc = case acc of + Nothing -> Just (s, siz) + Just (r, _) -> if s < r + then Just (s, siz) + else acc + in loop nacc (succ siz) + +guess :: Args -> IO () +guess Args {..} = do + let err = TIO.hPutStrLn SIO.stderr + + render :: Show a => a -> T.Text + render = T.pack . show + + s = B64.decodeBase64Lenient $ TE.encodeUtf8 argsInp + + case score s of + Nothing -> do + err "cryptopals: couldn't guess keysize" + SE.exitFailure + + Just (sc, siz) -> do + err ("cryptopals: keysize of " <> render siz <> + " yields minimum score of " <> render sc) + +main :: IO () +main = do + let pars = O.info (O.helper <*> ops) $ + O.fullDesc + <> O.progDesc "guess repeating-key-xor'd keysize" + <> O.header "detect-repeating-key-xor-keysize" + + args <- O.execParser pars + + guess args + diff --git a/src/DetectSingleByteXor.hs b/src/DetectSingleByteXor.hs @@ -9,7 +9,6 @@ import qualified Data.ByteString.Char8 as B8 import qualified Data.Foldable as F import Data.Function (on) import qualified Data.List as L --- import qualified Data.Text as T import qualified Data.Text.IO as TIO import qualified Data.Text.Encoding as TE import qualified Options.Applicative as O diff --git a/src/RepeatingKeyXor.hs b/src/RepeatingKeyXor.hs @@ -10,22 +10,37 @@ import qualified Data.Text.Encoding as TE import qualified Data.Text.IO as TIO import qualified Options.Applicative as O +data Encoding = + Utf8 + | Utf16 + data Args = Args { argsKey :: T.Text , argsInp :: T.Text + , argsEnc :: Encoding } ops :: O.Parser Args ops = Args <$> O.argument O.str (O.metavar "KEY") <*> O.argument O.str (O.metavar "INPUT") + <*> O.flag Utf8 Utf16 ( + O.long "hex" <> + O.help "input is hex-encoded" + ) rxor :: Args -> IO () rxor Args {..} = do - let (k, v) = (TE.encodeUtf8 argsKey, TE.encodeUtf8 argsInp) - res = CU.repeatingKeyXor k v - - TIO.putStrLn . TE.decodeUtf8 . B16.encodeBase16' $ res + let k = TE.encodeUtf8 argsKey + v = case argsEnc of + Utf8 -> pure $ TE.encodeUtf8 argsInp + Utf16 -> B16.decodeBase16 (TE.encodeUtf8 argsInp) + + case v of + Left e -> error "FIXME" + Right s -> + TIO.putStrLn . TE.decodeUtf8 . B16.encodeBase16' $ + CU.repeatingKeyXor k s main :: IO () main = do diff --git a/src/Rotate.hs b/src/Rotate.hs @@ -0,0 +1,51 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Main where + +import qualified Cryptopals.Util as CU +import qualified Data.ByteString.Base16 as B16 +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Text.IO as TIO +import Data.Foldable (for_) +import qualified Options.Applicative as O +import qualified System.Exit as SE +import qualified System.IO as SIO + +data Args = Args { + argsSiz :: Int + , argsInp :: T.Text + } + +ops :: O.Parser Args +ops = Args + <$> O.argument O.auto (O.metavar "ROWS") + <*> O.argument O.str (O.metavar "INPUT") + +rot :: Args -> IO () +rot Args {..} = do + let args = do + v <- B16.decodeBase16 $ TE.encodeUtf8 argsInp + pure (argsSiz, v) + + case args of + Left e -> do + TIO.hPutStrLn SIO.stderr ("cryptopals: " <> e) + SE.exitFailure + + Right (s, v) -> do + let res = CU.rotate s v + for_ res $ TIO.putStrLn . TE.decodeUtf8 . B16.encodeBase16' + +main :: IO () +main = do + let pars = O.info (O.helper <*> ops) $ + O.fullDesc + <> O.progDesc "transpose the target bytes" + <> O.header "rotate" + + args <- O.execParser pars + + rot args +