espial-0.0.38: src/Model/Custom.hs
module Model.Custom where
import Crypto.BCrypt (HashingPolicy (..), hashPasswordUsingPolicy, validatePassword)
import Crypto.Hash.SHA256 qualified as SHA256
import Data.Aeson qualified as A
import Data.Base64.Types qualified as Base64
import Data.ByteString (ByteString)
import Data.ByteString.Base64.URL qualified as Base64Url
import Data.ByteString.Builder qualified as BB
import Data.ByteString.Lazy qualified as LBS
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Database.Persist.Sql
import System.Entropy (getEntropy)
import Prelude
mkSlug :: Int -> IO T.Text
mkSlug size =
TE.decodeUtf8 . LBS.toStrict . BB.toLazyByteString . BB.byteStringHex
<$> getEntropy size
-- * Bookmark Slug
newtype BmSlug = BmSlug
{ unBmSlug :: T.Text
}
deriving (Eq, PersistField, PersistFieldSql, Show, Read, Ord, A.FromJSON, A.ToJSON)
mkBmSlug :: IO BmSlug
mkBmSlug = BmSlug <$> mkSlug 6
-- * Note Slug
newtype NtSlug = NtSlug
{ unNtSlug :: T.Text
}
deriving (Eq, PersistField, PersistFieldSql, Show, Read, Ord, A.FromJSON, A.ToJSON)
mkNtSlug :: IO NtSlug
mkNtSlug = NtSlug <$> mkSlug 10
-- * Password Hashing
newtype PasswordHash = PasswordHash
{ unPasswordHash :: T.Text
}
deriving (Eq, PersistField, PersistFieldSql, Show, Ord, A.FromJSON, A.ToJSON)
bcryptPolicy :: HashingPolicy
bcryptPolicy = HashingPolicy {preferredHashCost = 12, preferredHashAlgorithm = "$2a$"}
data HashAlgoConfig
= HashAlgoBCrypt HashingPolicy
-- | Hash a password with BCrypt.
hashPasswordBCrypt :: T.Text -> IO PasswordHash
hashPasswordBCrypt = hashPasswordWith (HashAlgoBCrypt bcryptPolicy)
hashPasswordBCryptWithPolicy :: HashingPolicy -> T.Text -> IO PasswordHash
hashPasswordBCryptWithPolicy policy = hashPasswordWith (HashAlgoBCrypt policy)
-- | True when the stored hash should be rehashed under 'targetAlgo'
needsRehash :: HashAlgoConfig -> PasswordHash -> Bool
needsRehash (HashAlgoBCrypt _) (PasswordHash stored) = not ("$2" `T.isPrefixOf` stored)
-- | Hash a password under the given algorithm/parameters
hashPasswordWith :: HashAlgoConfig -> T.Text -> IO PasswordHash
hashPasswordWith (HashAlgoBCrypt policy) pwd = do
mbs <- hashPasswordUsingPolicy policy (TE.encodeUtf8 pwd)
case mbs of
Nothing -> ioError (userError "bcrypt: invalid hashing policy")
Just bs -> pure (PasswordHash (TE.decodeUtf8 bs))
-- | Validate a plaintext password against a stored hash.
validatePasswordHash :: PasswordHash -> T.Text -> Bool
validatePasswordHash ph@(PasswordHash h) pwd
| "$2" `T.isPrefixOf` h = validateBCrypt ph pwd
| otherwise = False
validateBCrypt :: PasswordHash -> T.Text -> Bool
validateBCrypt (PasswordHash h) pwd =
validatePassword (TE.encodeUtf8 h) (TE.encodeUtf8 pwd)
-- * Model Crypto (API key)
newtype ApiKey = ApiKey {unApiKey :: T.Text}
newtype HashedApiKey
= HashedApiKey T.Text
deriving stock (Eq, Ord, Show)
deriving newtype (PersistField, PersistFieldSql, A.FromJSON, A.ToJSON)
generateApiKey :: IO ApiKey
generateApiKey = do
bytes <- getEntropy 32
pure $ ApiKey $ Base64.extractBase64 $ Base64Url.encodeBase64 bytes
hashApiKey :: ApiKey -> HashedApiKey
hashApiKey =
HashedApiKey
. TE.decodeUtf8
. Base64.extractBase64
. Base64Url.encodeBase64'
. sha256Bytes
. TE.encodeUtf8
. unApiKey
where
sha256Bytes :: ByteString -> ByteString
sha256Bytes = SHA256.hash