content: {-# LANGUAGE ConstraintKinds            #-}
  {-# LANGUAGE DataKinds                  #-}
  {-# LANGUAGE DeriveGeneric              #-}
  {-# LANGUAGE FlexibleContexts           #-}
  {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  {-# LANGUAGE OverloadedStrings          #-}
  {-# LANGUAGE RankNTypes                 #-}
  {-# LANGUAGE ScopedTypeVariables        #-}
  {-# LANGUAGE TypeFamilies               #-}
  {-# LANGUAGE TypeOperators              #-}
  
  module Thentos.CookieSession.CSRF
      ( CsrfSecret(..)
      , CsrfToken(..)
      , CsrfNonce(..)
      , GetCsrfSecret(..)
      , HasSessionCsrfToken(..)
      , MonadHasSessionCsrfToken
      , MonadViewCsrfSecret
      , genCsrfSecret
      , validFormatCsrfSecretField
      , validFormatCsrfToken
      , checkCsrfToken
      , refreshCsrfToken
      , clearCsrfToken
      ) where
  
  import Control.Lens
  import Control.Monad.Reader.Class (MonadReader)
  import Control.Monad.State.Class (MonadState)
  import Control.Monad (when)
  import Crypto.Hash (SHA256)
  import Crypto.MAC.HMAC (HMAC,hmac)
  import Crypto.Random (MonadRandom(..))
  import Data.Aeson (FromJSON, ToJSON)
  import Data.ByteArray.Encoding (convertToBase, convertFromBase, Base(Base16))
  import Data.String.Conversions (SBS, ST, cs, (<>))
  import Data.String (IsString)
  import Data.Typeable (Typeable)
  import GHC.Generics (Generic)
  
  import qualified Data.ByteString as SBS
  import qualified Data.Text as ST
  
  import Servant.Missing (MonadError500, throwError500)
  import Thentos.CookieSession.Types (ThentosSessionToken(fromThentosSessionToken), MonadUseThentosSessionToken, getThentosSessionToken)
  
  -- | This token is used to prevent CSRF (Cross Site Request Forgery).
  -- This token is part of 'FrontendSessionData' since it is required by the views which
  -- generate the forms with a special hidden field containing the value of this token.
  -- However, this token is cleared before being serialized as a cookie.
  -- Indeed we have no need yet to have it on the client side nor to make it persistent.
  -- When processing requests, this token is freshly generated from the 'CsrfSecret' and the
  -- 'ThentosSessionToken'. This token is only used by requests that yield an HTML form.
  -- Upon POST requests on such forms, the handlers will check the validity of the CSRF token.
  -- Verification of this token can be done solely from the 'CsrfSecret' and
  -- the 'ThentosSessionToken'.
  --
  -- This all means that if the attacker could get access to one of these tokens it would be enough to
  -- validate any form.  Changing the token on every request even inside the session helps to counter
  -- an attack based on entropy leakage through TLS plaintext compression.  TLS compression should be
  -- disabled for the exact reason that it is vulnerable to this attack, but this code does not rely
  -- on it.
  --
  -- *The idea of the attack:* When combining encryption (which hides the contents but not the length)
  -- and compression (which makes the length depend on the content).  If you compress after encryption
  -- its safe but useless; if you compress then encrypt (which is often done), then some data leaks
  -- through the length of the ciphertext.  The BEAST attack exploited this by guessing the CSRF token
  -- by sending request to the server pretending to be the client injecting in the request the guess
  -- of the token in a parameter which is echoed back by the server to the real client encrypted for
  -- the client.  Assuming the CSRF token is given as an attribute such as csrf:SOMESECRET to keep it
  -- simple, then the guess is going to be csrf:XYZ with all combination of XYZ then you look at which
  -- answer was the shortest it is highly likely that XYZ=SOM will compress better than the rest
  -- because of the repetition with the real secret also part of the response. You then proceed with
  -- your guess being csrf:SOMXYZ. On a test setup, it was possible to recover the full token in 30
  -- secs.
  newtype CsrfToken = CsrfToken { fromCsrfToken :: ST }
      deriving (Eq, Ord, Show, Read, FromJSON, ToJSON, Typeable, Generic, IsString)
  
  newtype CsrfSecret = CsrfSecret SBS
      deriving (Show, Eq)
  
  newtype CsrfNonce  = CsrfNonce  SBS
      deriving (Show, Eq)
  
  class GetCsrfSecret a where
      csrfSecret :: Getter a (Maybe CsrfSecret)
  
  instance GetCsrfSecret ST.Text where
      csrfSecret = to $ \secret -> do
          let Right sbs = convertFromBase Base16 (cs secret :: SBS)
          return $ CsrfSecret sbs
  
  class HasSessionCsrfToken a where
      sessionCsrfToken :: Lens' a (Maybe CsrfToken)
  
  type MonadHasSessionCsrfToken s m = (MonadState s m, HasSessionCsrfToken s)
  
  type MonadViewCsrfSecret e m = (MonadReader e m, GetCsrfSecret e)
  
  -- | This ONLY checks the format of a given CSRF secret, not if it has been randomly choosen (duh!).
  validFormatCsrfSecretField :: Maybe ST -> Bool
  validFormatCsrfSecretField ms
      | Just t <- ms
      , Right s' <- convertFromBase Base16 (cs t :: SBS)
      = SBS.length s' == 32
      | otherwise = False
  
  -- | This ONLY checks the format of a given CSRF token, not if it has been tampered with.
  validFormatCsrfToken :: CsrfToken -> Bool
  validFormatCsrfToken (CsrfToken st)
      | Right s' <- convertFromBase Base16 (cs st :: SBS) = SBS.length s' == 64
      | otherwise                                         = False
  
  -- | Computes a valid CSRF token given a nonce.
  makeCsrfToken :: (MonadError500 err m, MonadViewCsrfSecret e m, MonadUseThentosSessionToken s m) =>
                   CsrfNonce -> m CsrfToken
  makeCsrfToken (CsrfNonce rnd) = do
      maySessionToken <- use getThentosSessionToken
      case maySessionToken of
          Nothing -> throwError500 "No session token"
          Just sessionToken -> do
              Just (CsrfSecret key) <- view csrfSecret
              return $ CsrfToken . cs $ rnd <> convertToBase Base16 (hmac key (tok <> rnd) :: HMAC SHA256)
            where
              tok = cs $ fromThentosSessionToken sessionToken
  
  -- | Extracts the nonce part from the CSRF token.
  csrfNonceFromCsrfToken :: CsrfToken -> CsrfNonce
  csrfNonceFromCsrfToken = CsrfNonce . SBS.take 64 . cs . fromCsrfToken
  
  -- | Verify the authenticity of a given 'CsrfToken'.  This token should come from the form data of
  -- the POST request, NOT from 'FrontendSessionData'.
  checkCsrfToken :: (MonadError500 err m, MonadViewCsrfSecret e m, MonadUseThentosSessionToken s m) => CsrfToken -> m ()
  checkCsrfToken csrfToken
      | not (validFormatCsrfToken csrfToken) =
          throwError500 $ "Ill-formatted CSRF Token " <> show csrfToken
      | otherwise = do
          -- Here we essentially re-create the second half of the CSRF token.
          -- If it was made with the same sessionToken and csrfSecret then it will match.
          csrfToken' <- makeCsrfToken (csrfNonceFromCsrfToken csrfToken)
          when (csrfToken /= csrfToken') $
              throwError500 "Invalid CSRF token"
  
  -- | Generates a random 'CsrfSecret'.
  genCsrfSecret :: MonadRandom m => m CsrfSecret
  genCsrfSecret = CsrfSecret . (convertToBase Base16 :: SBS -> SBS) <$> getRandomBytes 32
  
  -- | Generates a random 'CsrfNonce'.
  genCsrfNonce :: MonadRandom m => m CsrfNonce
  genCsrfNonce = CsrfNonce . (convertToBase Base16 :: SBS -> SBS) <$> getRandomBytes 32
  
  -- | See 'CsrfToken'.
  -- This function assigns a newly generated 'CsrfToken' to the 'FrontendSessionData'.
  refreshCsrfToken :: (MonadError500 err m, MonadHasSessionCsrfToken s m,
                       MonadRandom m, MonadViewCsrfSecret e m, MonadUseThentosSessionToken s m) => m ()
  refreshCsrfToken = do
      csrfToken <- makeCsrfToken =<< genCsrfNonce
      sessionCsrfToken .= Just csrfToken
  
  -- | See 'CsrfToken'
  -- As long as we do not need to generate any forms on the client side it's better to
  -- clear the value of the 'CsrfToken', preventing it to be stored in the cookie.
  -- Still if we ever need to persist a 'CsrfToken' it might be safer to refresh it
  -- with 'refreshCsrfToken' first.
  clearCsrfToken :: MonadHasSessionCsrfToken s m => m ()
  clearCsrfToken = sessionCsrfToken .= Nothing
  