packages feed

ppad-poly1305-0.4.5: lib/Crypto/MAC/Poly1305.hs

{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE UnboxedTuples #-}

-- |
-- Module: Crypto.MAC.Poly1305
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- A pure Poly1305 MAC implementation, as specified by
-- [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439).

module Crypto.MAC.Poly1305 (
    -- * Poly1305 message authentication code
    MAC(..)
  , mac

    -- testing
  , _poly1305_loop
  , _roll16
  ) where

import qualified Crypto.MAC.Poly1305.Arm as Arm
import Data.Barrier (barrier)
import qualified Data.Bits as B
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BI
import qualified Data.ByteString.Unsafe as BU
import Data.Word (Word8)
import Data.Word.Limb (Limb(..))
import qualified Data.Word.Limb as L
import Data.Word.Wider (Wider(..))
import qualified Data.Word.Wider as W
import qualified Foreign.Storable as Storable (pokeByteOff)
import qualified GHC.Exts as Exts
import qualified GHC.Word (Word8(..))

-- utilities ------------------------------------------------------------------

-- convert a Word8 to a Limb
limb :: Word8 -> Limb
limb (GHC.Word.W8# (Exts.word8ToWord# -> w)) = Limb w
{-# INLINABLE limb #-}

-- convert a Limb to a Word8
word8 :: Limb -> Word8
word8 (Limb w) = GHC.Word.W8# (Exts.wordToWord8# w)
{-# INLINABLE word8 #-}

-- convert a Limb to a Word8 after right-shifting
word8s :: Limb -> Exts.Int# -> Word8
word8s l s =
  let !(Limb w) = L.shr# l s
  in  GHC.Word.W8# (Exts.wordToWord8# w)
{-# INLINABLE word8s #-}

-- 128-bit little-endian bytestring decoding
_roll16 :: BS.ByteString -> Wider
_roll16 bs@(BI.PS _ _ l) =
  let byte :: Int -> Limb
      byte i
        | i < l     = limb (BU.unsafeIndex bs i)
        | otherwise = Limb 0##
      {-# INLINE byte #-}
      !w0 =     (byte 07 `L.shl#` 56#)
        `L.or#` (byte 06 `L.shl#` 48#)
        `L.or#` (byte 05 `L.shl#` 40#)
        `L.or#` (byte 04 `L.shl#` 32#)
        `L.or#` (byte 03 `L.shl#` 24#)
        `L.or#` (byte 02 `L.shl#` 16#)
        `L.or#` (byte 01 `L.shl#` 08#)
        `L.or#` byte 00
      !w1 =     (byte 15 `L.shl#` 56#)
        `L.or#` (byte 14 `L.shl#` 48#)
        `L.or#` (byte 13 `L.shl#` 40#)
        `L.or#` (byte 12 `L.shl#` 32#)
        `L.or#` (byte 11 `L.shl#` 24#)
        `L.or#` (byte 10 `L.shl#` 16#)
        `L.or#` (byte 09 `L.shl#` 08#)
        `L.or#` byte 08
  in  Wider (# w0, w1, Limb 0##, Limb 0## #)
{-# INLINE _roll16 #-}

-- 128-bit little-endian bytestring encoding
unroll16 :: Wider -> BS.ByteString
unroll16 (Wider (# w0, w1, _, _ #)) =
  BI.unsafeCreate 16 $ \ptr -> do
    -- w0
    Storable.pokeByteOff ptr 00 (word8 w0)
    Storable.pokeByteOff ptr 01 (word8s w0 08#)
    Storable.pokeByteOff ptr 02 (word8s w0 16#)
    Storable.pokeByteOff ptr 03 (word8s w0 24#)
    Storable.pokeByteOff ptr 04 (word8s w0 32#)
    Storable.pokeByteOff ptr 05 (word8s w0 40#)
    Storable.pokeByteOff ptr 06 (word8s w0 48#)
    Storable.pokeByteOff ptr 07 (word8s w0 56#)
    -- w1
    Storable.pokeByteOff ptr 08 (word8 w1)
    Storable.pokeByteOff ptr 09 (word8s w1 08#)
    Storable.pokeByteOff ptr 10 (word8s w1 16#)
    Storable.pokeByteOff ptr 11 (word8s w1 24#)
    Storable.pokeByteOff ptr 12 (word8s w1 32#)
    Storable.pokeByteOff ptr 13 (word8s w1 40#)
    Storable.pokeByteOff ptr 14 (word8s w1 48#)
    Storable.pokeByteOff ptr 15 (word8s w1 56#)
{-# INLINABLE unroll16 #-}

-- set high bit for chunk of length l (max 16)
set_hi :: Int -> Wider
set_hi l
  | l < 8     = W.shl_limb 1 (8 * l)
  | l < 16    = Wider (# Limb 0##, L.shl# (Limb 1##) s, Limb 0##, Limb 0## #)
  | otherwise = Wider (# Limb 0##, Limb 0##, Limb 1##, Limb 0## #)
  where
    !(Exts.I# s) = 8 * (l - 8)
{-# INLINE set_hi #-}

-- bespoke constant-time 130-bit right shift
shr130 :: Wider -> Wider
shr130 (Wider (# _, _, l2, l3 #)) =
  let !r0 = L.or# (L.shr# l2 2#) (L.shl# l3 62#)
      !r1 = L.shr# l3 2#
  in  Wider (# r0, r1, Limb 0##, Limb 0## #)
{-# INLINE shr130 #-}

-------------------------------------------------------------------------------

clamp :: Wider -> Wider
clamp r = r `W.and` 0x0ffffffc0ffffffc0ffffffc0fffffff
{-# INLINE clamp #-}

-- | A Poly1305 message authentication code.
--
--   Note that you should compare MACs for equality using the 'Eq'
--   instance, which performs the comparison in constant time, instead
--   of unwrapping and comparing the underlying 'ByteStrings'.
--
--   >>> let Just foo@(MAC bs0) = mac key "hi"
--   >>> let Just bar@(MAC bs1) = mac key "there"
--   >>> foo == bar -- do this
--   False
--   >>> bs0 == bs1 -- don't do this
--   False
newtype MAC = MAC BS.ByteString
  deriving newtype Show

instance Eq MAC where
  -- | A constant-time equality check for message authentication codes.
  --
  --   Runs in variable-time only for invalid inputs.
  (MAC a@(BI.PS _ _ la)) == (MAC b@(BI.PS _ _ lb))
      | la /= lb  = False
      | otherwise = go 0 0
    where
      -- fused fold: OR the bytewise XORs into an accumulator
      -- directly, rather than via packZipWith, so no intermediate
      -- ByteString holding the (secret-derived) difference bytes
      -- is ever materialised on the heap. The accumulator is routed
      -- through 'barrier' before the zero-test so the LLVM backend
      -- cannot recover the array-equality idiom and short-circuit on
      -- the first mismatch (see "Data.Barrier").
      go :: Word8 -> Int -> Bool
      go !acc !i
        | i == la   = barrier acc == 0
        | otherwise =
            let !x = BU.unsafeIndex a i
                !y = BU.unsafeIndex b i
            in  go (acc B..|. B.xor x y) (i + 1)

-- | Produce a Poly1305 MAC for the provided message, given the
--   provided key.
--
--   Per RFC8439: the key, which is essentially a /one-time/ key,
--   should be unique, and MUST be unpredictable for each invocation.
--
--   The key must be exactly 256 bits in length.
--
--   >>> mac "i'll never use this key again!!!" "a message needing authentication"
--   Just "O'\231Z\224\149\148\246\203[}\210\203\b\200\207"
mac
  :: BS.ByteString -- ^ 256-bit one-time key
  -> BS.ByteString -- ^ arbitrary-length message
  -> Maybe MAC     -- ^ 128-bit message authentication code
mac key@(BI.PS _ _ kl) msg
  | kl /= 32  = Nothing
  | Arm.poly1305_arm_available =
      pure $! MAC (Arm.mac key msg)
  | otherwise =
      let (clamp . _roll16 -> r, _roll16 -> s) = BS.splitAt 16 key
      in  pure $! (MAC (_poly1305_loop r s msg))

-- p = 2^130 - 5
--
-- mask for the low 130 bits
mask130 :: Wider
mask130 = 0x3ffffffffffffffffffffffffffffffff
{-# INLINE mask130 #-}

-- partial reduction to [0, 2 ^ 131)
reduce_partial :: Wider -> Wider
reduce_partial x =
  let !lo = x `W.and` mask130
      !hi = shr130 x
  in  lo + 5 * hi
{-# INLINE reduce_partial #-}

-- [0, 2 ^ 131) -> [0, p)
reduce_full :: Wider -> Wider
reduce_full h =
  let !lo = h `W.and` mask130
      !hi  = shr130 h
      !h'  = lo + 5 * hi
      !h_5 = h' + 5
      !reduced = h_5 `W.and` mask130
      !carry   = shr130 h_5
      !gte     = W.lt 0 carry
  in  W.select h' reduced gte
{-# INLINE reduce_full #-}

_poly1305_loop :: Wider -> Wider -> BS.ByteString -> BS.ByteString
_poly1305_loop !r !s !msg =
    let loop !acc !bs = case BS.splitAt 16 bs of
          (chunk@(BI.PS _ _ l), etc)
            | l == 0 ->
                let !final = reduce_full (reduce_partial acc)
                in  unroll16 (final + s)
            | otherwise ->
                let !n = _roll16 chunk `W.or` set_hi l
                    !prod = r * (acc + n)
                    !nacc = reduce_partial (reduce_partial prod)
                in  loop nacc etc
    in  loop 0 msg
{-# INLINE _poly1305_loop #-}