packages feed

saltine-0.2.2.0: src/Crypto/Saltine/Core/Password.hs

-- |
-- Module      : Crypto.Saltine.Core.Password
-- Description : Argon2 password hash
-- Copyright   : (c) Promethea Raschke 2018
--                   Max Amanshauser 2021
-- License     : MIT
-- Maintainer  : max@lambdalifting.org
-- Stability   : experimental
-- Portability : non-portable
--
-- Password hashing and key derivation
--
-- When in doubt, just use one of [ interactivePolicy, moderatePolicy, sensitivePolicy ],
-- but this module also allows you to fine-tune parameters for specific circumstances.
--
-- This module uses the @Text@ type for passwords, because this seems to be the only
-- reasonable way to get consistent encodings across locales and architectures, short of
-- letting users mess around with ByteStrings themselves.

module Crypto.Saltine.Core.Password
  ( Salt
  , newSalt

  , needsRehash

  , pwhashStr
  , pwhashStrVerify
  , pwhash

  , Policy(..)
  , interactivePolicy
  , moderatePolicy
  , sensitivePolicy

  , Opslimit
  , opslimit
  , getOpslimit

  , minOpslimit
  , maxOpslimit

  , opslimitInteractive
  , opslimitModerate
  , opslimitSensitive

  , Memlimit
  , memlimit
  , getMemlimit

  , minMemlimit
  , maxMemlimit

  , memlimitInteractive
  , memlimitModerate
  , memlimitSensitive

  , Algorithm
  , defaultAlgorithm
  ) where

import Crypto.Saltine.Internal.Util
import Crypto.Saltine.Internal.Password as I
import Data.ByteString (ByteString)
import Data.Text       (Text)
import Foreign.C
import System.IO.Unsafe

import qualified Crypto.Saltine.Internal.Password as Bytes
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE


newSalt :: IO Salt
newSalt = Salt <$> randomByteString Bytes.pwhash_saltbytes

-- | Hashes a password according to the policy
-- This function is non-deterministic and hence in IO.
-- Since this function may cause a huge amount of memory to be allocated, it will return
-- Nothing if the allocation failed and on any other error.
pwhashStr :: Text -> Policy -> IO (Maybe PasswordHash)
pwhashStr pw policy = do
  let (ops, mem, _alg) = unpackPolicy policy

  -- Hash is always ASCII, so no decoding needed
  fmap (fmap (PasswordHash . T.pack)) $ allocaBytes pwhash_strbytes $ \pp ->
    constByteStrings [TE.encodeUtf8 pw] $ \ [(ppw, ppwlen)] -> do
          ret <- c_pwhash_str pp ppw (fromIntegral ppwlen) (fromIntegral ops) (fromIntegral mem)

          case ret of
              0 -> Just <$> peekCAString pp
              _ -> pure Nothing

-- | Verifies that a certain password hash was constructed from the supplied password
pwhashStrVerify :: PasswordHash -> Text -> Bool
pwhashStrVerify (PasswordHash h) pw = unsafePerformIO $
  constByteStrings [TE.encodeUtf8 $ T.snoc h '\NUL', TE.encodeUtf8 pw] $ \[(ph, _), (ppw, ppwlen)] -> do
    res <- c_pwhash_str_verify ph ppw (fromIntegral ppwlen)
    pure (res == 0)

-- | Indicates whether a password needs to be rehashed, because the opslimit/memlimit parameters
-- used to hash the password are inconsistent with the supplied values.
-- Returns Nothing if the hash appears to be invalid.
-- Internally this function will always use the current DefaultAlgorithm and hence will give
-- undefined results if a different algorithm was used to hash the password.
needsRehash :: Opslimit -> Memlimit -> PasswordHash -> Maybe Bool
needsRehash (Opslimit ops) (Memlimit mem) (PasswordHash h) =
    unsafePerformIO $
        constByteStrings [TE.encodeUtf8 $ T.snoc h '\NUL'] $ \[(ph,_)] -> do
            res <- c_pwhash_str_needs_rehash ph (fromIntegral ops) (fromIntegral mem)
            pure $  if res == -1
                    then Nothing
                    else Just (res == 1)

-- | Derives a key of the specified length from a password using a salt according to the provided policy.
-- Since this function may cause a huge amount of memory to be allocated, it will return
-- Nothing if the allocation failed and on any other error.
pwhash :: Text -> Int -> Salt -> Policy -> Maybe ByteString
pwhash pw len (Salt salt) policy = do
  let (ops, mem, alg) = unpackPolicy policy

  let (e, hashed) =
        buildUnsafeByteString len $ \hbuf ->
          constByteStrings [TE.encodeUtf8 pw, salt] $ \[(ppw,ppwlen), (psalt,_)] ->
              c_pwhash
                    hbuf (fromIntegral len)
                    ppw (fromIntegral ppwlen)
                    psalt
                    (fromIntegral ops)
                    (fromIntegral mem)
                    (fromIntegral $ fromEnum alg)

  if e == -1
  then Nothing
  else Just hashed


-- | Smart constructor for opslimit
opslimit :: Algorithm -> Int -> Maybe Opslimit
opslimit alg x
  | Opslimit x < minOpslimit alg = Nothing
  | Opslimit x > maxOpslimit alg = Nothing
  | otherwise = Just (Opslimit x)

opslimitInteractive :: Algorithm -> Opslimit
opslimitInteractive DefaultAlgorithm = Opslimit (fromIntegral Bytes.pwhash_opslimit_interactive)
opslimitInteractive Argon2i13        = Opslimit (fromIntegral Bytes.pwhash_argon2i_opslimit_interactive)
opslimitInteractive Argon2id13       = Opslimit (fromIntegral Bytes.pwhash_argon2id_opslimit_interactive)

