cryptopals

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

commit 8bd46b8164ab79a1152a9e22753c3f183acaadee
parent d007b01a5b96446cd98f35ebc0403892e96ad29f
Author: Jared Tobin <jared@jtobin.io>
Date:   Mon, 14 Aug 2023 14:32:38 -0230

More refactoring.

Diffstat:
Mlib/Cryptopals/DH.hs | 52++++++++++++++++++++++++++++++++++++----------------
Mlib/Cryptopals/DH/Session.hs | 268+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
2 files changed, 236 insertions(+), 84 deletions(-)

diff --git a/lib/Cryptopals/DH.hs b/lib/Cryptopals/DH.hs @@ -11,6 +11,7 @@ import Control.Monad.Trans.State (StateT) import qualified Control.Monad.Trans.State as S import Cryptopals.DH.Core import Cryptopals.DH.Session +import qualified Data.Binary as DB import qualified Data.ByteString as BS import qualified Data.Text as T import GHC.Word (Word32) @@ -21,50 +22,69 @@ import qualified Pipes.Network as PN import qualified System.Random.MWC as MWC -- await key exchange -bob :: MonadIO m => PN.ServiceName -> m a -bob port = PN.serve "localhost" port $ \(sock, _) -> do - let sesh = Sesh { +bob + :: (DB.Binary b, DB.Binary c) + => PN.ServiceName + -> Handler (StateT Sesh IO) b c + -> IO () +bob port eval = PN.serve "localhost" port $ \(sock, _) -> do + let host = "bob" + sesh = Sesh { dhGroup = Nothing + , dhHost = host , dhKeys = Nothing , dhKey = Nothing , dhGen = MWC.createSystemRandom } - slog "bob" $ "listening.." - void $ S.evalStateT (runEffect (session "bob" sock)) sesh + blog host "listening.." + void $ S.evalStateT (runEffect (session sock eval)) sesh -- initiate key exchange -alice :: PN.ServiceName -> IO () -alice port = PN.connect "localhost" port $ \(sock, _) -> do - slog "alice" $ "session established" +alice + :: (DB.Binary b, DB.Binary c) + => PN.ServiceName + -> Handler (StateT Sesh IO) b c + -> IO () +alice port eval = PN.connect "localhost" port $ \(sock, _) -> do + let host = "alice" + blog host "session established" let grp = Group p g gen <- MWC.createSystemRandom per@Keys {..} <- genpair grp gen - slog "alice" $ "sending group parameters and public key" + blog host "sending group parameters and public key" runEffect $ do PB.encode (Just (SendParams grp pub)) >-> PN.toSocket sock let sesh = Sesh { dhGroup = Just grp + , dhHost = host , dhKeys = Just per , dhKey = Nothing , dhGen = pure gen } - void $ S.runStateT (runEffect (session "alice" sock)) sesh + void $ S.runStateT (runEffect (session sock eval)) sesh --- await key exchange -mallory :: MonadIO m => PN.ServiceName -> PN.ServiceName -> m a -mallory port bport = +-- await key exchange, initiate key exchange +mallory + :: (DB.Binary b, DB.Binary c) + => PN.ServiceName + -> PN.ServiceName + -> Handler (StateT Sesh IO) b c + -> IO () +mallory port bport eval = do + let host = "mallory" PN.serve "localhost" port $ \(asock, _) -> do - slog "mallory" $ "LiSteNIng.." + blog host "LiSteNIng.." PN.connect "localhost" bport $ \(bsock, _) -> do let sesh = Sesh { dhGroup = Nothing + , dhHost = host , dhKeys = Nothing , dhKey = Nothing , dhGen = MWC.createSystemRandom } - slog "mallory" $ "eStabLisHed coNNecTion" - void $ S.runStateT (runEffect (dance "mallory" asock bsock)) sesh + blog host "eStabLisHed coNNecTion" + void $ S.runStateT (runEffect (dance asock bsock eval)) sesh diff --git a/lib/Cryptopals/DH/Session.hs b/lib/Cryptopals/DH/Session.hs @@ -4,7 +4,9 @@ module Cryptopals.DH.Session ( Command(..) , Sesh(..) + , Handler + , blog , slog , beval , meval @@ -42,58 +44,112 @@ import qualified System.Exit as SE import qualified System.Random.MWC as MWC data Command = - SendParams Group Natural - | SendPublic Natural - | SendMessage BS.ByteString - | SendTerminal BS.ByteString + SendGroup Group -- group only + | AckGroup -- ack receipt of group params + | SendParams Group Natural -- group + public key + | SendPublic Natural -- public key only + | SendMessage BS.ByteString -- send initial ciphertext + | SendTerminal BS.ByteString -- send final ciphertext deriving (Eq, Show, Generic) instance DB.Binary Command +type Handler m b c = b -> m c + -- session state data Sesh = Sesh { dhGroup :: Maybe Group + , dhHost :: T.Text + -- , dhSock :: PN.Socket -- XX add me , dhKeys :: Maybe Keys , dhKey :: Maybe BS.ByteString , dhGen :: IO (MWC.Gen RealWorld) } +-- basic log +blog :: T.Text -> T.Text -> IO () +blog host msg = TIO.putStrLn $ "(cryptopals) " <> host <> ": " <> msg + -- session log -slog :: T.Text -> T.Text -> IO () -slog host msg = TIO.putStrLn $ "(cryptopals) " <> host <> ": " <> msg +slog :: T.Text -> StateT Sesh IO () +slog msg = do + host <- S.gets dhHost + liftIO $ TIO.putStrLn $ "(cryptopals) " <> host <> ": " <> msg + +-- basic TCP coordination +session + :: (MonadIO m, DB.Binary b, DB.Binary c) + => PN.Socket + -> Handler m b c + -> Effect m (PB.DecodingError, Producer BS.ByteString m ()) +session sock eval = + deco + >-> P.mapM eval + >-> for cat PB.encode + >-> send + where + recv = PN.fromSocket sock 4096 + deco = PP.parsed PB.decode recv + send = PN.toSocket sock + +-- MITM TCP coordination +dance + :: (MonadIO m, DB.Binary b, DB.Binary c) + => PN.Socket + -> PN.Socket + -> Handler m b c + -> Effect m (PB.DecodingError, Producer BS.ByteString m ()) +dance asock bsock eval = + PP.parsed PB.decode recv + >-> P.mapM eval + >-> for cat PB.encode + >-> PN.foxtrot bsock asock + where + recv = PN.rhumba asock bsock 4096 -- generic session evaluator -geval - :: MonadIO m - => (T.Text -> Command -> m a) - -> T.Text +seval + :: (Command -> StateT Sesh IO a) -> Maybe Command - -> m a -geval cont host = \case - Nothing -> liftIO $ do - slog host "ending session" - SE.exitSuccess -- XX should really just close the socket + -> StateT Sesh IO a +seval cont = \case + Nothing -> do + slog "ending session" + liftIO $ SE.exitSuccess -- XX should really just close the socket Just cmd -> do - liftIO $ threadDelay 1000000 - cont host cmd + liftIO $ threadDelay 3000000 + cont cmd -- basic dh evaluation -beval :: T.Text -> Maybe Command -> StateT Sesh IO (Maybe Command) -beval = geval dheval +beval :: Maybe Command -> StateT Sesh IO (Maybe Command) +beval = seval dheval -- mitm dh evaluation -meval :: T.Text -> Maybe Command -> StateT Sesh IO (Maybe Command) -meval = geval mitmeval +meval :: Maybe Command -> StateT Sesh IO (Maybe Command) +meval = seval mitmeval + +-- negotiated-group dh evaluation +geval :: Maybe Command -> StateT Sesh IO (Maybe Command) +geval = seval ngeval + +-- XX refactor some common actions, e.g. assembling ciphertexts -- diffie-hellman protocol eval dheval - :: T.Text - -> Command + :: Command -> StateT Sesh IO (Maybe Command) -dheval host = \case +dheval = \case + SendGroup _ -> do + slog "missing public key, aborting.." + pure Nothing + + AckGroup -> do + slog "didn't send group, aborting.." + pure Nothing + SendParams grp pk -> do sesh@Sesh {..} <- S.get - liftIO $ slog host "received group parameters and public key" + slog "received group parameters and public key" gen <- liftIO dhGen per@Keys {..} <- liftIO $ genpair grp gen let key = derivekey grp per pk @@ -103,19 +159,19 @@ dheval host = \case , dhKey = Just key } S.put nex - liftIO $ slog host "sending public key" + slog "sending public key" pure $ Just (SendPublic pub) SendPublic pk -> do sesh@Sesh {..} <- S.get - liftIO $ slog host "received public key" + slog "received public key" let key = do per@Keys {..} <- dhKeys grp <- dhGroup pure $ derivekey grp per pk case key of Nothing -> do - liftIO $ slog host "key derivation failed" + slog "key derivation failed" pure Nothing Just k -> do gen <- liftIO dhGen @@ -123,7 +179,7 @@ dheval host = \case let msg = CU.lpkcs7 "attack at 10pm" cip = AES.encryptCbcAES128 iv k msg cod = B64.encodeBase64 cip - liftIO . slog host $ "sending ciphertext " <> cod + slog $ "sending ciphertext " <> cod let rep = Just (SendMessage cip) nex = sesh { dhKey = key } S.put nex @@ -132,15 +188,15 @@ dheval host = \case SendMessage cip -> do sesh@Sesh {..} <- S.get let cod = B64.encodeBase64 cip - liftIO $ slog host $ "received ciphertext " <> cod + slog $ "received ciphertext " <> cod case dhKey of Nothing -> do - liftIO $ slog host "shared key not established" + slog "shared key not established" pure Nothing Just k -> do let Just msg = CU.unpkcs7 (AES.decryptCbcAES128 k cip) cod = TE.decodeLatin1 msg - liftIO $ slog host $ "decrypted ciphertext: \"" <> cod <> "\"" + slog $ "decrypted ciphertext: \"" <> cod <> "\"" let hourOfDestiny = case B8.findIndex C.isDigit msg of Nothing -> error "did i fat-finger a digit?" @@ -151,87 +207,163 @@ dheval host = \case let nmsg = CU.lpkcs7 $ "confirmed, attacking at " <> hourOfDestiny ncip = AES.encryptCbcAES128 iv k nmsg ncod = B64.encodeBase64 ncip - liftIO $ slog host $ "replying with ciphertext " <> ncod + slog $ "replying with ciphertext " <> ncod pure $ Just (SendTerminal ncip) SendTerminal cip -> do sesh@Sesh {..} <- S.get let cod = B64.encodeBase64 cip - liftIO $ slog host $ "received ciphertext " <> cod + slog $ "received ciphertext " <> cod case dhKey of Nothing -> do - liftIO $ slog host "shared key not established" + slog "shared key not established" pure Nothing Just k -> do let Just msg = CU.unpkcs7 (AES.decryptCbcAES128 k cip) cod = TE.decodeLatin1 msg - liftIO $ slog host $ "decrypted ciphertext: \"" <> cod <> "\"" + slog $ "decrypted ciphertext: \"" <> cod <> "\"" pure Nothing -- man-in-the-middle protocol eval mitmeval - :: T.Text - -> Command + :: Command -> StateT Sesh IO (Maybe Command) -mitmeval host = \case +mitmeval = \case SendParams grp pk -> do sesh@Sesh {..} <- S.get - liftIO $ slog host "reCEiVed GRoUp pArAmeTErs And pUBliC kEy" + slog "reCEiVed GRoUp pArAmeTErs And pUBliC kEy" let key = derivekey grp (Keys p 1) p nex = sesh { dhKey = Just key } S.put nex - liftIO $ slog host "sEnDinG BOguS paRaMeTeRs" + slog "sEnDinG BOguS paRaMeTeRs" pure $ Just (SendParams grp p) SendPublic pk -> do - liftIO $ slog host "REceIvED pUBlic keY" - liftIO $ slog host "seNDINg boGus kEy" + slog "REceIvED pUBlic keY" + slog "seNDINg boGus kEy" pure $ Just (SendPublic p) SendMessage cip -> do sesh@Sesh {..} <- S.get let cod = B64.encodeBase64 cip - liftIO $ slog host $ "rECeIveD CiPHeRTexT " <> cod + slog $ "rECeIveD CiPHeRTexT " <> cod case dhKey of Nothing -> error "mallory knows key" Just k -> do let Just msg = CU.unpkcs7 (AES.decryptCbcAES128 k cip) cod = TE.decodeLatin1 msg - liftIO $ slog host $ "DEcRyptEd cIPheRTeXt: \"" <> cod <> "\"" - liftIO $ slog host $ "reLayINg cIpheRtExt" + slog $ "DEcRyptEd cIPheRTeXt: \"" <> cod <> "\"" + slog "reLayINg cIpheRtExt" pure $ Just (SendMessage cip) SendTerminal cip -> do sesh@Sesh {..} <- S.get let cod = B64.encodeBase64 cip - liftIO $ slog host $ "reCeiVeD CipHeRtExt " <> cod + slog $ "reCeiVeD CipHeRtExt " <> cod case dhKey of Nothing -> error "mallory knows key" Just k -> do let Just msg = CU.unpkcs7 (AES.decryptCbcAES128 k cip) cod = TE.decodeLatin1 msg - liftIO $ slog host $ "DeCrYpteD cIphErteXt: \"" <> cod <> "\"" - liftIO $ slog host $ "ReLaYINg CiPHeRTexT" + slog $ "DeCrYpteD cIphErteXt: \"" <> cod <> "\"" + slog "ReLaYINg CiPHeRTexT" pure $ Just (SendTerminal cip) --- basic TCP coordination -session host sock = - deco - >-> P.mapM eval - >-> for cat PB.encode - >-> send - where - recv = PN.fromSocket sock 4096 - deco = PP.parsed PB.decode recv - send = PN.toSocket sock - eval = beval host +-- negotiated-group protocol eval +ngeval + :: Command + -> StateT Sesh IO (Maybe Command) +ngeval = \case + SendGroup grp -> do + sesh@Sesh {..} <- S.get + slog "received group parameters" + let nex = sesh { dhGroup = Just grp } + S.put nex + slog "ACK" + pure (Just AckGroup) --- MITM TCP coordination -dance host asock bsock = - PP.parsed PB.decode recv - >-> P.mapM (meval host) - >-> for cat PB.encode - >-> PN.foxtrot bsock asock - where - recv = PN.rhumba asock bsock 4096 + AckGroup -> do + sesh@Sesh {..} <- S.get + slog "ACK ACK" + gen <- liftIO dhGen + case dhGroup of + Nothing -> do + slog "haven't generated group yet" + pure Nothing + Just grp -> do + per@Keys {..} <- liftIO $ genpair grp gen + let nex = sesh { dhKeys = Just per } + S.put nex + slog "sending public key" + pure $ Just (SendPublic pub) + + SendParams grp pk -> do + slog "not expecting group parameters and public key" + pure Nothing + + SendPublic pk -> do + sesh@Sesh {..} <- S.get + slog "received public key" + case dhGroup of + Nothing -> do + slog "don't have group parameters" + pure Nothing + Just grp -> case dhKeys of + Nothing -> do + gen <- liftIO dhGen + per@Keys {..} <- liftIO $ genpair grp gen + let nex = sesh { dhKeys = Just per } + S.put nex + slog "sending public key" + pure (Just (SendPublic pub)) + Just per@Keys {..} -> do + let key = derivekey grp per pk + nex = sesh { dhKey = Just key } + S.put nex + gen <- liftIO dhGen + iv <- liftIO $ CU.bytes 16 gen + let msg = CU.lpkcs7 "attack at 10pm" + cip = AES.encryptCbcAES128 iv key msg + cod = B64.encodeBase64 cip + slog $ "sending ciphertext " <> cod + pure $ Just (SendMessage cip) + + SendMessage cip -> do + sesh@Sesh {..} <- S.get + let cod = B64.encodeBase64 cip + slog $ "received ciphertext " <> cod + case dhKey of + Nothing -> do + slog "shared key not established" + pure Nothing + Just k -> do + let Just msg = CU.unpkcs7 (AES.decryptCbcAES128 k cip) + cod = TE.decodeLatin1 msg + slog $ "decrypted ciphertext: \"" <> cod <> "\"" + + let hourOfDestiny = case B8.findIndex C.isDigit msg of + Nothing -> error "did i fat-finger a digit?" + Just j -> BS.drop j msg + + gen <- liftIO dhGen + iv <- liftIO $ CU.bytes 16 gen + let nmsg = CU.lpkcs7 $ "confirmed, attacking at " <> hourOfDestiny + ncip = AES.encryptCbcAES128 iv k nmsg + ncod = B64.encodeBase64 ncip + slog $ "replying with ciphertext " <> ncod + pure $ Just (SendTerminal ncip) + + SendTerminal cip -> do + sesh@Sesh {..} <- S.get + let cod = B64.encodeBase64 cip + slog $ "received ciphertext " <> cod + case dhKey of + Nothing -> do + slog "shared key not established" + pure Nothing + Just k -> do + let Just msg = CU.unpkcs7 (AES.decryptCbcAES128 k cip) + cod = TE.decodeLatin1 msg + slog $ "decrypted ciphertext: \"" <> cod <> "\"" + pure Nothing