{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Main (main) where
import Control.Exception (bracket_, evaluate)
import Control.Monad (unless, void, (<=<))
import qualified Data.ByteString.Char8 as B (readFile)
import Data.Maybe (isNothing)
import Data.Password.Bcrypt (PasswordCheck (..))
import Data.Password.Types (Password, mkPassword)
import Data.Text as T (Text, strip)
import qualified Data.Text.Encoding as TE
import qualified Data.Text.IO as T
import Options.Applicative
import System.Exit (exitFailure)
import System.IO (IOMode (ReadMode), hIsTerminalDevice, hSetEcho, stderr, stdin, stdout, withFile)
import Options
main :: IO ()
main =
customExecParser (prefs showHelpOnEmpty) cliOpts >>= runCmd
runCmd :: CLIOptions -> IO ()
runCmd CLIOptions{..} =
case cmd of
HashCmd opts -> runHashCmd quiet opts
CheckCmd opts -> runCheckCmd quiet opts
HelpCmd cmds -> void $ runHelpCmd cmds
runHashCmd :: Bool -> HashOpts -> IO ()
runHashCmd quiet HashOpts {..} = do
pw <- getPassword quiet hashPassword
hash <- hashWithAlgorithm pw hashAlgorithm
b <- hIsTerminalDevice stdin
-- We only want the extra newline in interactive mode
-- and NOT when the password is given via file path.
let output =
if b && isNothing hashPassword
then T.putStrLn
else T.putStr
output hash
runCheckCmd :: Bool -> CheckOpts -> IO ()
runCheckCmd quiet CheckOpts {..} = do
pw <- getPassword quiet checkPassword
hashedPW <- getHash hash
case checkWithAlgorithm pw hashedPW checkAlgorithm of
PasswordCheckSuccess ->
qLog stdout "Password matches provided hash"
PasswordCheckFail -> do
qLog stderr "Password does not match provided hash"
exitFailure
where
qLog h = unless quiet . T.hPutStrLn h
runHelpCmd :: [String] -> IO CLIOptions
runHelpCmd cmds =
let args = cmds ++ ["-h"]
in handleParseResult $ execParserPure defaultPrefs cliOpts args
getPassword :: Bool -> Maybe FromFileOptions -> IO Password
getPassword quiet = \case
Just FromFileOptions{..} ->
mkPassword <$> readLine parseLiteralContents fromFile
Nothing -> do
-- Wondering if we should also gate this behind `hIsTerminalDevice`?
-- Though that could mean Windows users would never see this in certain terminals.
unless quiet $
putStrLn "Enter password:"
bracket_
(hSetEcho stdin False)
(hSetEcho stdin True)
(mkPassword <$> T.hGetLine stdin)
getHash :: Either FromFileOptions Text -> IO Text
getHash = \case
Right hash -> pure hash
Left FromFileOptions{..} -> readLine parseLiteralContents fromFile
readLine :: Bool -> FilePath -> IO Text
readLine literal x
| literal = readFullFile x
-- When not given the 'parseLiteralContents' flag, we want to be
-- lenient, and ignore white space around any hash, since those
-- will never be part of any hash, and probably also not a password.
| otherwise = T.strip <$> withFile x ReadMode T.hGetLine
-- | Copied from newest 'text-2.1' 'Data.Text.IO.Utf8' module
readFullFile :: FilePath -> IO Text
readFullFile = evaluate . TE.decodeUtf8 <=< B.readFile