diff --git a/hikaru.cabal b/hikaru.cabal index 1624ede06a33853e7cab03fd64d06df2aec8efbb..31cadfff8c6c1f66262e323cbec885e402d991ca 100644 --- a/hikaru.cabal +++ b/hikaru.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 00d0ac3f76ad05002c67411c92f1e77aeee6a21668bc86d354917560a1f0ae2f +-- hash: 909aa9806ed1dc9c21215c17ba94cb4e184959ac38e8f35cef2091f7639d4f52 name: hikaru version: 0.1.0.0 @@ -35,6 +35,7 @@ library exposed-modules: Hikaru Hikaru.Action + Hikaru.CSRF Hikaru.Dispatch Hikaru.Form Hikaru.Link @@ -46,7 +47,7 @@ library Paths_hikaru hs-source-dirs: lib - default-extensions: BlockArguments DeriveGeneric FlexibleInstances GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NamedFieldPuns NoImplicitPrelude OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving DataKinds + default-extensions: BlockArguments DefaultSignatures FlexibleInstances GeneralizedNewtypeDeriving GADTs LambdaCase MultiParamTypeClasses NamedFieldPuns NoImplicitPrelude OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving ghc-options: -Wall -Wcompat build-depends: aeson >=1.4 && <1.5 @@ -57,6 +58,7 @@ library , case-insensitive >=1.2 && <1.3 , containers >=0.6 && <0.7 , cookie >=0.4 && <0.5 + , cryptonite >=0.25 && <0.26 , http-types >=0.12 && <0.13 , lucid >=2.9 && <2.10 , mtl >=2.2 && <2.3 @@ -64,6 +66,7 @@ library , string-conversions >=0.4 && <0.5 , text >=1.2 && <1.3 , text-icu >=0.7 && <0.8 + , time >=1.8 && <1.9 , wai >=3.2 && <3.3 , wai-extra >=3.0 && <3.1 default-language: Haskell2010 diff --git a/lib/Hikaru/Action.hs b/lib/Hikaru/Action.hs index c1fd2cf62498dadad5316926bc9cd7136e8ddc68..8b4bd5543b08ad87f7612c1186bd48a842acd75b 100644 --- a/lib/Hikaru/Action.hs +++ b/lib/Hikaru/Action.hs @@ -101,7 +101,7 @@ where import qualified Data.ByteString.Lazy as LBS import qualified Data.Text.Lazy as LT - import Control.Monad.Trans (lift) + import Control.Monad.Trans import Control.Monad.Trans.Resource import Data.Aeson (Value, ToJSON, encode, eitherDecode') import Data.Binary.Builder @@ -137,11 +137,14 @@ where -- getActionEnv :: m ActionEnv + default getActionEnv + :: (MonadTrans t, MonadAction n, m ~ t n) => m ActionEnv + getActionEnv = lift getActionEnv + -- | -- Allow access to action when building HTML responses. -- - instance (MonadAction m) => MonadAction (HtmlT m) where - getActionEnv = lift getActionEnv + instance (MonadAction m) => MonadAction (HtmlT m) -- | diff --git a/lib/Hikaru/CSRF.hs b/lib/Hikaru/CSRF.hs new file mode 100644 index 0000000000000000000000000000000000000000..73b195637a5577a5f8b9af07755626d8196b22d2 --- /dev/null +++ b/lib/Hikaru/CSRF.hs @@ -0,0 +1,93 @@ +{-| +Module : Hikaru.CSRF +Copyright : Jan Hamal Dvořák +License : AGPL-3 + +Maintainer : mordae@anilinux.org +Stability : unstable +Portability : non-portable (ghc) + +CSRF mitigation utilities. +-} + +module Hikaru.CSRF + ( MonadCsrf(..) + , generateToken + , isTokenValid + ) +where + import BasePrelude + + import Control.Monad.Trans + import Crypto.Hash + import Crypto.MAC.HMAC + import Data.ByteString (ByteString) + import Data.String.Conversions + import Data.Text (Text, splitOn) + import Data.Time.Clock.POSIX (getPOSIXTime) + + + class (MonadIO m) => MonadCsrf m where + csrfTokenValidity :: m Int64 + csrfTokenSecret :: m Text + + default csrfTokenValidity + :: (MonadTrans t, MonadCsrf n, m ~ t n) => m Int64 + csrfTokenValidity = lift csrfTokenValidity + + default csrfTokenSecret + :: (MonadTrans t, MonadCsrf n, m ~ t n) => m Text + csrfTokenSecret = lift csrfTokenSecret + + + -- | + -- TODO + -- + generateToken :: (MonadCsrf m) => m Text + generateToken = do + now <- getTimestamp + secret <- csrfTokenSecret + + let signature = sign now secret + in return $ mconcat [ cs (show now), ":", signature ] + + + -- | + -- TODO + -- + isTokenValid :: (MonadCsrf m) => Text -> m Bool + isTokenValid token = do + case splitOn ":" token of + [time, signature] -> do + case readMaybe (cs time) of + Just (timestamp :: Int64) -> do + now <- getTimestamp + valid <- csrfTokenValidity + secret <- csrfTokenSecret + + if timestamp + valid >= now + then return (sign timestamp secret == signature) + else return False + + Nothing -> return False + + _else -> return False + + + -- Internals -------------------------------------------------------------- + + + getTimestamp :: (MonadIO m) => m Int64 + getTimestamp = round <$> liftIO getPOSIXTime + + + sign :: Int64 -> Text -> Text + sign timestamp secret = cs $ show $ hmacGetDigest digest + where + digest = hmac timeBytes secretBytes :: HMAC SHA256 + secretBytes = cs secret :: ByteString + timeBytes = cs time :: ByteString + time = show timestamp + + +-- vim:set ft=haskell sw=2 ts=2 et: diff --git a/lib/Hikaru/Form.hs b/lib/Hikaru/Form.hs index 2a96fc755803ca4bad4a99363e58519638216290..6d40f49064a98ef1509bd01976408bf27a46a0e9 100644 --- a/lib/Hikaru/Form.hs +++ b/lib/Hikaru/Form.hs @@ -11,17 +11,19 @@ This module provides applicative form handling. -} module Hikaru.Form - ( View(..) + ( MonadCsrf(..) + , View(..) , FormNote(..) , Form , FieldT , newForm , getForm , postForm - , inputField - , inputField' , hiddenField , hiddenField' + , csrfTokenField + , inputField + , inputField' , textArea , textArea' , selectField @@ -47,6 +49,7 @@ where import Control.Monad.State import Data.Text (Text, strip) import Hikaru.Action + import Hikaru.CSRF import Hikaru.Localize import Hikaru.Types import Lucid @@ -132,7 +135,7 @@ where = FormT { unFormT :: ReaderT (Env l) (StateT (View l) m) a } - deriving (Monad, Applicative, Functor) + deriving (MonadIO, Monad, Applicative, Functor) deriving instance (Monad m) => MonadReader (Env l) (FormT l m) deriving instance (Monad m) => MonadState (View l) (FormT l m) @@ -140,6 +143,8 @@ where instance MonadTrans (FormT l) where lift = FormT . lift . lift + instance (MonadCsrf m) => MonadCsrf (FormT l m) + newtype Form l m a = Form @@ -159,9 +164,6 @@ where r <- unFormR return $ l <*> r - instance MonadTrans (Form l) where - lift = Form . fmap Just . lift - data Env l = Env @@ -176,11 +178,13 @@ where = FieldT { unFieldT :: ReaderT Bool (StateT (View l, Maybe a) m) b } - deriving (Monad, Applicative, Functor) + deriving (MonadIO, Monad, Applicative, Functor) instance MonadTrans (FieldT l a) where lift = FieldT . lift . lift + instance (MonadCsrf m) => MonadCsrf (FieldT l a m) + -- | -- Build a fresh form without using any request data. @@ -239,6 +243,46 @@ where else return (view', value) + -- | + -- TODO + -- +{- + csrfTokenField :: (MonadCsrf m) => l -> Form l m Text + csrfTokenField msg = Form do + token <- generateToken + + unForm $ hiddenField "_token" (Just token) do + whenChecking do + value <- fromMaybe "" <$> fieldValue + valid <- isTokenValid value + + if valid + then return () + else addNote $ NoteError msg +-} + + + csrfTokenField :: (MonadCsrf m) => l -> Form l m Text + csrfTokenField msg = Form do + Env{envCheck} <- ask + + name' <- makeName "csrftoken" + value <- fromMaybe "" <$> formParamMaybe name' + valid <- isTokenValid value + token <- generateToken + + let view = HiddenField { viewName = name' + , viewValue = Just token + , viewNotes = if envCheck && not valid + then [NoteError msg] + else [] + , viewAttrs = [] + } + + modify (<> view) + return $ Just token + + -- | -- TODO -- diff --git a/package.yaml b/package.yaml index fc6c75a1bebcbdb768112b23ae42d48cae0b381c..6811789b2c244187743ac0cbfd8810884881a98a 100644 --- a/package.yaml +++ b/package.yaml @@ -24,8 +24,10 @@ extra-source-files: README.md ghc-options: -Wall -Wcompat default-extensions: - BlockArguments + - DefaultSignatures - FlexibleInstances - GeneralizedNewtypeDeriving + - GADTs - LambdaCase - MultiParamTypeClasses - NamedFieldPuns @@ -45,6 +47,7 @@ dependencies: - case-insensitive >= 1.2 && <1.3 - containers >= 0.6 && <0.7 - cookie >= 0.4 && <0.5 + - cryptonite >= 0.25 && <0.26 - http-types >= 0.12 && <0.13 - lucid >= 2.9 && <2.10 - mtl >= 2.2 && <2.3 @@ -52,6 +55,7 @@ dependencies: - string-conversions >= 0.4 && <0.5 - text >= 1.2 && <1.3 - text-icu >= 0.7 && <0.8 + - time >= 1.8 && <1.9 - wai >= 3.2 && <3.3 - wai-extra >= 3.0 && <3.1 @@ -62,6 +66,7 @@ library: exposed-modules: - Hikaru - Hikaru.Action + - Hikaru.CSRF - Hikaru.Dispatch - Hikaru.Form - Hikaru.Link