-- |
-- 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           Crypto.Internal.ByteArray (ScrubbedBytes, ByteArray, ByteArrayAccess)
import qualified Crypto.Internal.ByteArray as B
import           Crypto.Error
import           Control.Monad (when)
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  -- ^ 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 -- ^ Argon2d is faster and uses data-depending memory access, which
              -- makes it suitable for cryptocurrencies and applications with no
              -- threats from side-channel timing attacks.
    | Argon2id -- ^ 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
    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

outputMaxLength :: Int
outputMaxLength = 0xffffffff

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 "cryptonite_argon2_hash"
    argon2_hash :: Word32 -> Word32 -> Word32
                -> Ptr Pass -> CSize
                -> Ptr Salt -> CSize
                -> Ptr HashOut -> CSize
                -> CVariant
                -> CVersion
                -> IO CInt