packages feed

hashes-0.3.0: src/Data/Hash/SipHash.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE UndecidableInstances #-}

-- |
-- Module: Data.Hash.SipHash
-- Copyright: Copyright © 2021-2024 Lars Kuhtz <lakuhtz@gmail.com>
-- License: MIT
-- Maintainer: Lars Kuhtz <lakuhtz@gmail.com>
-- Stability: experimental
--
module Data.Hash.SipHash
( SipHashKey(..)
, SipHash(..)
, sipHash

-- * SipHash-c-d
, sipHashCD
, sipHash24
, sipHash13
, sipHash48

-- * Incremental SipHash
, SipHashContext
, sipHashInitialize
, sipHashUpdate
, sipHashFinalize

-- * Utils
, module Data.Hash.Class.Pure.Salted
) where

import Control.Monad

import Data.Bits
import Data.Type.Equality
import Data.Word

import Foreign.Marshal
import Foreign.Ptr
import Foreign.Storable

import GHC.TypeNats

-- internal modules

import Data.Hash.Class.Pure.Salted

-- -------------------------------------------------------------------------- --
-- SipHash

-- | SipHash, with recommended default parameters of c=2 and d=4.
--
-- The first and second argument is the 128 bit key, represented as two 64 bit
-- words.
--
sipHash
    :: SipHashKey
    -> Ptr Word8
    -> Int
    -> IO (SipHash 2 4)
