-- |
-- Module      : Crypto.KDF.Argon2
-- License     : BSD-style
-- Maintainer  : Vincent Hanquez <vincent@snarc.org>
-- Stability   : experimental
-- Portability : unknown
--
-- Argon2 hashing function (P-H-C winner)
--
-- Recommended to use this module qualified
--
-- File started from Argon2.hs, from Oliver Charles
-- at https://github.com/ocharles/argon2
module Crypto.KDF.Argon2 (
    Options (..),
    TimeCost,
    MemoryCost,
    Parallelism,
    Variant (..),
    Version (..),
    defaultOptions,

    -- * Hashing function
    hash,
) where

import Control.Monad (when)
import Crypto.Error
import Crypto.Internal.ByteArray (ByteArray, ByteArrayAccess)
import qualified Crypto.Internal.ByteArray as B
import Data.Word
import Foreign.C
import Foreign.Ptr

-- | Which variant of Argon2 to use. You should choose the variant that is most
-- applicable to your intention to hash inputs.
data Variant
    = -- | Argon2d is faster than Argon2i and uses data-depending memory access,
      -- which makes it suitable for cryptocurrencies and applications with no
      -- threats from side-channel timing attacks.
      Argon2d
    | -- | Argon2i uses data-independent memory access, which is preferred
      -- for password hashing and password-based key derivation. Argon2i
      -- is slower as it makes more passes over the memory to protect from
      -- tradeoff attacks.
      Argon2i
    | -- | Argon2id is a hybrid of Argon2i and Argon2d, using a combination
      -- of data-depending and data-independent memory accesses, which gives
      -- some of Argon2i's resistance to side-channel cache timing attacks
      -- and much of Argon2d's resistance to GPU cracking attacks
      Argon2id
    deriving (Eq, Ord, Read, Show, Enum, Bounded)

-- | Which version of Argon2 to use
data Version = Version10 | Version13
    deriving (Eq, Ord, Read, Show, Enum, Bounded)

-- | The time cost, which defines the amount of computation realized and therefore the execution time, given in number of iterations.
--
-- 'FFI.ARGON2_MIN_TIME' <= 'hashIterations' <= 'FFI.ARGON2_MAX_TIME'
type TimeCost = Word32

-- | The memory cost, which defines the memory usage, given in kibibytes.
--
-- max 'FFI.ARGON2_MIN_MEMORY' (8 * 'hashParallelism') <= 'hashMemory' <= 'FFI.ARGON2_MAX_MEMORY'
type MemoryCost = Word32

-- | A parallelism degree, which defines the number of parallel threads.
--
-- 'FFI.ARGON2_MIN_LANES' <= 'hashParallelism' <= 'FFI.ARGON2_MAX_LANES' && 'FFI.ARGON_MIN_THREADS' <= 'hashParallelism' <= 'FFI.ARGON2_MAX_THREADS'
type Parallelism = Word32

-- | Parameters that can be adjusted to change the runtime performance of the
-- hashing.
data Options = Options
    { iterations :: !TimeCost
    , memory :: !MemoryCost
    , parallelism :: !Parallelism
    , variant :: !Variant
    -- ^ Which variant of Argon2 to use.
    , version :: !Version
    -- ^ Which version of Argon2 to use.
    }
    deriving (Eq, Ord, Read, Show)

saltMinLength :: Int
saltMinLength = 8

outputMinLength :: Int
outputMinLength = 4

-- specification allows up to 2^32-1 but this is too big for a signed Int
-- on a 32-bit architecture, so we limit tag length to 2^31-1 bytes
outputMaxLength :: Int
outputMaxLength = 0x7fffffff

defaultOptions :: Options
defaultOptions =
    Options
        { iterations = 1
        , memory = 2 ^ (17 :: Int)
        , parallelism = 4
        , variant = Argon2i
        , version = Version13
        }

hash
    :: (ByteArrayAccess password, ByteArrayAccess salt, ByteArray out)
    => Options
    -> password
    -> salt
    -> Int
    -> CryptoFailable out
hash options password salt outLen
    | saltLen < saltMinLength = CryptoFailed CryptoError_SaltTooSmall
    | outLen < outputMinLength = CryptoFailed CryptoError_OutputLengthTooSmall
    | outLen > outputMaxLength = CryptoFailed CryptoError_OutputLengthTooBig
    | otherwise = CryptoPassed $ B.allocAndFreeze outLen $ \out -> do
        res <- B.withByteArray password $ \pPass ->
            B.withByteArray salt $ \pSalt ->
                argon2_hash
                    (iterations options)
                    (memory options)
                    (parallelism options)
                    pPass
                    (csizeOfInt passwordLen)
                    pSalt
                    (csizeOfInt saltLen)
                    out
                    (csizeOfInt outLen)
                    (cOfVariant $ variant options)
                    (cOfVersion $ version options)
        when (res /= 0) $ error "argon2: hash: internal error"
  where
    saltLen = B.length salt
    passwordLen = B.length password

data Pass
data Salt
data HashOut

type CVariant = CInt -- valid value is 0 (Argon2d), 1 (Argon2i) and 2 (Argon2id)
type CVersion = CInt -- valid value is 0x10, 0x13

cOfVersion :: Version -> CVersion
cOfVersion Version10 = 0x10
cOfVersion Version13 = 0x13

cOfVariant :: Variant -> CVariant
cOfVariant Argon2d = 0
cOfVariant Argon2i = 1
cOfVariant Argon2id = 2

csizeOfInt :: Int -> CSize
csizeOfInt = fromIntegral

foreign import ccall unsafe "crypton_argon2_hash"
    argon2_hash
        :: Word32
        -> Word32
        -> Word32
        -> Ptr Pass
        -> CSize
        -> Ptr Salt
        -> CSize
        -> Ptr HashOut
        -> CSize
        -> CVariant
        -> CVersion
        -> IO CInt
