aead

Pure Haskell AEAD-ChaCha20-Poly1305 (docs.ppad.tech/aead).
git clone git://git.ppad.tech/aead.git
Log | Files | Refs | README | LICENSE

ChaCha20Poly1305.hs (4689B)


      1 {-# OPTIONS_HADDOCK prune #-}
      2 {-# LANGUAGE BangPatterns #-}
      3 {-# LANGUAGE LambdaCase #-}
      4 {-# LANGUAGE OverloadedStrings #-}
      5 {-# LANGUAGE ViewPatterns #-}
      6 
      7 -- |
      8 -- Module: Crypto.AEAD.ChaCha20Poly1305
      9 -- Copyright: (c) 2025 Jared Tobin
     10 -- License: MIT
     11 -- Maintainer: Jared Tobin <jared@ppad.tech>
     12 --
     13 -- A pure AEAD-ChaCha20-Poly1305 implementation, as specified by
     14 -- [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439).
     15 
     16 module Crypto.AEAD.ChaCha20Poly1305 (
     17     -- * AEAD construction
     18     encrypt
     19   , decrypt
     20 
     21     -- testing
     22   , _poly1305_key_gen
     23   ) where
     24 
     25 import qualified Crypto.Cipher.ChaCha20 as ChaCha20
     26 import qualified Crypto.MAC.Poly1305 as Poly1305
     27 import Data.Bits ((.>>.))
     28 import qualified Data.ByteString as BS
     29 import qualified Data.ByteString.Internal as BI
     30 import Data.Word (Word64)
     31 
     32 fi :: (Integral a, Num b) => a -> b
     33 fi = fromIntegral
     34 {-# INLINE fi #-}
     35 
     36 -- little-endian bytestring encoding
     37 unroll :: Word64 -> BS.ByteString
     38 unroll i = case i of
     39     0 -> BS.singleton 0
     40     _ -> BS.unfoldr coalg i
     41   where
     42     coalg = \case
     43       0 -> Nothing
     44       m -> Just $! (fi m, m .>>. 8)
     45 {-# INLINE unroll #-}
     46 
     47 -- little-endian bytestring encoding for 64-bit ints, right-padding with zeros
     48 unroll8 :: Word64 -> BS.ByteString
     49 unroll8 (unroll -> u@(BI.PS _ _ l))
     50   | l < 8 = u <> BS.replicate (8 - l) 0
     51   | otherwise = u
     52 {-# INLINE unroll8 #-}
     53 
     54 -- RFC8439 2.6
     55 
     56 _poly1305_key_gen
     57   :: BS.ByteString -- ^ 256-bit initial keying material
     58   -> BS.ByteString -- ^ 96-bit nonce
     59   -> BS.ByteString -- ^ 256-bit key (suitable for poly1305)
     60 _poly1305_key_gen key@(BI.PS _ _ l) nonce
     61   | l /= 32   = error "ppad-aead (poly1305_key_gen): invalid key"
     62   | otherwise = BS.take 32 (ChaCha20.block key 0 nonce)
     63 {-# INLINEABLE _poly1305_key_gen #-}
     64 
     65 pad16 :: BS.ByteString -> BS.ByteString
     66 pad16 (BI.PS _ _ l)
     67   | l `rem` 16 == 0 = mempty
     68   | otherwise = BS.replicate (16 - l `rem` 16) 0
     69 {-# INLINE pad16 #-}
     70 
     71 -- RFC8439 2.8
     72 
     73 -- | Perform authenticated encryption on a plaintext and some additional
     74 --   authenticated data, given a 256-bit key and 96-bit nonce, using
     75 --   AEAD-ChaCha20-Poly1305.
     76 --
     77 --   Produces a ciphertext and 128-bit message authentication code pair.
     78 --
     79 --   Providing an invalid key or nonce will result in an 'ErrorCall'
     80 --   exception being thrown.
     81 --
     82 --   >>> let key = "don't tell anyone my secret key!"
     83 --   >>> let non = "or my nonce!"
     84 --   >>> let pan = "and here's my plaintext"
     85 --   >>> let aad = "i approve this message"
     86 --   >>> let (cip, mac) = encrypt aad key nonce pan
     87 --   >>> (cip, mac)
     88 --   <(ciphertext, 128-bit MAC)>
     89 encrypt
     90   :: BS.ByteString -- ^ arbitrary-length additional authenticated data
     91   -> BS.ByteString -- ^ 256-bit key
     92   -> BS.ByteString -- ^ 96-bit nonce
     93   -> BS.ByteString -- ^ arbitrary-length plaintext
     94   -> (BS.ByteString, BS.ByteString) -- ^ (ciphertext, 128-bit MAC)
     95 encrypt aad key nonce plaintext
     96   | BS.length key  /= 32  = error "ppad-aead (encrypt): invalid key"
     97   | BS.length nonce /= 12 = error "ppad-aead (encrypt): invalid nonce"
     98   | otherwise =
     99       let otk = _poly1305_key_gen key nonce
    100           cip = ChaCha20.cipher key 1 nonce plaintext
    101           md0 = aad <> pad16 aad
    102           md1 = md0 <> cip <> pad16 cip
    103           md2 = md1 <> unroll8 (fi (BS.length aad))
    104           md3 = md2 <> unroll8 (fi (BS.length cip))
    105           tag = Poly1305.mac otk md3
    106       in  (cip, tag)
    107 
    108 -- | Decrypt an authenticated ciphertext, given a message authentication
    109 --   code and some additional authenticated data, via a 256-bit key and
    110 --   96-bit nonce.
    111 --
    112 --   Returns 'Nothing' if the MAC fails to validate.
    113 --
    114 --   Providing an invalid key or nonce will result in an 'ErrorCall'
    115 --   exception being thrown.
    116 --
    117 --   >>> decrypt aad key non (cip, mac)
    118 --   Just "and here's my plaintext"
    119 --   >>> decrypt aad key non (cip, "it's a valid mac")
    120 --   Nothing
    121 decrypt
    122   :: BS.ByteString                  -- ^ arbitrary-length AAD
    123   -> BS.ByteString                  -- ^ 256-bit key
    124   -> BS.ByteString                  -- ^ 96-bit nonce
    125   -> (BS.ByteString, BS.ByteString) -- ^ (arbitrary-length ciphertext, 128-bit MAC)
    126   -> Maybe BS.ByteString
    127 decrypt aad key nonce (cip, mac)
    128   | BS.length key /= 32   = error "ppad-aead (decrypt): invalid key"
    129   | BS.length nonce /= 12 = error "ppad-aead (decrypt): invalid nonce"
    130   | BS.length mac /= 16   = Nothing
    131   | otherwise =
    132       let otk = _poly1305_key_gen key nonce
    133           md0 = aad <> pad16 aad
    134           md1 = md0 <> cip <> pad16 cip
    135           md2 = md1 <> unroll8 (fi (BS.length aad))
    136           md3 = md2 <> unroll8 (fi (BS.length cip))
    137           tag = Poly1305.mac otk md3
    138       in  if   mac == tag
    139           then pure (ChaCha20.cipher key 1 nonce cip)
    140           else Nothing
    141