sipHash = sipHashCD
{-# INLINE sipHash #-}

-- | Generic SipHash with c rounds per block and d finalization rounds.
--
-- The first and second argument is the 128 bit key, represented as two 64 bit
-- words.
--
sipHashCD
    :: forall c d
    . SipHashParam c
    => SipHashParam d
    => SipHashKey
    -> Ptr Word8
    -> Int
    -> IO (SipHash c d)
sipHashCD key ptr n = sipHashFinalize
    <$> sipHashUpdate (sipHashInitialize key) ptr n
{-# INLINE sipHashCD #-}

-- | SipHash-2-4
--
-- The first and second argument is the 128 bit key, represented as two 64 bit
-- words.
--
sipHash24 :: SipHashKey -> Ptr Word8 -> Int -> IO (SipHash 2 4)
sipHash24 = sipHashCD
{-# INLINE sipHash24 #-}

-- | SipHash-1-3
--
-- The first and second argument is the 128 bit key, represented as two 64 bit
-- words.
--
sipHash13 :: SipHashKey -> Ptr Word8 -> Int -> IO (SipHash 1 3)
sipHash13 = sipHashCD
{-# INLINE sipHash13 #-}

-- | SipHash-4-8
--
-- The first and second argument is the 128 bit key, represented as two 64 bit
-- words.
--
sipHash48 :: SipHashKey -> Ptr Word8 -> Int -> IO (SipHash 4 8)
sipHash48 = sipHashCD
{-# INLINE sipHash48 #-}

-- -------------------------------------------------------------------------- --
-- Class

instance (SipHashParam c, SipHashParam d) => IncrementalHash (SipHash c d) where
    type Context (SipHash c d) = SipHashContext c d
    update = sipHashUpdate
    finalize = sipHashFinalize

    {-# INLINE update #-}
    {-# INLINE finalize #-}

instance (SipHashParam c, SipHashParam d) => Hash (SipHash c d) where
    type Salt (SipHash c d) = SipHashKey
    initialize = sipHashInitialize
    {-# INLINE initialize #-}

-- -------------------------------------------------------------------------- --
-- Incremental Version of SipHash

-- | SipHash with @c@ compression rounds and @d@ finalization rounds.
--
-- cf. http://cr.yp.to/siphash/siphash-20120918.pdf
--
newtype SipHash (c :: Nat) (d :: Nat) = SipHash Word64
    deriving (Show, Eq, Ord)

-- | The 'Word46' constructor parameters represent the 128 bit key in little
-- endian encoding.
--
data SipHashKey = SipHashKey {-# UNPACK #-} !Word64 {-# UNPACK #-} !Word64
    deriving (Show, Eq, Ord)

-- | Internal mutable SipHashContext.
--
-- The first four arguments are the internal state values \(v_{0..3}\) and the
-- last argument represents the pending bytes from an incomplete word of the
-- last chunk of input.
--
data SipHashContext (c :: Nat) (d :: Nat) = SipHashContext
    {-# UNPACK #-} !Word64
    {-# UNPACK #-} !Word64
    {-# UNPACK #-} !Word64
    {-# UNPACK #-} !Word64
    {-# UNPACK #-} !Word64
        -- ^ the most significant byte keeps track of the total number of input
        -- bytes modulo 256. The remaining bytes are the currently pending input
        -- bytes (i.e. the last \(totalInput `mod` 8\) many bytes of the input).

-- | Initialize a new SipHashContext
--
sipHashInitialize :: SipHashKey -> SipHashContext c d
sipHashInitialize (SipHashKey k0 k1) = SipHashContext
    (0x736f6d6570736575 `xor` k0)
    (0x646f72616e646f6d `xor` k1)
    (0x6c7967656e657261 `xor` k0)
    (0x7465646279746573 `xor` k1)
    0x0
{-# INLINE sipHashInitialize #-}

-- | Incrementally add input bytes to an SipHash computation and update
-- the internal context.
--
sipHashUpdate
    :: forall (c :: Nat) (d :: Nat)
    . SipHashParam c
    => SipHashContext c d
    -> Ptr Word8
    -> Int
    -> IO (SipHashContext c d)
sipHashUpdate (SipHashContext s0 s1 s2 s3 r) ptr8 len
    | 0 <- rlen `rem` 8 = loop s0 s1 s2 s3 ptr64 len64

    -- Consume the first input word using any possibly pending input bytes from
    -- previous updates.
    --
    | a <- rlen `rem` 8 = do
        let !missing = 8 - a

        -- get enough bytes to fill up next word (if there are less than 8 - a
        -- bytes the most significant bytes are set to 0)
        !m <- ptrToWord64 ptr64 $ fromIntegral missing

        -- add new bytes to get full word64. Input is parsed as little endian,
        -- so new bytes are more significant than pending bytes.
        let !m' = (0x00ffffffffffffff .&. r {- pending bytes -}) .|. m

        if len64 < missing
          then
            -- nothing left to do
            return $ SipHashContext s0 s1 s2 s3 (shiftL (rlen + len64) 56 .|. m')
          else do
            -- compute c round with first word
            let (# v0', v1', v2', v3' #) = rounds @c s0 s1 s2 (s3 `xor` m')
            loop (v0' `xor` m') v1' v2' v3' (plusPtr ptr64 (fromIntegral missing)) (len64 - missing)
  where
    len64 = fromIntegral len
    {-# INLINE len64 #-}

    !ptr64 = castPtr ptr8
    {-# INLINE ptr64 #-}

    !rlen = 0xff00000000000000 .&. r
    {-# INLINE rlen #-}


    -- Assumes that there are no pending bytes.
    loop !v0 !v1 !v2 !v3 !p !l
        | l < 8 = do
            !m <- ptrToWord64 p l
            return $ SipHashContext v0 v1 v2 v3 (shiftL (rlen + len64) 56 .|. m)
        | otherwise = do
            -- TODO enforce little endian encoding
            !m <- peek p
            let (# v0', v1', v2', v3' #) = rounds @c v0 v1 v2 (v3 `xor` m)
            loop (v0' `xor` m) v1' v2' v3' (plusPtr p 8) (l - 8)
    {-# INLINE loop #-}
{-# INLINE sipHashUpdate #-}

sipHashFinalize
    :: forall (c :: Nat) (d :: Nat)
    . SipHashParam c
    => SipHashParam d
    => SipHashContext c d
    -> SipHash c d
sipHashFinalize (SipHashContext v0 v1 v2 v3 m) =
    SipHash $! v0'' `xor` v1'' `xor` v2'' `xor` v3''
  where
    (# !v0', !v1', !v2', !v3' #) = rounds @c v0 v1 v2 (v3 `xor` m)
    (# !v0'', !v1'', !v2'', !v3'' #) = rounds @d (v0' `xor` m) v1' (v2' `xor` 0xff) v3'
{-# INLINE sipHashFinalize #-}

ptrToWord64 :: Ptr Word64 -> Word64 -> IO Word64
ptrToWord64 _ 0 = pure 0
ptrToWord64 !p 1 = fromIntegral <$!> peek @Word8 (castPtr p)
ptrToWord64 !p 2 = fromIntegral <$!> peek @Word16 (castPtr p)
ptrToWord64 !p 4 = fromIntegral <$!> peek @Word32 (castPtr p)
ptrToWord64 !p !i = with @Word64 0 $ \p' -> do
        -- using 'with' within unsafeDupablePerformIO is probably safe because
        -- with uses 'alloca', which guarantees that the memory is released
        -- when computation is abondended before being terminated.
    copyBytes p' p (fromIntegral i)
    peek p'
{-# INLINE ptrToWord64 #-}

class SipHashParam (n :: Nat) where
    rounds :: Word64 -> Word64 -> Word64 -> Word64 -> (# Word64, Word64, Word64, Word64 #)

instance SipHashRounds n (SlowRounds n) => SipHashParam (n :: Nat) where
    rounds = rounds_ @n @(SlowRounds n)
    {-# INLINE rounds #-}

-- -------------------------------------------------------------------------- --
-- SipHash Rounds

-- Decide wether to pick an fast specialized routes implementation or a somewhat
-- less efficient generic implementation.
--
type SlowRounds r = CmpNat r 8 == 'GT

-- TODO: create benchmark to check how well inlining works for recursive type class function calls,
-- It's possibly, that we don't need all these specializations but inlining gets the job done all by
-- itself.

class SipHashRounds (n :: Nat) (x :: Bool) where
    rounds_ :: Word64 -> Word64 -> Word64 -> Word64 -> (# Word64, Word64, Word64, Word64 #)

instance SipHashRounds 1 'False where
    rounds_ !v0 !v1 !v2 !v3 = sipRound v0 v1 v2 v3
    {-# INLINE rounds_ #-}

instance SipHashRounds 2 'False where
    rounds_ !v0 !v1 !v2 !v3 =
        let (# !v0', !v1', !v2', !v3' #) = sipRound v0 v1 v2 v3
        in sipRound v0' v1' v2' v3'
    {-# INLINE rounds_ #-}

instance SipHashRounds 3 'False where
    rounds_ !v0 !v1 !v2 !v3 =
        let (# !v0', !v1', !v2', !v3' #) = sipRound v0 v1 v2 v3
            (# !v0'', !v1'', !v2'', !v3'' #) = sipRound v0' v1' v2' v3'
        in sipRound v0'' v1'' v2'' v3''
    {-# INLINE rounds_ #-}

instance SipHashRounds 4 'False where
    rounds_ !v0 !v1 !v2 !v3 =
        let (# !v0', !v1', !v2', !v3' #) = sipRound v0 v1 v2 v3
            (# !v0'', !v1'', !v2'', !v3'' #) = sipRound v0' v1' v2' v3'
            (# !v0''', !v1''', !v2''', !v3''' #) = sipRound v0'' v1'' v2'' v3''
        in sipRound v0''' v1''' v2''' v3'''
    {-# INLINE rounds_ #-}

instance SipHashRounds 5 'False where
    rounds_ !v0 !v1 !v2 !v3 = case rounds_ @4 @'False v0 v1 v2 v3 of
        (# v0', v1', v2', v3' #) -> rounds_ @1 @'False v0' v1' v2' v3'
    {-# INLINE rounds_ #-}

instance SipHashRounds 6 'False where
    rounds_ !v0 !v1 !v2 !v3 = case rounds_ @4 @'False v0 v1 v2 v3 of
        (# v0', v1', v2', v3' #) -> rounds_ @2 @'False v0' v1' v2' v3'
    {-# INLINE rounds_ #-}

instance SipHashRounds 7 'False where
    rounds_ !v0 !v1 !v2 !v3 = case rounds_ @4 @'False v0 v1 v2 v3 of
        (# v0', v1', v2', v3' #) -> rounds_ @3 @'False v0' v1' v2' v3'
    {-# INLINE rounds_ #-}

instance SipHashRounds 8 'False where
    rounds_ !v0 !v1 !v2 !v3 =
        let (# !v0', !v1', !v2', !v3' #) = sipRound v0 v1 v2 v3
            (# !v0'', !v1'', !v2'', !v3'' #) = sipRound v0' v1' v2' v3'
            (# !v0''', !v1''', !v2''', !v3''' #) = sipRound v0'' v1'' v2'' v3''
            (# !v0'''', !v1'''', !v2'''', !v3'''' #) = sipRound v0''' v1''' v2''' v3'''
            (# !v0''''', !v1''''', !v2''''', !v3''''' #) = sipRound v0'''' v1'''' v2'''' v3''''
            (# !v0'''''', !v1'''''', !v2'''''', !v3'''''' #) = sipRound v0''''' v1''''' v2''''' v3'''''
            (# !v0''''''', !v1''''''', !v2''''''', !v3''''''' #) = sipRound v0'''''' v1'''''' v2'''''' v3''''''
        in sipRound v0''''''' v1''''''' v2''''''' v3'''''''
    {-# INLINE rounds_ #-}

instance ((CmpNat n 8 == 'GT) ~ 'True, SipHashRounds (n-8) t) => SipHashRounds n 'True where
    rounds_ !v0 !v1 !v2 !v3 = case rounds_ @8 @'False v0 v1 v2 v3 of
        (# v0', v1', v2', v3' #) -> rounds_ @(n - 8) @t v0' v1' v2' v3'
    {-# INLINE rounds_ #-}

sipRound :: Word64 -> Word64 -> Word64 -> Word64 -> (# Word64, Word64, Word64, Word64 #)
sipRound !v0 !v1 !v2 !v3 = (# v0''', v1'''', v2''', v3'''' #)
  where
    !v0' = v0 + v1
    !v2' = v2 + v3
    !v1' = v1 `rotateL` 13
    !v3' = v3 `rotateL` 16
    !v1'' = v1' `xor` v0'
    !v3'' = v3' `xor` v2'
    !v0'' = v0' `rotateL` 32
    !v2'' = v2' + v1''
    !v0''' = v0'' + v3''
    !v1''' = v1'' `rotateL` 17
    !v3''' = v3'' `rotateL` 21
    !v1'''' = v1''' `xor` v2''
    !v3'''' = v3''' `xor` v0'''
    !v2''' = v2'' `rotateL` 32
{-# INLINE sipRound #-}