cryptopals

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

commit fc3a9295ef7e0876407a89fb9019cb6c4166d4c1
parent 4ab6c492675ae8cadd151004cd1e51e07d7f89c2
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 13 Aug 2023 20:36:51 -0230

Moar fun for 5.33.

Diffstat:
M.ghci | 1+
Mcryptopals.cabal | 6++++++
Mdocs/s5.md | 60+++++++++++++++++++++++++++++++++---------------------------
Mlib/Cryptopals/DH.hs | 239++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Alib/Pipes/Network.hs | 46++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 304 insertions(+), 48 deletions(-)

diff --git a/.ghci b/.ghci @@ -1,5 +1,6 @@ :set prompt "> " :set -XOverloadedStrings +import qualified Data.Binary as DB import qualified Data.Binary.Get as BG import qualified Data.Binary.Put as BP import qualified Data.Bits as B diff --git a/cryptopals.cabal b/cryptopals.cabal @@ -34,6 +34,7 @@ library , Cryptopals.Util , Cryptopals.Util.ByteString , Cryptopals.Util.Similarity + , Pipes.Network build-depends: base , base16 @@ -44,6 +45,11 @@ library , cryptonite , HTTP , mwc-random + , network + , network-simple + , pipes + , pipes-binary + , pipes-parse , primitive , text , time diff --git a/docs/s5.md b/docs/s5.md @@ -1,13 +1,13 @@ #### 5.33 The basic Diffie-Hellman algorithm for key exchange between Alice and -Bob goes as follows. Alice and Bob agree on a cyclic group of some -particular order to use. They each randomly and independently pick some -number of times to perform the group operation (which must be less than -the order of the group) and perform the group operation on the generator -that number of times, publishing their results. For generator g, Alice's -number x, and Bob's number y: Alice publishes g ^ x, and Bob publishes g -^ y. +Bob goes as follows. Alice and Bob agree on a finite cyclic group of +some particular order to use. They each randomly and independently pick +some number of times to perform the group operation (which must be +greater than zero, and less than the order of the group) and perform the +group operation on the generator that number of times, publishing their +results. For generator g, Alice's number x, and Bob's number y: Alice +publishes g ^ x, and Bob publishes g ^ y. Each then performs his or her own secret number of additional group operations on the other's public result, establishing the key g ^ xy. @@ -22,8 +22,8 @@ group, i.e. the multiplicative group of 16-bit words modulo 37: > gen <- MWC.create > let p = 37 > let g = 5 - > a <- fmap (`mod` p) (MWC.uniform gen) :: IO Word16 - > b <- fmap (`mod` p) (MWC.uniform gen) :: IO Word16 + > a <- fmap (`mod` p) (MWC.uniformR (1, p - 1) gen) :: IO Word16 + > b <- fmap (`mod` p) (MWC.uniformR (1, p - 1) gen) :: IO Word16 > let bigA = g ^ a `mod` p > let bigB = g ^ b `mod` p > let s = bigB ^ a `mod` p @@ -49,8 +49,8 @@ range. First a modular exponentiation routine: and given that (and appropriate p, g), the key exchange: > gen <- MWC.create - > a <- fmap (`mod` p) (MWC.uniformRM (0, p - 1) gen) - > b <- fmap (`mod` p) (MWC.uniformRM (0, p - 1) gen) + > a <- fmap (`mod` p) (MWC.uniformRM (1, p - 1) gen) + > b <- fmap (`mod` p) (MWC.uniformRM (1, p - 1) gen) > let bigA = modexp g a p > let bigB = modexp g b p > let s = modexp bigB a p @@ -58,23 +58,29 @@ and given that (and appropriate p, g), the key exchange: > s == t True -Our SHA1 function needs a bytestring to operate on, so to hash the -Natural key we need a quick serialization routine from Naturals -to ByteString. I happened to write one of these a few years ago -for [urbit-hob](http://git.jtobin.io/urbit-hob); it serializes in -little-endian format, and here we'll use a lazy bytestring: +That's all well and good, but let's have a bit of fun. - unroll :: Natural -> BL.ByteString - unroll nat = case nat of - 0 -> BL.singleton 0 - _ -> BL.pack (L.unfoldr step nat) - where - step 0 = Nothing - step i = Just (fromIntegral i, i `B.shiftR` 8) +Cryptopals.DH implements the Diffie-Hellman protocol over TCP. Two +functions, 'bob' and 'alice', will initiate a TCP server and client, +respectively. Each end will then cooperate to perform Diffie-Hellman, +for the lulz then exchanging an AES128-encrypted message with the shared +key à la the initial illustration in the next challenge. -so we can get a key via: +Opening two instances of GHCi, we can run 'bob' in one and 'alice' in +the other and watch the logs for fun. Here I'll interleave the relevant +parts of the logs for illustration: - > let k = S.sha1 $ unroll s - > k - 6a86b02347be741dfb6f876819022ae1d7adda18 + (cryptopals) bob: listening.. + (cryptopals) alice: session established + (cryptopals) alice: sending group parameters and public key + (cryptopals) bob: received group parameters and public key + (cryptopals) bob: sending public key + (cryptopals) alice: received public key + (cryptopals) alice: sending ciphertext + (cryptopals) bob: received ciphertext + (cryptopals) bob: decrypted ciphertext: "attack at 10pm" + (cryptopals) bob: replying with ciphertext + (cryptopals) alice: received ciphertext + (cryptopals) alice: decrypted ciphertext: "confirmed, attacking at 10pm" + (cryptopals) bob: ending session diff --git a/lib/Cryptopals/DH.hs b/lib/Cryptopals/DH.hs @@ -1,25 +1,83 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE RecordWildCards #-} module Cryptopals.DH ( p , g , modexp - - , unroll - , roll ) where +import Control.Concurrent (threadDelay) +import Control.Monad import Control.Monad.Primitive -import qualified Control.Monad.Trans.Reader as R -import qualified Cryptopals.Digest.Pure.SHA as S +import Control.Monad.Trans.State (StateT) +import qualified Control.Monad.Trans.State as S +import qualified Cryptopals.AES as AES +import qualified Cryptopals.Digest.Pure.SHA as CS +import qualified Cryptopals.Util as CU +import Data.Binary as DB import qualified Data.Binary.Get as BG import qualified Data.Binary.Put as BP import Data.Bits ((.|.)) import qualified Data.Bits as B +import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as B8 +import qualified Data.ByteString.Base16 as B16 +import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy as BL +import qualified Data.Char as C import qualified Data.List as L +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Text.IO as TIO +import GHC.Generics (Generic) +import GHC.Word (Word16) +import qualified Network.Simple.TCP as N import Numeric.Natural +import Pipes +import qualified Pipes.Binary as PB +import qualified Pipes.Network as PN +import qualified Pipes.Prelude as P +import qualified Pipes.Parse as PP +import qualified System.Exit as SE import qualified System.Random.MWC as MWC -import GHC.Word (Word16) + +data Group = Group Natural Natural + deriving (Eq, Show, Generic) + +instance DB.Binary Group + +data Command = + SendParams Group Natural + | SendPublic Natural + | SendMessage BS.ByteString + | SendTerminal BS.ByteString + deriving (Eq, Show, Generic) + +instance DB.Binary Command + +data Keys = Keys { + pub :: Natural + , sec :: Natural + } + +-- session state +data Sesh = Sesh { + dhGroup :: Maybe Group + , dhKeys :: Maybe Keys + , dhKey :: Maybe BS.ByteString + , dhGen :: IO (MWC.Gen RealWorld) + } + +p :: Natural +p = 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff + +g :: Natural +g = 2 + +-- XX i should really put this somewhere instead of copying it every time +bytes :: PrimMonad m => Int -> MWC.Gen (PrimState m) -> m BS.ByteString +bytes n gen = fmap BS.pack $ replicateM n (MWC.uniform gen) -- modified from https://gist.github.com/trevordixon/6788535 modexp :: Natural -> Natural -> Natural -> Natural @@ -29,22 +87,161 @@ modexp b e m let t = if B.testBit e 0 then b `mod` m else 1 in t * modexp ((b * b) `mod` m) (B.shiftR e 1) m `mod` m --- little-endian natural serialization -unroll :: Natural -> BL.ByteString -unroll nat = case nat of - 0 -> BL.singleton 0 - _ -> BL.pack (L.unfoldr step nat) - where - step 0 = Nothing - step i = Just (fromIntegral i, i `B.shiftR` 8) +-- generate public, private keypair +genpair + :: PrimMonad m + => Group + -> MWC.Gen (PrimState m) + -> m Keys +genpair (Group p g) gen = do + sk <- fmap (`mod` p) (MWC.uniformRM (1, p - 1) gen) + let pk = modexp g sk p + pure $ Keys pk sk -roll :: BL.ByteString -> Natural -roll = foldr unstep 0 . BL.unpack where - unstep b a = a `B.shiftL` 8 .|. fromIntegral b +-- derive shared key from secret and other public +derivekey :: Group -> Keys -> Natural -> BS.ByteString +derivekey (Group p _) Keys {..} pk = + let nat = modexp pk sec p + in BS.take 16 . BL.toStrict . CS.bytestringDigest $ CS.sha1 (DB.encode nat) -p :: Natural -p = 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff +-- session log +slog :: T.Text -> T.Text -> IO () +slog host msg = TIO.putStrLn $ "(cryptopals) " <> host <> ": " <> msg -g :: Natural -g = 2 +-- session eval +seval :: T.Text -> Maybe Command -> StateT Sesh IO (Maybe Command) +seval host = \case + Nothing -> liftIO $ do + slog host "ending session" + SE.exitSuccess + Just cmd -> do + liftIO $ threadDelay 1000000 + dheval host cmd + +-- diffie-hellman eval +dheval + :: T.Text + -> Command + -> StateT Sesh IO (Maybe Command) +dheval host = \case + SendParams grp pk -> do + sesh@Sesh {..} <- S.get + liftIO $ slog host "received group parameters and public key" + gen <- liftIO dhGen + per@Keys {..} <- liftIO $ genpair grp gen + let key = derivekey grp per pk + nex = sesh { + dhGroup = Just grp + , dhKeys = Just per + , dhKey = Just key + } + S.put nex + liftIO $ slog host "sending public key" + pure $ Just (SendPublic pub) + + SendPublic pk -> do + sesh@Sesh {..} <- S.get + liftIO $ slog host "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" + pure Nothing + Just k -> do + gen <- liftIO dhGen + iv <- liftIO $ bytes 16 gen + let msg = CU.lpkcs7 "attack at 10pm" + cip = AES.encryptCbcAES128 iv k msg + cod = B64.encodeBase64 cip + liftIO . slog host $ "sending ciphertext " <> cod + let rep = Just (SendMessage cip) + nex = sesh { dhKey = key } + S.put nex + pure rep + + SendMessage cip -> do + sesh@Sesh {..} <- S.get + let cod = B64.encodeBase64 cip + liftIO $ slog host $ "received ciphertext " <> cod + case dhKey of + Nothing -> do + liftIO $ slog host "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 <> "\"" + + 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 $ bytes 16 gen + 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 + pure $ Just (SendTerminal ncip) + + SendTerminal cip -> do + sesh@Sesh {..} <- S.get + let cod = B64.encodeBase64 cip + liftIO $ slog host $ "received ciphertext " <> cod + case dhKey of + Nothing -> do + liftIO $ slog host "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 <> "\"" + pure Nothing + +-- await key exchange +bob :: MonadIO m => PN.ServiceName -> m a +bob port = PN.serve "localhost" port $ \(sock, _) -> do + let sesh = Sesh { + dhGroup = Nothing + , dhKeys = Nothing + , dhKey = Nothing + , dhGen = MWC.createSystemRandom + } + slog "bob" $ "listening.." + void $ S.runStateT (runEffect (handle "bob" sock)) sesh + +-- initiate key exchange +alice :: PN.ServiceName -> IO () +alice port = PN.connect "localhost" port $ \(sock, _) -> do + slog "alice" $ "session established" + + let grp = Group p g + gen <- MWC.createSystemRandom + per@Keys {..} <- genpair grp gen + slog "alice" $ "sending group parameters and public key" + runEffect $ do + PB.encode (Just (SendParams grp pub)) + >-> PN.toSocket sock + + let sesh = Sesh { + dhGroup = Just grp + , dhKeys = Just per + , dhKey = Nothing + , dhGen = pure gen + } + void $ S.runStateT (runEffect (handle "alice" sock)) sesh + +handle 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 = seval host diff --git a/lib/Pipes/Network.hs b/lib/Pipes/Network.hs @@ -0,0 +1,46 @@ +{-# LANGUAGE RankNTypes #-} + +module Pipes.Network ( + N.Socket(..) + , N.SockAddr(..) + , NT.HostPreference(..) + , N.ServiceName + + , fromSocket + , toSocket + + , NT.connect + , NT.serve + , NT.send + , NT.recv + , NT.closeSock + ) where + +import Control.Monad.IO.Class +import qualified Data.ByteString as BS +import qualified Pipes as P +import qualified Network.Simple.TCP as NT +import qualified Network.Socket as N +import qualified Network.Socket.ByteString as NB +import GHC.Word (Word32) + +fromSocket + :: MonadIO m + => N.Socket + -> Word32 + -> P.Producer' BS.ByteString m () +fromSocket s n = loop where + loop = do + b <- liftIO (NB.recv s (fromIntegral n)) + if BS.null b + then pure () + else do + P.yield b + loop + +toSocket + :: MonadIO m + => N.Socket + -> P.Consumer' BS.ByteString m r +toSocket s = P.for P.cat (NT.send s) +