opslimitModerate :: Algorithm -> Opslimit
opslimitModerate DefaultAlgorithm   = Opslimit (fromIntegral Bytes.pwhash_opslimit_moderate)
opslimitModerate Argon2i13          = Opslimit (fromIntegral Bytes.pwhash_argon2i_opslimit_moderate)
opslimitModerate Argon2id13         = Opslimit (fromIntegral Bytes.pwhash_argon2id_opslimit_moderate)

opslimitSensitive :: Algorithm -> Opslimit
opslimitSensitive DefaultAlgorithm  = Opslimit (fromIntegral Bytes.pwhash_opslimit_sensitive)
opslimitSensitive Argon2i13         = Opslimit (fromIntegral Bytes.pwhash_argon2i_opslimit_sensitive)
opslimitSensitive Argon2id13        = Opslimit (fromIntegral Bytes.pwhash_argon2id_opslimit_sensitive)


-- | Smart constructor for memlimit
memlimit :: Algorithm -> Int -> Maybe Memlimit
memlimit alg x
  | Memlimit x < minMemlimit alg  = Nothing
  | Memlimit x > maxMemlimit alg= Nothing
  | otherwise = Just (Memlimit x)

memlimitInteractive :: Algorithm -> Memlimit
memlimitInteractive DefaultAlgorithm = Memlimit (fromIntegral Bytes.pwhash_memlimit_interactive)
memlimitInteractive Argon2i13        = Memlimit (fromIntegral Bytes.pwhash_argon2i_memlimit_interactive)
memlimitInteractive Argon2id13       = Memlimit (fromIntegral Bytes.pwhash_argon2id_memlimit_interactive)

memlimitModerate :: Algorithm -> Memlimit
memlimitModerate DefaultAlgorithm   = Memlimit (fromIntegral Bytes.pwhash_memlimit_moderate)
memlimitModerate Argon2i13          = Memlimit (fromIntegral Bytes.pwhash_argon2i_memlimit_moderate)
memlimitModerate Argon2id13         = Memlimit (fromIntegral Bytes.pwhash_argon2id_memlimit_moderate)

memlimitSensitive :: Algorithm -> Memlimit
memlimitSensitive DefaultAlgorithm  = Memlimit (fromIntegral Bytes.pwhash_memlimit_sensitive)
memlimitSensitive Argon2i13         = Memlimit (fromIntegral Bytes.pwhash_argon2i_memlimit_sensitive)
memlimitSensitive Argon2id13        = Memlimit (fromIntegral Bytes.pwhash_argon2id_memlimit_sensitive)

defaultAlgorithm :: Algorithm
defaultAlgorithm = DefaultAlgorithm


-- | Get raw C types from a policy, suitable for passing to FFI functions
unpackPolicy :: Policy -> (CULLong, CSize, CInt)
unpackPolicy (Policy ops mem alg) =
  ( fromIntegral (getOpslimit ops)
  , fromIntegral (getMemlimit mem)
  , algorithm alg
  )


{-
Fast policy suitable for low-powered devices

Takes approximately 0.1 seconds on a typical desktop computer
and requires 64 MiB of dedicated RAM
-}
interactivePolicy :: Policy
interactivePolicy = Policy (opslimitInteractive defaultAlgorithm)
                           (memlimitInteractive defaultAlgorithm)
                           defaultAlgorithm

{-|
Moderate policy with a balance of speed and security

Takes approximately 1 second on a typical desktop computer
and requires 256 MiB of dedicated RAM
-}
moderatePolicy :: Policy
moderatePolicy = Policy (opslimitModerate defaultAlgorithm)
                        (memlimitModerate defaultAlgorithm)
                        defaultAlgorithm

{-|
High-security policy designed to make attacking the password extremely expensive

Takes several seconds on a typical desktop computer
and requires 1024 MiB of dedicated RAM
-}
sensitivePolicy :: Policy
sensitivePolicy = Policy (opslimitSensitive defaultAlgorithm)
                         (memlimitSensitive defaultAlgorithm)
                         defaultAlgorithm


minOpslimit :: Algorithm -> Opslimit
minOpslimit DefaultAlgorithm = Opslimit $ fromIntegral Bytes.pwhash_opslimit_min
minOpslimit Argon2i13        = Opslimit $ fromIntegral Bytes.pwhash_argon2i_opslimit_min
minOpslimit Argon2id13       = Opslimit $ fromIntegral Bytes.pwhash_argon2id_opslimit_min

maxOpslimit :: Algorithm -> Opslimit
maxOpslimit DefaultAlgorithm = Opslimit $ fromIntegral Bytes.pwhash_opslimit_max
maxOpslimit Argon2i13        = Opslimit $ fromIntegral Bytes.pwhash_argon2i_opslimit_max
maxOpslimit Argon2id13       = Opslimit $ fromIntegral Bytes.pwhash_argon2id_opslimit_max

minMemlimit :: Algorithm -> Memlimit
minMemlimit DefaultAlgorithm = Memlimit $ fromIntegral Bytes.pwhash_memlimit_min
minMemlimit Argon2i13        = Memlimit $ fromIntegral Bytes.pwhash_argon2i_memlimit_min
minMemlimit Argon2id13       = Memlimit $ fromIntegral Bytes.pwhash_argon2id_memlimit_min

maxMemlimit :: Algorithm -> Memlimit
maxMemlimit DefaultAlgorithm = Memlimit $ fromIntegral Bytes.pwhash_memlimit_max
maxMemlimit Argon2i13        = Memlimit $ fromIntegral Bytes.pwhash_argon2i_memlimit_max
maxMemlimit Argon2id13       = Memlimit $ fromIntegral Bytes.pwhash_argon2id_memlimit_max