Adding security to MFlow

As of March 2020, School of Haskell has been switched to read-only mode.

Improving MFlow Security

MFlow is a Web Framework that turns Web programing back into just ordinary programming by automating all the extra complexities.

Provided Demo

While looking at implementing a service that I wanted to be secure, I looked into the way passwords were stored in MFlow, particularly the User Widget. There is a demo:

import Data.Monoid
import MFlow.Wai.Blaze.Html.All

main= runNavigation "" $ transientNav loginSample

loginSample= do
    userRegister "user" "user"
    r <- page  $   p <<  "Please login with user/user"
               ++> userWidget Nothing userLogin
               <|> wlink "exit" << p << "or exit"
        
    if r == "exit" then return () else do
        user <- getCurrentUser
    
        r <- page  $   b <<  ("user logged as " <>  user)
                   ++> wlink True  << p <<  "logout"
                   <|> wlink False << p <<  "or exit"

        if r
          then do
             logout
             page  $ p << "logged out" ++> wlink () << "press here to exit"
          else return ()

Salting and Key Stretching

We can do better. Let's see how it can be done. SoH gives access to pwstore-fast which will handle the low level details for us. At first I was not aware of this library and implemented it by hand, then this made it much simpler. Usage is simple:

{-# LANGUAGE OverloadedStrings #-}
import Crypto.PasswordStore
import qualified Data.ByteString.Char8 as B
main = do
    passwordHash <- makePassword "ilovesushi" 14
    B.putStrLn passwordHash
    print $ verifyPassword "bananas" passwordHash
    print $ verifyPassword "ilovesushi" passwordHash

We see that we can create a properly salted and stretched hash.

Update Demo to use Crypto.PasswordStore

Let's add a few lines to take advantage of the benefits of the salting and stretching. Because TCache uses a file backend for persistence, we want to ensure if our files (or DB, or key-value store, etc) are compromised, that the attacker does not get plaintext passwords.

import MFlow.Wai.Blaze.Html.All
import Data.Monoid
import Data.IORef
import Data.ByteString hiding (null,map,putStr)
import qualified Data.ByteString.Char8 as BC
import Crypto.PasswordStore

import MFlow.Wai(waiMessageFlow)
import Network.Wai.Handler.WarpTLS as TLS
import Network.Wai.Handler.Warp (defaultSettings,Settings,setPort)
import Control.Workflow (Workflow)
import Control.Monad(unless)
import Data.Maybe (fromMaybe)
import System.Environment(getArgs,getEnvironment)
import Data.Char(isNumber)
-- show
main= do
    {-hi-}setAuthMethod $ Auth (tCacheRegister' defaultHashStrength){-/hi-}
                          {-hi-}tCacheValidate'{-/hi-}
    runNavigation "" $ transientNav loginSample

{-hi-}type HashStrength = Int
defaultHashStrength = 14 :: HashStrength{-/hi-}

-- | Register an user/password
tCacheRegister' ::  HashStrength -> String -> String  -> IO (Maybe String)
tCacheRegister' strength user password  =  do
  {-hi-}salted_password <- makePassword (BC.pack password) strength{-/hi-}
  atomically $ do
    let newuser = User user {-hi-}(BC.unpack salted_password){-/hi-}
    withSTMResources [newuser] $ doit {-hi-}newuser{-/hi-}
  where
    doit {-hi-}u{-/hi-} [Just (User _ _)] = resources{toReturn= Just "user already exist"}
    doit {-hi-}u{-/hi-} [Nothing] = resources{toAdd= [u],toReturn= Nothing}

tCacheValidate' ::  UserStr -> PasswdStr -> IO (Maybe String)
tCacheValidate'  u p =
    let user= eUser{userName=u}
    in  atomically
     $ withSTMResources [user]
     $ \ mu -> case mu of
         [Nothing] -> resources{toReturn= err }
         [Just u@(User _ pass )] -> resources{toReturn =
               case {-hi-}verifyPassword (BC.pack p) (BC.pack pass){-/hi-} of
                 True -> Nothing
                 False -> err
               }
     where
     err= Just  "Username or password invalid"

loginSample= do
    userRegister "user" "user"
    r <- page $ p << "Please login with user/user"
               ++> userWidget Nothing userLogin
               <|> wlink "exit" << p << "or exit"

    if r == "exit" then return () else do
        user <- getCurrentUser

        r <- page $ b << ("user logged as " <> user)
                   ++> wlink True << p << "logout"
                   <|> wlink False << p << "or exit"

        if r
          then do
             logout
             page $ p << "logged out" ++> wlink () << "press here to exit"
          else return ()
-- /show

The highlighted portions show what we did. We override the default Auth using setAuthMethod and use custom register and validate functions. The tCacheRegister` and tCacheValidate` are straight from MFlow.tCacheRegister and MFlow.tCacheValidate other than the highlighted portions. A small patch into those functions would add this capability to the first demo shown above.

Conclusions

That was pretty simple to change. I think something like that should be the default for the userWidget.

Mistakes

Let me know if I made any mistakes, especially with crypto, any ByteString conversion issues, etc.

comments powered by Disqus