cryptopals

Matasano's cryptopals challenges (cryptopals.com).
git clone git://git.jtobin.io/cryptopals.git
Log | Files | Refs | README | LICENSE

s5.md (19453B)


      1 #### 5.33
      2 
      3 The basic Diffie-Hellman algorithm for key exchange between Alice and
      4 Bob goes as follows. Alice and Bob agree on a finite cyclic group of
      5 some particular order to use. They each randomly and independently pick
      6 some number of times to perform the group operation (which must be
      7 greater than zero, and less than the order of the group) and perform the
      8 group operation on the generator that number of times, publishing their
      9 results. For generator g, Alice's number x, and Bob's number y: Alice
     10 publishes g ^ x, and Bob publishes g ^ y.
     11 
     12 Each then performs his or her own secret number of additional group
     13 operations on the other's public result, establishing the key g ^ xy.
     14 Since the discrete logarithm problem is hard, an eavesdropper can't (for
     15 an appropriate group) determine how many times either has performed the
     16 individual group operations, even given g; he can only naïvely calculate
     17 g ^ (x + y).
     18 
     19 For the initial illustration here we're using a decidedly insecure
     20 group, i.e. the multiplicative group of 16-bit words modulo 37:
     21 
     22     > gen <- MWC.create
     23     > let p = 37
     24     > let g = 5
     25     > a <- fmap (`mod` p) (MWC.uniformR (1, p - 1) gen) :: IO Word16
     26     > b <- fmap (`mod` p) (MWC.uniformR (1, p - 1) gen) :: IO Word16
     27     > let bigA = g ^ a `mod` p
     28     > let bigB = g ^ b `mod` p
     29     > let s = bigB ^ a `mod` p
     30     > let t = bigA ^ b `mod` p
     31     > s == t
     32     True
     33     > let k = S.sha1 . BP.runPut $ BP.putWord16be s
     34     > k
     35     ace6f761db204030c1a65c0930bd01fd55ecc429
     36 
     37 For the big guns, we can pull out GHC's Natural type, for which
     38 mwc-random (our preferred random library) can generate something in a
     39 range. First a modular exponentiation routine:
     40 
     41     -- modified from https://gist.github.com/trevordixon/6788535
     42     modexp :: Natural -> Natural -> Natural -> Natural
     43     modexp b e m
     44       | e == 0    = 1
     45       | otherwise =
     46           let t = if B.testBit e 0 then b `mod` m else 1
     47           in  t * modexp ((b * b) `mod` m) (B.shiftR e 1) m `mod` m
     48 
     49 and given that (and appropriate p, g), the key exchange:
     50 
     51     > gen <- MWC.create
     52     > a <- fmap (`mod` p) (MWC.uniformRM (1, p - 1) gen)
     53     > b <- fmap (`mod` p) (MWC.uniformRM (1, p - 1) gen)
     54     > let bigA = modexp g a p
     55     > let bigB = modexp g b p
     56     > let s = modexp bigB a p
     57     > let t = modexp bigA b p
     58     > s == t
     59     True
     60 
     61 That's all well and good, but let's have a bit of fun.
     62 
     63 Cryptopals.DH implements the Diffie-Hellman protocol over TCP. Two
     64 functions, 'bob' and 'alice', will initiate a TCP server and client,
     65 respectively. Each takes as argument a port to bind to and a protocol to
     66 follow, with Alice also taking an argument specifying the initial action
     67 she'll take. The two will then perform the specified protocol.
     68 
     69 The 'dh' protocol specifies Diffie-Hellman, and for the lulz Alice and
     70 Bob will exchange a message, AES128-encrypted with the shared key, à la
     71 the initial illustration in the next challenge.
     72 
     73 Opening two instances of GHCi, we can run e.g.:
     74 
     75     > bob "3000" dh
     76 
     77 in one, and:
     78 
     79     > alice "3000" dh
     80 
     81 in the other and watch the logs for fun. Here I'll interleave the
     82 relevant parts of the logs for readability:
     83 
     84     (cryptopals) bob: listening..
     85     (cryptopals) alice: session established
     86     (cryptopals) alice: sending group parameters and public key
     87     (cryptopals) bob: received group parameters and public key
     88     (cryptopals) bob: sending public key
     89     (cryptopals) alice: received public key
     90     (cryptopals) alice: sending ciphertext fQwcd2smXxBojyEDLrIdySNLAV3UHhl8/X2F/x4FIb0=
     91     (cryptopals) bob: received ciphertext fQwcd2smXxBojyEDLrIdySNLAV3UHhl8/X2F/x4FIb0=
     92     (cryptopals) bob: decrypted ciphertext: "attack at 10pm"
     93     (cryptopals) bob: replying with ciphertext NaObgnz4rNShMKRGdGv+OGnI2gZnWOoXuYmCZhlcyymRbHPaprFVOz4Ls5eH9y/W
     94     (cryptopals) alice: received ciphertext NaObgnz4rNShMKRGdGv+OGnI2gZnWOoXuYmCZhlcyymRbHPaprFVOz4Ls5eH9y/W
     95     (cryptopals) alice: decrypted ciphertext: "confirmed, attacking at 10pm"
     96     (cryptopals) bob: ending session
     97 
     98 #### 5.34
     99 
    100 If B = p in s = B ^ a mod p, then s = p ^ a mod p, which is zero for any
    101 'a' in the group. Our shared key is thus going to be the first 16 bytes
    102 of the SHA1 hash of an appropriately-serialized 0x00.
    103 
    104 Cryptopals.DH includes a 'mallory' agent that requires a port to listen
    105 on, a port to bind to, and a protocol to follow. By using the 'dhmitm'
    106 protocol, we get our man-in-the-middle attack on Alice and Bob's DH key
    107 exchange.
    108 
    109 You can get this going by opening three GHCi's, then launching e.g.:
    110 
    111     > bob "3000" dh
    112 
    113 in one, then:
    114 
    115     > mallory "3001" "3000" dhmitm
    116 
    117 in another, and finally:
    118 
    119     > alice "3001" dh sendParams
    120 
    121 in the third. Again, I'm interleaving the logs for readability:
    122 
    123     (cryptopals) bob: listening..
    124     (cryptopals) mallory: LiSteNIng..
    125     (cryptopals) alice: session established
    126     (cryptopals) mallory: eStabLisHed coNNecTion
    127     (cryptopals) alice: sending group parameters and public key
    128     (cryptopals) mallory: reCEiVed GRoUp pArAmeTErs And pUBliC kEy
    129     (cryptopals) mallory: sEnDinG BOguS paRaMeTeRs
    130     (cryptopals) bob: received group parameters and public key
    131     (cryptopals) bob: sending public key
    132     (cryptopals) mallory: REceIvED pUBlic keY
    133     (cryptopals) mallory: seNDINg boGus kEy
    134     (cryptopals) alice: received public key
    135     (cryptopals) alice: sending ciphertext tM4Y5fpafrsf9+A4UB5UaudkAVzwMtjsjDIwShPKHcU=
    136     (cryptopals) mallory: rECeIveD CiPHeRTexT tM4Y5fpafrsf9+A4UB5UaudkAVzwMtjsjDIwShPKHcU=
    137     (cryptopals) mallory: DEcRyptEd cIPheRTeXt: "attack at 10pm"
    138     (cryptopals) mallory: reLayINg cIpheRtExt
    139     (cryptopals) bob: received ciphertext tM4Y5fpafrsf9+A4UB5UaudkAVzwMtjsjDIwShPKHcU=
    140     (cryptopals) bob: decrypted ciphertext: "attack at 10pm"
    141     (cryptopals) bob: replying with ciphertext ux4PoPTCS7pz5H4IQ11AuZkMBHmEcT9Waz68y/a9nggIY38Z6mbwSrCwNO3OKcDQ
    142     (cryptopals) mallory: reCeiVeD CipHeRtExt ux4PoPTCS7pz5H4IQ11AuZkMBHmEcT9Waz68y/a9nggIY38Z6mbwSrCwNO3OKcDQ
    143     (cryptopals) mallory: DeCrYpteD cIphErteXt: "confirmed, attacking at 10pm"
    144     (cryptopals) mallory: ReLaYINg CiPHeRTexT
    145     (cryptopals) alice: received ciphertext ux4PoPTCS7pz5H4IQ11AuZkMBHmEcT9Waz68y/a9nggIY38Z6mbwSrCwNO3OKcDQ
    146     (cryptopals) alice: decrypted ciphertext: "confirmed, attacking at 10pm"
    147 
    148 #### 5.35
    149 
    150 Cryptopals.DH.dhng implements the negotiated-groups DH protocol, so that
    151 can be run by firing off e.g.:
    152 
    153     > bob "3000" dhng
    154 
    155 in one GHCi session, and:
    156 
    157     > alice "3000" dhng sendGroup
    158 
    159 in the other. In the meantime, we can figure out the outcomes of using
    160 the different malicious group parameters analytically.
    161 
    162 For g = 1, the MITM attack starts as follows:
    163 
    164     alice sends      p, g
    165     bob gets         p, 1
    166 
    167 If Bob receives g = 1, then Mallory knows his public key will equal 1 ^
    168 b mod p = 1, as will the shared key from Alice's perspective (since 1 ^
    169 a mod p = 1). Mallory thus needs to forward a 1 as Alice's public key in
    170 order for Bob to agree on the shared key.
    171 
    172 For g = p, Bob computes B = p ^ b mod p = 0, so Mallory can forward p as
    173 Alice's public key in order for them to agree on the shared key.
    174 
    175 Finally, the case of g = p - 1. Note that for any p > 1 and any even b, we
    176 have (for appropriate coefficients a, c, etc.):
    177 
    178     (p - 1) ^ b mod p
    179       = (p^b + .. + ap^2 + cp + 1) mod p
    180       = (p^b mod p + .. + ap^2 mod p + cp mod p + 1 mod p) mod p
    181       = (0 + .. + 0 + 1 mod p) mod p
    182       = 1
    183 
    184 whereas for any odd b, we have:
    185 
    186     (p - 1) ^ b mod p
    187       = (p^b - .. - cp^2 + dp - 1) mod p
    188       = (p^b mod p - .. - ap^2 mod p + cp mod p - 1 mod p) mod p
    189       = (0 + .. + 0 - 1 mod p) mod p
    190       = p - 1.
    191 
    192 So Bob's public key will be either 1 or p - 1 depending on whether his
    193 secret key is even or odd. Alice will thus compute:
    194 
    195     s = B ^ a mod p
    196       = 1               } b even or a even  (p = 3/4)
    197         p - 1           } b odd and a odd.  (p = 1/4).
    198 
    199 If Mallory then forwards A = g = p - 1 to Bob, we have:
    200 
    201     t = A ^ b mod p
    202       = 1               } b even  (p = 1/2)
    203       = p - 1           } b odd   (p = 1/2).
    204 
    205 This all yields the following table:
    206 
    207                             a
    208                 even              odd
    209         even   s = 1, t = 1      s = 1, t = 1
    210     b
    211         odd    s = 1, t = p - 1  s = p - 1, t = p - 1
    212 
    213 such that Alice and Bob will agree on the shared key with probability
    214 3/4. Mallory also has to choose which shared key value he uses; if he
    215 uses 1, then the attack succeeds with probability 1/2, and if he uses
    216 p - 1, then it succeeds with probability 1/4.
    217 
    218 Here are the interleaved logs of a successful attack. Start mallory with
    219 e.g. the `dhngmitm 1`, `dhngmitm p`, or `dhngmitm (p - 1)` protocol:
    220 
    221     (cryptopals) bob: listening..
    222     (cryptopals) mallory: LiSteNIng..
    223     (cryptopals) alice: session established
    224     (cryptopals) mallory: eStabLisHed MiTm coNNecTion
    225     (cryptopals) alice: sending group parameters
    226     (cryptopals) mallory: reCEiVed GRoUp pArAmeTErs
    227     (cryptopals) mallory: sEnDinG BOguS GRoUp paRaMeTeRs
    228     (cryptopals) bob: received group parameters
    229     (cryptopals) bob: acking group parameters
    230     (cryptopals) mallory: rECeiVed aCK
    231     (cryptopals) mallory: ReLaYINg ACk
    232     (cryptopals) alice: received ack
    233     (cryptopals) alice: sending public key 3f44f49421cbb3b2ed40aa8f068236affba15335
    234     (cryptopals) mallory: REceIvED pUBlic keY 3f44f49421cbb3b2ed40aa8f068236affba15335
    235     (cryptopals) mallory: SeNDing BoGuS kEy d14952314d5de233ef0dd0a178617f7f07ea082c
    236     (cryptopals) bob: received public key d14952314d5de233ef0dd0a178617f7f07ea082c
    237     (cryptopals) bob: sending public key
    238     (cryptopals) mallory: REceIvED pUBlic keY d14952314d5de233ef0dd0a178617f7f07ea082c
    239     (cryptopals) mallory: ReLAyINg pUbliC KeY d14952314d5de233ef0dd0a178617f7f07ea082c
    240     (cryptopals) alice: received public key d14952314d5de233ef0dd0a178617f7f07ea082c
    241     (cryptopals) alice: sending ciphertext +nbU0t3nLX3WmKoY3+pdmilVcd2I6fJfGuC3RTn0h5E=
    242     (cryptopals) mallory: rECeIveD CiPHeRTexT +nbU0t3nLX3WmKoY3+pdmilVcd2I6fJfGuC3RTn0h5E=
    243     (cryptopals) mallory: DEcRyptEd cIPheRTeXt: "attack at 10pm"
    244     (cryptopals) mallory: reLayINg cIpheRtExt
    245     (cryptopals) bob: received ciphertext +nbU0t3nLX3WmKoY3+pdmilVcd2I6fJfGuC3RTn0h5E=
    246     (cryptopals) bob: decrypted ciphertext: "attack at 10pm"
    247     (cryptopals) bob: replying with ciphertext 3i7fLAZXJv7+cr3qrI8KDKhfe6FpJq62yVtaCt9dlrUodMiRVtJ7ZmKtJ8ku0r4x
    248     (cryptopals) mallory: reCeiVeD CipHeRtExt 3i7fLAZXJv7+cr3qrI8KDKhfe6FpJq62yVtaCt9dlrUodMiRVtJ7ZmKtJ8ku0r4x
    249     (cryptopals) mallory: DeCrYpteD cIphErteXt: "confirmed, attacking at 10pm"
    250     (cryptopals) mallory: ReLaYINg CiPHeRTexT
    251     (cryptopals) alice: received ciphertext 3i7fLAZXJv7+cr3qrI8KDKhfe6FpJq62yVtaCt9dlrUodMiRVtJ7ZmKtJ8ku0r4x
    252     (cryptopals) alice: decrypted ciphertext: "confirmed, attacking at 10pm"
    253     (cryptopals) mallory: ending session
    254 
    255 #### 5.36
    256 
    257 SRP (Secure Remote Password) is an authentication protocol for which
    258 a client authenticates with a server via a zero-knowledge proof.
    259 Cryptopals.SRP implements it much in the same way that Cryptopals.DH
    260 implements Diffie-Hellman; here one can perform the protocol via the
    261 'server' and 'client' functions analogously.
    262 
    263 Some interleaved logs for 'server "3000" srp' and 'client "3000" srp
    264 auth':
    265 
    266     (cryptopals) server: listening..
    267     (cryptopals) client: session established
    268     (cryptopals) client: sending authentication request
    269     (cryptopals) server: received authentication request for l33th4x0r@hotmail.com
    270     (cryptopals) server: acking authentication request for l33th4x0r@hotmail.com
    271     (cryptopals) client: received authentication request ack
    272     (cryptopals) client: sending MAC 6p7eE/pTSijdReePtswOKDZZUFYhLkJfeKps0GD4Yc4=
    273     (cryptopals) server: received MAC 6p7eE/pTSijdReePtswOKDZZUFYhLkJfeKps0GD4Yc4=
    274     (cryptopals) server: OK
    275 
    276 #### 5.37
    277 
    278 If the client forwards A = 0 (or anything congruent modulo N to 0) as
    279 its public key, then the server will compute S = 0 as its shared secret.
    280 Whoops! The client can then just pass along the appropriate MAC to
    281 authenticate.
    282 
    283 Example, with the client using the 'srpzero' protocol and 'authZero'
    284 initial action:
    285 
    286     -- GHCi instance one
    287     > server "3000" srp
    288     -- GHCi instance two
    289     > client "3000" srpZero authZero
    290     (cryptopals) server: listening..
    291     (cryptopals) client: session established
    292     (cryptopals) client: sending authentication request with a zero key
    293     (cryptopals) server: received authentication request for l33th4x0r@hotmail.com
    294     (cryptopals) server: acking authentication request for l33th4x0r@hotmail.com
    295     (cryptopals) client: received authentication request ack
    296     (cryptopals) client: sending MAC 5xO9hEUJOTX5EIU+DmYV0QOs1L1oVp3fphREooN/8L4=
    297     (cryptopals) server: received MAC 5xO9hEUJOTX5EIU+DmYV0QOs1L1oVp3fphREooN/8L4=
    298     (cryptopals) server: OK
    299 
    300 #### 5.38
    301 
    302 The simplified protocol can be run with the 'server' and 'client'
    303 functions in Cryptopals.SRP.Simple.
    304 
    305 For the MITM attack, the idea is that, posing as the server, Mallory has
    306 control over the parameters 'salt', 'b', 'B', and 'u', but doesn't know
    307 anything to do with 'x', and so has to guess at that.
    308 
    309 If Mallory supplies salt = mempty, B = g mod n, and u = 1, then the
    310 client will compute:
    311 
    312     S = g ^ (a + x) mod n
    313 
    314 and forward him MAC = HMAC-SHA256(SHA256(S), mempty). Duly supplied with
    315 the client's public key ('A') and MAC, and using a trivial b = 1 as a
    316 secret key, Mallory can guess x = SHA256(password) to compute:
    317 
    318     S' = (A v) mod n
    319        = (A g ^ x) mod n
    320 
    321 and then check if HMAC-SHA256(SHA256(S'), mempty) = MAC. If it verifies,
    322 then he knows the password.
    323 
    324 To not make this too annoying, I'll draw the password to be cracked from
    325 /usr/share/dict/words. Once Mallory provides the public key and MAC
    326 from the client, we'll generate our dictionary and check if the MAC is
    327 present in the keyspace using a compiled, optimized binary.
    328 
    329 Here's a run of the MITM protocol (`mallory "3000" mitm` and `client
    330 "3000" srpsimple auth`):
    331 
    332     (cryptopals) mallory: LiSteNiNG..
    333     (cryptopals) client: session established
    334     (cryptopals) client: sending authentication request
    335     (cryptopals) mallory: rECeIvEd aUTheNtICaTioN ReQUesT fOr l33th4x0r@hotmail.com
    336     (cryptopals) mallory: wiTh PuBLiC kEy 4992116105881074929461308645820763003777270799868975573291
    337     (cryptopals) mallory: aCKiNg AuTheNTicAtIon ReQueST FOr l33th4x0r@hotmail.com
    338     (cryptopals) client: received authentication request ack
    339     (cryptopals) client: sending MAC f20ac41224b4054d2f89a7c319ed5bf3f8bb68cf4169f620f45e49acb4dd179c
    340     (cryptopals) mallory: rECeIvEd MAC f20ac41224b4054d2f89a7c319ed5bf3f8bb68cf4169f620f45e49acb4dd179c
    341     (cryptopals) mallory: USiNg PaRaMeTeRs 4992116105881074929461308645820763003777270799868975573291 aNd f20ac41224b4054d2f89a7c319ed5bf3f8bb68cf4169f620f45e49acb4dd179c
    342     (cryptopals) mallory: GoINg ofFLinE..
    343 
    344 Now taking those parameters to the `offline-dictionary-attack` binary we get
    345 the result pretty quickly:
    346 
    347     $ PK="4992116105881074929461308645820763003777270799868975573291"
    348     $ MAC="f20ac41224b4054d2f89a7c319ed5bf3f8bb68cf4169f620f45e49acb4dd179c"
    349     $ offline-dictionary-attack "$PK" "$MAC"
    350     (cryptopals) success
    351     (cryptopals) password: omniana
    352 
    353 #### 5.39
    354 
    355 A note on primegen for RSA: I didn't bother with it, as recommended, but
    356 looked at how it should be done. It seems straightforward; one generates
    357 a sufficiently large random number, then tests that it isn't divisible
    358 by the first several hundred primes, then performs a probabilistic
    359 primality test sufficiently many times that the error probability is
    360 very small. A reference suggested that the error probability should be
    361 less than 1 / 2^128.
    362 
    363 I used cryptonite's Crypto.Number.Prime module, which implements the
    364 above procedure.
    365 
    366 In any case, RSA: one finds two k-bit primes, 'p' and 'q', and uses
    367 their product to construct a public modulus n = pq and value
    368 `t = (p - 1) (q - 1)`. The public key is (n, e) for 'e' a number
    369 relatively prime to 't', and the private key is (n, d), for d such that
    370 `ed = 1 mod t` (i.e., 'd' is congruent mod 't' to the inverse of 'e').
    371 "Relatively prime" or "coprime" means, for two numbers 'a' and 'b', that
    372 they have a greatest common denominator of 1.
    373 
    374 Encryption and decryption are then just modular exponentiation
    375 operations using the keys. To go from Natural to ByteString and back,
    376 I used some old functions -- roll and unroll -- that I wrote for
    377 [urbit-hob](http://git.jtobin.io/urbit-hob) a few years back (though
    378 actually I think I cribbed them from the Data.Binary internals):
    379 
    380     data Key = Key Natural Natural
    381       deriving (Eq, Show)
    382 
    383     data Keypair = Keypair {
    384         sec :: Key
    385       , pub :: Key
    386       } deriving (Eq, Show)
    387 
    388     keygen :: Int -> IO Keypair
    389     keygen siz = loop where
    390       loop = do
    391         p <- fromIntegral <$> P.generatePrime siz
    392         q <- fromIntegral <$> P.generatePrime siz
    393         let n   = p * q
    394             et  = pred p * pred q
    395             e   = 3
    396             md  = modinv e et
    397         case md of
    398           Nothing -> loop
    399           Just d  -> pure $ Keypair (Key d n) (Key e n)
    400 
    401     encrypt :: Key -> BS.ByteString -> BS.ByteString
    402     encrypt (Key e n) m = unroll (DH.modexp (roll m) e n)
    403 
    404     decrypt :: Key -> BS.ByteString -> BS.ByteString
    405     decrypt = encrypt
    406 
    407 Works fine:
    408 
    409     > Keypair sec pub <- keygen 1024
    410     > let cip = encrypt pub "secret!"
    411     > TIO.putStrLn $ T.take 32 $ B64.encodeBase64 cip
    412     zZFjaw7BR6rP0EaNsTFRnCInwsghANrE
    413     > let msg = decrypt sec cip
    414     > msg
    415     "secret!"
    416 
    417 The Cryptopals.RSA module exports the keygen, encrypt, and decrypt
    418 functions, as well as modinv and roll & unroll.
    419 
    420 #### 5.40
    421 
    422 The problem behind the Chinese Remainder Theorem, as formulated by one
    423 孫子 (not *that* 孫子) and passed on to me via Wikipedia, is:
    424 
    425 > There are certain things whose number is unknown. If we count them by
    426 > threes, we have two left over; by fives, we have three left over; and
    427 > by sevens, two are left over. How many things are there?
    428 
    429 I.e. what is x, if x is congruent to 2 mod 3, 3 mod 5, and 2 mod 7?
    430 
    431 In Sunzi's case the answer is congruent to 23 mod 105, and the Chinese
    432 Remainder Theorem states that such a solution always exists given
    433 certain conditions (notably that the moduli are pairwise coprime).
    434 
    435 In our case, for plaintext 'm' and ciphertexts c0, c1, and c2, we have
    436 that m ^ 3 is congruent to c0 mod n0, c1 mod n1, and c2 mod n2. The
    437 Chinese Remainder Theorem asserts that there exists some 'c' that is
    438 congruent to m ^ 3 modulo `n0 * n1 * n2`. The trick here (this is known
    439 as Håstad's attack, apparently) is that since m is smaller than every n,
    440 we have that m ^ 3 is smaller than `n0 * n1 * n2`, and so that 'c' is
    441 precisely equal to m ^ 3, rather than congruent to it.
    442 
    443 So, following the CRT construction:
    444 
    445     > let msg = "attack at 10pm"
    446     >
    447     > Keypair _ p0@(Key e0 n0) <- keygen 1024
    448     > Keypair _ p1@(Key e1 n1) <- keygen 1024
    449     > Keypair _ p2@(Key e2 n2) <- keygen 1024
    450     >
    451     > let c0 = encrypt p0 msg
    452     > let c1 = encrypt p1 msg
    453     > let c2 = encrypt p2 msg
    454     >
    455     > let ms0 = n1 * n2
    456     > let ms1 = n0 * n2
    457     > let ms2 = n0 * n1
    458     >
    459     > :{
    460     > let res = (roll c0 * ms0 * modinv' ms0 n0
    461     >          + roll c1 * ms1 * modinv' ms1 n1
    462     >          + roll c2 * ms2 * modinv' ms2 n2)
    463     >       `mod`
    464     >           (n0 * n1 * n2)
    465     > :}
    466 
    467 The 'integer-roots' package provides a helpful cube root function:
    468 
    469     > unroll $ R.integerCubeRoot res
    470     "attack at 10pm"
    471