ppad-chacha-0.2.2: lib/Crypto/Cipher/ChaCha20.hs
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE UnboxedTuples #-}
-- |
-- Module: Crypto.Cipher.ChaCha20
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- A fast ChaCha20 implementation, as specified by
-- [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439).
module Crypto.Cipher.ChaCha20 (
-- * ChaCha20 stream cipher
cipher
-- * ChaCha20 block function
, block
-- * Error information
, Error(..)
-- testing
, ChaCha(..)
, _chacha
, _parse_key
, _parse_nonce
, _quarter
, _quarter_pure
, _rounds
) where
import Control.Monad.ST
import qualified Crypto.Cipher.ChaCha20.Arm as Arm
import qualified Data.Bits as B
import Data.Bits ((.|.), (.<<.), (.^.))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Internal as BI
import qualified Data.ByteString.Unsafe as BU
import Control.Monad.Primitive (PrimMonad, PrimState)
import Data.Foldable (for_)
import qualified Data.Primitive.PrimArray as PA
import Foreign.ForeignPtr
import GHC.Exts
import GHC.Word
-- utils ----------------------------------------------------------------------
-- keystroke saver
fi :: (Integral a, Num b) => a -> b
fi = fromIntegral
{-# INLINE fi #-}
-- parse strict ByteString in LE order to Word32 (verbatim from
-- Data.Binary)
unsafe_word32le :: BS.ByteString -> Word32
unsafe_word32le s =
(fi (s `BU.unsafeIndex` 3) `B.unsafeShiftL` 24) .|.
(fi (s `BU.unsafeIndex` 2) `B.unsafeShiftL` 16) .|.
(fi (s `BU.unsafeIndex` 1) `B.unsafeShiftL` 8) .|.
(fi (s `BU.unsafeIndex` 0))
{-# INLINE unsafe_word32le #-}
data WSPair = WSPair
{-# UNPACK #-} !Word32
{-# UNPACK #-} !BS.ByteString
-- variant of Data.ByteString.splitAt that behaves like an incremental
-- Word32 parser
unsafe_parseWsPair :: BS.ByteString -> WSPair
unsafe_parseWsPair (BI.BS x l) =
WSPair (unsafe_word32le (BI.BS x 4)) (BI.BS (plusForeignPtr x 4) (l - 4))
{-# INLINE unsafe_parseWsPair #-}
-- chacha quarter round -------------------------------------------------------
-- RFC8439 2.2
_quarter
:: PrimMonad m
=> ChaCha (PrimState m)
-> Int
-> Int
-> Int
-> Int
-> m ()
_quarter (ChaCha m) i0 i1 i2 i3 = do
!(W32# a) <- PA.readPrimArray m i0
!(W32# b) <- PA.readPrimArray m i1
!(W32# c) <- PA.readPrimArray m i2
!(W32# d) <- PA.readPrimArray m i3
let !(# a1, b1, c1, d1 #) = quarter# a b c d
PA.writePrimArray m i0 (W32# a1)
PA.writePrimArray m i1 (W32# b1)
PA.writePrimArray m i2 (W32# c1)
PA.writePrimArray m i3 (W32# d1)
{-# INLINEABLE _quarter #-}
_quarter_pure
:: Word32 -> Word32 -> Word32 -> Word32 -> (Word32, Word32, Word32, Word32)
_quarter_pure (W32# a) (W32# b) (W32# c) (W32# d) =
let !(# a', b', c', d' #) = quarter# a b c d
in (W32# a', W32# b', W32# c', W32# d')
{-# INLINE _quarter_pure #-}
-- RFC8439 2.1
quarter#
:: Word32# -> Word32# -> Word32# -> Word32#
-> (# Word32#, Word32#, Word32#, Word32# #)
quarter# a b c d =
let a0 = plusWord32# a b
d0 = xorWord32# d a0
d1 = rotateL# d0 16#
c0 = plusWord32# c d1
b0 = xorWord32# b c0
b1 = rotateL# b0 12#
a1 = plusWord32# a0 b1
d2 = xorWord32# d1 a1
d3 = rotateL# d2 8#
c1 = plusWord32# c0 d3
b2 = xorWord32# b1 c1
b3 = rotateL# b2 7#
in (# a1, b3, c1, d3 #)
{-# INLINE quarter# #-}
rotateL# :: Word32# -> Int# -> Word32#
rotateL# w i
| isTrue# (i ==# 0#) = w
| otherwise = wordToWord32# (
((word32ToWord# w) `uncheckedShiftL#` i)
`or#` ((word32ToWord# w) `uncheckedShiftRL#` (32# -# i)))
{-# INLINE rotateL# #-}
-- key and nonce parsing ------------------------------------------------------
data Key = Key {
k0 :: {-# UNPACK #-} !Word32
, k1 :: {-# UNPACK #-} !Word32
, k2 :: {-# UNPACK #-} !Word32
, k3 :: {-# UNPACK #-} !Word32
, k4 :: {-# UNPACK #-} !Word32
, k5 :: {-# UNPACK #-} !Word32
, k6 :: {-# UNPACK #-} !Word32
, k7 :: {-# UNPACK #-} !Word32
}
deriving Show
-- parse strict 256-bit bytestring (length unchecked) to key
_parse_key :: BS.ByteString -> Key
_parse_key bs =
let !(WSPair k0 t0) = unsafe_parseWsPair bs
!(WSPair k1 t1) = unsafe_parseWsPair t0
!(WSPair k2 t2) = unsafe_parseWsPair t1
!(WSPair k3 t3) = unsafe_parseWsPair t2
!(WSPair k4 t4) = unsafe_parseWsPair t3
!(WSPair k5 t5) = unsafe_parseWsPair t4
!(WSPair k6 t6) = unsafe_parseWsPair t5
!(WSPair k7 t7) = unsafe_parseWsPair t6
in if BS.null t7
then Key {..}
else error "ppad-chacha (_parse_key): internal error, bytes remaining"
data Nonce = Nonce {
n0 :: {-# UNPACK #-} !Word32
, n1 :: {-# UNPACK #-} !Word32
, n2 :: {-# UNPACK #-} !Word32
}
deriving Show
-- parse strict 96-bit bytestring (length unchecked) to nonce
_parse_nonce :: BS.ByteString -> Nonce
_parse_nonce bs =
let !(WSPair n0 t0) = unsafe_parseWsPair bs
!(WSPair n1 t1) = unsafe_parseWsPair t0
!(WSPair n2 t2) = unsafe_parseWsPair t1
in if BS.null t2
then Nonce {..}
else error "ppad-chacha (_parse_nonce): internal error, bytes remaining"
-- chacha20 block function ----------------------------------------------------
newtype ChaCha s = ChaCha (PA.MutablePrimArray s Word32)
deriving Eq
_chacha
:: PrimMonad m
=> Key
-> Word32
-> Nonce
-> m (ChaCha (PrimState m))
_chacha key counter nonce = do
state <- _chacha_alloc
_chacha_set state key counter nonce
pure state
-- allocate a new chacha state
_chacha_alloc :: PrimMonad m => m (ChaCha (PrimState m))
_chacha_alloc = fmap ChaCha (PA.newPrimArray 16)
{-# INLINE _chacha_alloc #-}
-- set the values of a chacha state
_chacha_set
:: PrimMonad m
=> ChaCha (PrimState m)
-> Key
-> Word32
-> Nonce
-> m ()
_chacha_set (ChaCha arr) Key {..} counter Nonce {..}= do
PA.writePrimArray arr 00 0x61707865
PA.writePrimArray arr 01 0x3320646e
PA.writePrimArray arr 02 0x79622d32
PA.writePrimArray arr 03 0x6b206574
PA.writePrimArray arr 04 k0
PA.writePrimArray arr 05 k1
PA.writePrimArray arr 06 k2
PA.writePrimArray arr 07 k3
PA.writePrimArray arr 08 k4
PA.writePrimArray arr 09 k5
PA.writePrimArray arr 10 k6
PA.writePrimArray arr 11 k7
PA.writePrimArray arr 12 counter
PA.writePrimArray arr 13 n0
PA.writePrimArray arr 14 n1
PA.writePrimArray arr 15 n2
{-# INLINEABLE _chacha_set #-}
_chacha_counter
:: PrimMonad m
=> ChaCha (PrimState m)
-> Word32
-> m ()
_chacha_counter (ChaCha arr) counter =
PA.writePrimArray arr 12 counter
-- two full rounds (eight quarter rounds)
_rounds :: PrimMonad m => ChaCha (PrimState m) -> m ()
_rounds state = do
_quarter state 00 04 08 12
_quarter state 01 05 09 13
_quarter state 02 06 10 14
_quarter state 03 07 11 15
_quarter state 00 05 10 15
_quarter state 01 06 11 12
_quarter state 02 07 08 13
_quarter state 03 04 09 14
{-# INLINEABLE _rounds #-}
_block
:: PrimMonad m
=> ChaCha (PrimState m)
-> Word32
-> m BS.ByteString
_block state@(ChaCha s) counter = do
_chacha_counter state counter
i <- PA.freezePrimArray s 0 16
for_ [1..10 :: Int] (const (_rounds state))
for_ [0..15 :: Int] $ \idx -> do
let iv = PA.indexPrimArray i idx
sv <- PA.readPrimArray s idx
PA.writePrimArray s idx (iv + sv)
serialize state
-- | Error values.
data Error =
InvalidKey -- ^ the provided key was not 256 bits long
| InvalidNonce -- ^ the provided nonce was none 96 bits long
deriving (Eq, Show)
-- RFC8439 2.3
-- | The ChaCha20 block function. Useful for generating a keystream.
--
-- Per [RFC8439](https://datatracker.ietf.org/doc/html/rfc8439), the
-- key must be exactly 256 bits, and the nonce exactly 96 bits.
block
:: BS.ByteString -- ^ 256-bit key
-> Word32 -- ^ 32-bit counter
-> BS.ByteString -- ^ 96-bit nonce
-> Either Error BS.ByteString -- ^ 512-bit keystream
block key@(BI.PS _ _ kl) counter nonce@(BI.PS _ _ nl)
| kl /= 32 = Left InvalidKey
| nl /= 12 = Left InvalidNonce
| Arm.chacha20_arm_available =
Right (Arm.block key counter nonce)
| otherwise = pure $ runST $ do
let k = _parse_key key
n = _parse_nonce nonce
state@(ChaCha s) <- _chacha k counter n
i <- PA.freezePrimArray s 0 16
for_ [1..10 :: Int] (const (_rounds state))
for_ [0..15 :: Int] $ \idx -> do
let iv = PA.indexPrimArray i idx
sv <- PA.readPrimArray s idx
PA.writePrimArray s idx (iv + sv)
serialize state
serialize :: PrimMonad m => ChaCha (PrimState m) -> m BS.ByteString
serialize (ChaCha m) = do
w64_0 <- w64 <$> PA.readPrimArray m 00 <*> PA.readPrimArray m 01
w64_1 <- w64 <$> PA.readPrimArray m 02 <*> PA.readPrimArray m 03
w64_2 <- w64 <$> PA.readPrimArray m 04 <*> PA.readPrimArray m 05
w64_3 <- w64 <$> PA.readPrimArray m 06 <*> PA.readPrimArray m 07
w64_4 <- w64 <$> PA.readPrimArray m 08 <*> PA.readPrimArray m 09
w64_5 <- w64 <$> PA.readPrimArray m 10 <*> PA.readPrimArray m 11
w64_6 <- w64 <$> PA.readPrimArray m 12 <*> PA.readPrimArray m 13
w64_7 <- w64 <$> PA.readPrimArray m 14 <*> PA.readPrimArray m 15
pure . BS.toStrict . BSB.toLazyByteString . mconcat $
[w64_0, w64_1, w64_2, w64_3, w64_4, w64_5, w64_6, w64_7]
where
w64 a b = BSB.word64LE (fi a .|. (fi b .<<. 32))
-- chacha20 encryption --------------------------------------------------------
-- RFC8439 2.4
-- | The ChaCha20 stream cipher. Generates a keystream and then XOR's
-- the supplied input with it; use it both to encrypt plaintext and
-- decrypt ciphertext.
--
-- Per [RFC8439](https://datatracker.ietf.org/doc/html/rfc8439), the
-- key must be exactly 256 bits, and the nonce exactly 96 bits.
--
-- >>> let key = "don't tell anyone my secret key!"
-- >>> let non = "or my nonce!"
-- >>> let cip = cipher key 1 non "but you can share the plaintext"
-- >>> cip
-- "\192*c\248A\204\211n\130y8\197\146k\245\178Y\197=\180_\223\138\146:^\206\&0\v[\201"
-- >>> cipher key 1 non cip
-- Right "but you can share the plaintext"
cipher
:: BS.ByteString -- ^ 256-bit key
-> Word32 -- ^ 32-bit counter
-> BS.ByteString -- ^ 96-bit nonce
-> BS.ByteString -- ^ arbitrary-length plaintext
-> Either Error BS.ByteString -- ^ ciphertext
cipher raw_key@(BI.PS _ _ kl) counter raw_nonce@(BI.PS _ _ nl) plaintext
| kl /= 32 = Left InvalidKey
| nl /= 12 = Left InvalidNonce
| Arm.chacha20_arm_available =
Right (Arm.cipher raw_key counter raw_nonce plaintext)
| otherwise = pure $ runST $ do
let key = _parse_key raw_key
non = _parse_nonce raw_nonce
_cipher key counter non plaintext
_cipher
:: PrimMonad m
=> Key
-> Word32
-> Nonce
-> BS.ByteString
-> m BS.ByteString
_cipher key counter nonce plaintext = do
ChaCha initial <- _chacha key counter nonce
state@(ChaCha s) <- _chacha_alloc
let loop acc !j bs = case BS.splitAt 64 bs of
(chunk@(BI.PS _ _ l), etc@(BI.PS _ _ le))
| l == 0 && le == 0 -> pure $
BS.toStrict (BSB.toLazyByteString acc)
| otherwise -> do
PA.copyMutablePrimArray s 0 initial 0 16
stream <- _block state j
let cip = BS.packZipWith (.^.) chunk stream
loop (acc <> BSB.byteString cip) (j + 1) etc
loop mempty counter plaintext
{-# INLINE _cipher #-}