packages feed

esqueleto-pgcrypto-0.1.0.0: src/Database/Esqueleto/PostgreSQL/Pgcrypto.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | This module contains functions specific to the [pgcrypto](https://www.postgresql.org/docs/current/pgcrypto.html) module

module Database.Esqueleto.PostgreSQL.Pgcrypto
    (HashAlgorithm(..),
     toCrypt,
     fromCrypt,
    ) where

import qualified Data.Text.Internal.Builder as TLB
import Database.Esqueleto.Experimental (toPersistValue)
import Database.Esqueleto.Internal.Internal

{- | pgcrypto hashing algorithms
see: https://www.postgresql.org/docs/current/pgcrypto.html

`bf` and `xdes` algorithms have an optional iterations count parameter. All limitations and considerations
mentioned in the `pgcrypto` module documentation regarding iteration count apply. It is possible to supply
an invalid iteration count, which will lead to an sql error.

/Requires/ the pgcrypto module.

-}
data HashAlgorithm
    = BF (Maybe Word)
    | MD5
    | XDES (Maybe Word)
    | DES
  deriving (Eq, Show)

{- | (@crypt()@) Calculate a crypt-like hash from the provided password

/Requires/ the pgcrypto module.

/WARNING/: Using `toCrypt` may leak sensitive data via logging. Filtering logs in production environments
when using `toCrypt`, such as using `filterLogger` on `monad-logger` based stacks is highly advised.

example:

@
share
    [mkPersist sqlSettings]
    [persistLowerCase|
    UserAccount json
        name T.Text
        UniqueName name
        passwordHash T.Text
        deriving Show Read Eq

insertSelect $ do
    pure $
        UserAccount
            <# val "username"
            <&> toCrypt (BF Nothing) "1234password"
@

-}
toCrypt :: SqlString s => HashAlgorithm -> s -> SqlExpr (Value s)
toCrypt algorithm pass =
    let alg = case algorithm of
            BF mIterCount ->
                "'bf'" <> case mIterCount of
                             Nothing -> mempty
                             Just iterCount ->
                                "," <> TLB.fromString (show iterCount)
            MD5 -> "'md5'"
            XDES mIterCount ->
                "'xdes'" <> case mIterCount of
                             Nothing -> mempty
                             Just iterCount ->
                                "," <> TLB.fromString (show iterCount)
            DES -> "'des'"
    in ERaw noMeta $ \_ _ -> ("crypt (?, gen_salt(" <> alg <> "))", [toPersistValue pass])

{- | (@crypt()@) Retrieve a hashed password

/Requires/ the pgcrypto module.

example:

@
share
    [mkPersist sqlSettings]
    [persistLowerCase|
    UserAccount json
        name T.Text
        UniqueName name
        passwordHash T.Text
        deriving Show Read Eq


login name pwd = select $ do
    user <- from $ Table UserAccount
    where_ $ user ^. UserAccountName ==. val name
        &&. fromCrypt (user ^. UserAccountPasswordHash) pwd
    pure user
@

-}
fromCrypt :: SqlString s => SqlExpr (Value s) -> s -> SqlExpr (Value Bool)
fromCrypt expr pass =
    expr
        ==. ERaw
            noMeta
            ( \_ info ->
                let name = columnName expr info
                in ("crypt (?, " <> name <> ")", [toPersistValue pass])
            )
  where
    columnName (ERaw _ f) info =
            fst $ f Never info