Skip to content

Commit

Permalink
[NO-ISSUE] Use Sel for password hashing
Browse files Browse the repository at this point in the history
  • Loading branch information
tchoutri committed Dec 27, 2023
1 parent f5d6a0f commit 00ef9af
Show file tree
Hide file tree
Showing 8 changed files with 26 additions and 72 deletions.
8 changes: 0 additions & 8 deletions flora.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ library
-- cabal-fmt: expand src/datatypes
exposed-modules:
Data.Aeson.Orphans
Data.Password.Orphans
Data.Positive
Data.Text.Display.Orphans
Data.Time.Orphans
Expand Down Expand Up @@ -127,7 +126,6 @@ library
Flora.Model.Release.Update
Flora.Model.Requirement
Flora.Model.User
Flora.Model.User.Orphans
Flora.Model.User.Query
Flora.Model.User.Update
Flora.Publish
Expand Down Expand Up @@ -172,8 +170,6 @@ library
, odd-jobs
, openapi3
, optics-core
, password
, password-types
, pcre2
, pg-entity
, pg-transact
Expand Down Expand Up @@ -318,7 +314,6 @@ library flora-web
, one-time-password
, openapi3
, optics-core
, password
, pg-entity
, pg-transact-effectful
, postgresql-simple
Expand Down Expand Up @@ -430,7 +425,6 @@ executable flora-cli
, monad-time-effectful
, optics-core
, optparse-applicative
, password-types
, pg-transact-effectful
, PyF
, text
Expand Down Expand Up @@ -464,8 +458,6 @@ test-suite flora-test
, log-effectful
, monad-time-effectful
, optics-core
, password
, password-types
, pg-entity
, pg-transact
, pg-transact-effectful
Expand Down
51 changes: 22 additions & 29 deletions src/core/Flora/Model/User.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Flora.Model.User
Expand All @@ -10,38 +11,31 @@ module Flora.Model.User
, mkUser
, mkAdmin
, hashPassword
, validatePassword
)
where

import Control.DeepSeq (NFData)
import Control.DeepSeq (NFData (..))
import Control.Monad.IO.Class
import Data.Aeson
import Data.Password.Argon2
( Argon2
, Password
, PasswordCheck (PasswordCheckSuccess)
, PasswordHash
)
import Data.Password.Argon2 qualified as Argon2
import Data.Password.Orphans ()
import Data.Text (Text)
import Data.Text.Display (Display, ShowInstance (..), displayBuilder)
import Data.Text.Display (Display, ShowInstance (..))
import Data.Time (UTCTime)
import Data.UUID
import Data.UUID.V4 qualified as UUID
import Database.PostgreSQL.Entity
import Database.PostgreSQL.Entity.Types
import Database.PostgreSQL.Simple.FromField (FromField (..), fromJSONField)
import Database.PostgreSQL.Simple.FromField (FromField (..), ResultError (..), fromJSONField, returnError)
import Database.PostgreSQL.Simple.FromRow (FromRow (..))
import Database.PostgreSQL.Simple.Orphans ()
import Database.PostgreSQL.Simple.ToField (ToField (..), toJSONField)
import Database.PostgreSQL.Simple.ToField (Action (..), ToField (..), toJSONField)
import Database.PostgreSQL.Simple.ToRow (ToRow (..))
import Effectful
import Effectful.Time qualified as Time
import GHC.Generics
import GHC.TypeLits (ErrorMessage (..), TypeError)
import Sel.HMAC.SHA256 qualified as HMAC
import Sel.Hashing.Password
import Sel.Hashing.Password qualified as Sel
import Web.HttpApiData (FromHttpApiData, ToHttpApiData)

newtype UserId = UserId {getUserId :: UUID}
Expand All @@ -58,7 +52,7 @@ data User = User
, username :: Text
, email :: Text
, displayName :: Text
, password :: PasswordHash Argon2
, password :: PasswordHash
, userFlags :: UserFlags
, createdAt :: UTCTime
, updatedAt :: UTCTime
Expand Down Expand Up @@ -90,27 +84,23 @@ instance ToField UserFlags where
data UserCreationForm = UserCreationForm
{ username :: Text
, email :: Text
, password :: PasswordHash Argon2
, password :: PasswordHash
}
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)

data AdminCreationForm = AdminCreationForm
{ username :: Text
, email :: Text
, password :: PasswordHash Argon2
, password :: PasswordHash
}
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)

