{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE OverloadedStrings #-}

{- | A module for hashing passwords with bcrypt.

     >>> import Crypto.BCrypt
     >>> let p = Data.ByteString.Char8.pack
     >>> hashPasswordUsingPolicy slowerBcryptHashingPolicy (p "mypassword")
     Just "$2y$14$xBBZdWgTa8fSU1aPFP5IxeVdUKfT7hUDjmusZEAiNBiYaYEGY/Sh6"
     >>> validatePassword (p "$2y$14$xBBZdWgTa8fSU1aPFP5IxeVdUKfT7hUDjmusZEAiNBiYaYEGY/Sh6") (p "badpass")
     False
     >>> validatePassword (p "$2y$14$xBBZdWgTa8fSU1aPFP5IxeVdUKfT7hUDjmusZEAiNBiYaYEGY/Sh6") (p "mypassword")
     True
     >>> hashUsesPolicy slowerBcryptHashingPolicy (p "$2y$14$xBBZdWgTa8fSU1aPFP5IxeVdUKfT7hUDjmusZEAiNBiYaYEGY/Sh6")
     True
     >>> hashUsesPolicy fastBcryptHashingPolicy (p "$2y$14$xBBZdWgTa8fSU1aPFP5IxeVdUKfT7hUDjmusZEAiNBiYaYEGY/Sh6")
     False
 -}
module Crypto.BCrypt (HashingPolicy(..), hashPasswordUsingPolicy, validatePassword,
                      fastBcryptHashingPolicy, slowerBcryptHashingPolicy,
                      hashUsesPolicy, hashPassword, genSalt, genSaltUsingPolicy,
                      defaultHashAlgorithm)
where

import Foreign
import Foreign.C.String
import Foreign.C.Types
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Unsafe as BS
import qualified System.IO.Unsafe as U
import Control.Monad
import Data.ByteArray (constEq)
import Data.Default
import System.Entropy

foreign import ccall "crypt_ra" c_crypt_ra :: CString -> CString -> Ptr CString -> Ptr CInt -> IO CString
foreign import ccall "crypt_gensalt_ra" c_crypt_gensalt_ra :: CString -> CULong -> CString -> CInt -> IO CString

-- | A hashing policy defines the type of password hashing to use.
data HashingPolicy = HashingPolicy {
    -- | Preferred cost - how strong new passwords should be. This is a trade-off
    --   between making hasing / checking passwords faster in your system, and making
    --   brute forcing passwords harder for an adversary.
    --   The intention is that this can be increased as computers get faster.
    --   To give a rough indication of the scale of preferredCost,
    --     on a 2.6 GHz AMD Athlon machine (64 bit kernel), using a single core:
    --
    --       * Cost 4: 139 passwords / second
    --
    --       * Cost 5: 85 passwords / second
    --
    --       * Cost 6: 44 passwords / second
    --
    --       * Cost 7: 23 passwords / second
    --
    --       * Cost 8: 11 passwords / second
    --
    --       * Cost 9: 5.7 passwords / second
    --
    --       * Cost 10: 2.8 passwords / second
    --
    --       * Cost 11: 1.4 passwords / second
    --
    --       * Cost 12: 0.72 passwords / second
    preferredHashCost :: Int,
    -- | Preferred algorithm - the preferred hash algorithm.
    --   The default is $2y$ (compatible with other Openwall-based
    --   libraries). The most up-to-date version is $2b$.
    preferredHashAlgorithm :: BS.ByteString
  } deriving (Show, Eq)

instance Default HashingPolicy where
  -- Slower hashing seems like a decent default.
  def = slowerBcryptHashingPolicy

-- | Default is compatible with other Openwall-based libraries.
defaultHashAlgorithm :: BS.ByteString
defaultHashAlgorithm = "$2y$"

-- | Hashes a password, using a hashing policy.
hashPasswordUsingPolicy :: HashingPolicy -> BS.ByteString -> IO (Maybe BS.ByteString)
hashPasswordUsingPolicy hp pw = do
  ms <- genSaltUsingPolicy hp
  return $ do
    s <- ms
    hashPassword pw s

-- | Validates a password. The first argument is the hashed password, the second is
--   the password attempt.
--   Note: If a password validates successfully, it is a good idea to check if the
--   password is up to the current policy using hashUsesPolicy, and re-hashing it
--   if not.
validatePassword :: BS.ByteString -> BS.ByteString -> Bool
validatePassword h pw =
  case hashPassword pw h
    of
      Nothing -> False
      Just h2 -> h2 `constEq` h

-- | A policy that allows passwords to be hashed reasonably quickly, but for that
--   reason isn't suitable for high security applications.
fastBcryptHashingPolicy :: HashingPolicy
fastBcryptHashingPolicy = HashingPolicy 4 defaultHashAlgorithm

-- | A policy which makes password hashing substantially slower than
--   fastBcryptHashingPolicy, and so makes it more difficult for an adversary to
--   decrypt passwords. In a high security environment, this policy should be
--   regularly reviewed against hardware developments.
slowerBcryptHashingPolicy :: HashingPolicy
slowerBcryptHashingPolicy = fastBcryptHashingPolicy { preferredHashCost = 14 }

-- | Check whether a password hash is consistent with the current policy, or if
--   it should be updated.
hashUsesPolicy :: HashingPolicy -> BS.ByteString -> Bool
hashUsesPolicy (HashingPolicy phc pha) str =
  let phaLen = BS.length pha
      strPref = BS.take phaLen str
      strInfo = BS.take 2 (BS.drop phaLen str)
      hcBase = if phc < 10 then '0':(show phc) else show phc
  in
   (strPref == pha) && (hcBase == BS.unpack strInfo)

-- | Hashes a password (first argument) using the settings specified in second
--   argument. The settings describe the hashing variant and salt to use; because
--   the settings are prepended to password hashes, passing in an existing password
--   hash will cause the same settings to be used again.
--   You can create a hash using genSalt.
--   Result: Just hash on success, Nothing on failure (invalid settings).
hashPassword :: BS.ByteString -> BS.ByteString -> Maybe BS.ByteString
hashPassword pw setting =
  U.unsafePerformIO $ BS.useAsCString pw $
    \pw' -> BS.useAsCString setting $
      \setting' -> alloca $ \data' -> alloca $ \dlen -> do
        poke dlen 0
        poke data' nullPtr
        res <- c_crypt_ra pw' setting' data' dlen
        newData <- peek data'
        if newData == nullPtr
          then
              return Nothing
          else
            do
              ret <- if res == nullPtr
                       then
                         return Nothing
                       else
                         liftM Just $ BS.packCString res
              free newData
              return ret

-- | Prepares a settings string and salt suitable for use with hashPassword.
--   Takes a prefix specifying the type of hash, an integer specifying the
--   computational cost of hashing (4-32, or 0 for a low default), and a
--   string of random entropy.
genSalt :: BS.ByteString -> Int -> BS.ByteString -> Maybe BS.ByteString
genSalt settings cost entropy =
  U.unsafePerformIO $ BS.useAsCString settings $ \settings' ->
    BS.unsafeUseAsCString entropy $ \entropy' -> do
      res <- c_crypt_gensalt_ra settings' (fromIntegral cost) entropy' (fromIntegral $ BS.length entropy)
      if res == nullPtr
        then
          return Nothing
        else
          do
            ret <- BS.packCString res
            free res
            return $ Just ret

-- | Generates a salt using a policy, sampling from a system-appropriate source.
genSaltUsingPolicy :: HashingPolicy -> IO (Maybe BS.ByteString)
genSaltUsingPolicy (HashingPolicy hc ha) = do
  ent <- getEntropy 16
  return $ genSalt ha hc ent