-- | Type error! Do not use 'toJSON' on a 'Password'!
instance TypeError (CannotDisplayPassword "JSON") => ToJSON Password where
instance TypeError (CannotDisplayPassword "JSON") => ToJSON PasswordHash where
toJSON = error "unreachable"

-- | Type error! Do not use 'display' on a 'Password'!
instance TypeError (CannotDisplayPassword "Text") => Display Password where
displayBuilder = error "unreachable"

type CannotDisplayPassword e =
'Text "🚫 Tried to convert plain-text Password to "
':<>: 'Text e
Expand All @@ -120,8 +110,15 @@ type CannotDisplayPassword e =
':<>: 'Text e
':$$: 'Text ""

deriving via Text instance ToField (PasswordHash a)
deriving via Text instance FromField (PasswordHash a)
instance ToField PasswordHash where
toField = Escape . Sel.passwordHashToByteString

instance FromField PasswordHash where
fromField f Nothing = returnError UnexpectedNull f ""
fromField _ (Just bs) = pure $ Sel.asciiByteStringToPasswordHash bs

instance NFData PasswordHash where
rnf a = seq a ()

mkUser :: IOE :> es => UserCreationForm -> Eff es User
mkUser UserCreationForm{username, email, password} = do
Expand All @@ -147,9 +144,5 @@ mkAdmin AdminCreationForm{username, email, password} = do
let totpEnabled = False
pure User{..}

hashPassword :: IOE :> es => Password -> Eff es (PasswordHash Argon2)
hashPassword = Argon2.hashPassword

validatePassword :: Password -> PasswordHash Argon2 -> Bool
validatePassword inputPassword hashedPassword =
Argon2.checkPassword inputPassword hashedPassword == PasswordCheckSuccess
hashPassword :: IOE :> es => Text -> Eff es PasswordHash
hashPassword = liftIO . Sel.hashText
17 changes: 0 additions & 17 deletions src/core/Flora/Model/User/Orphans.hs

This file was deleted.

10 changes: 0 additions & 10 deletions src/datatypes/Data/Password/Orphans.hs

This file was deleted.

1 change: 0 additions & 1 deletion src/web/FloraWeb/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import Servant.Client.Core
import Servant.Client.Generic

import Flora.Model.PersistentSession
import Flora.Model.User.Orphans ()
import FloraWeb.Common.Auth
import FloraWeb.Pages.Routes qualified as Pages
import FloraWeb.Pages.Routes qualified as Web
Expand Down
1 change: 0 additions & 1 deletion src/web/FloraWeb/Pages/Routes/Sessions.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Servant.HTML.Lucid
import Web.FormUrlEncoded

import Flora.Model.PersistentSession
import Flora.Model.User.Orphans ()
import Web.Cookie

type Routes = NamedRoutes Routes'
Expand Down
9 changes: 4 additions & 5 deletions src/web/FloraWeb/Pages/Server/Sessions.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@

module FloraWeb.Pages.Server.Sessions where

import Control.Monad.IO.Class
import Data.Maybe
import Data.Password.Argon2
import Data.Text (Text)
import Data.Text.Display
import Log qualified
import Optics.Core
import Sel.Hashing.Password qualified as Sel
import Servant

import Control.Monad.IO.Class
import Data.Text (Text)
import Flora.Model.PersistentSession
import Flora.Model.User
import Flora.Model.User.Orphans ()
import Flora.Model.User.Query qualified as Query
import FloraWeb.Common.Auth
import FloraWeb.Common.Auth.TwoFactor qualified as TwoFactor
Expand Down Expand Up @@ -60,7 +59,7 @@ createSessionHandler LoginForm{email, password, totp} = do
Just user ->
if user.userFlags.canLogin
then
if validatePassword (mkPassword password) user.password
if Sel.verifyText user.password password
then do
if user.totpEnabled
then guardThatUserHasProvidedTOTP totp $ \userCode -> do
Expand Down
1 change: 0 additions & 1 deletion test/fixtures/Cabal/hackage/flora.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ library
Flora.Model.Release.Update
Flora.Model.Requirement
Flora.Model.User
Flora.Model.User.Orphans
Flora.Model.User.Query
Flora.Model.User.Update
Flora.Publish
Expand Down

0 comments on commit 00ef9af

Please sign in to comment.