{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Options where
import qualified Data.Password.Argon2 as Argon2
import Data.Password.Bcrypt (PasswordCheck (..))
import qualified Data.Password.Bcrypt as Bcrypt
import qualified Data.Password.PBKDF2 as PBKDF2
import qualified Data.Password.Scrypt as Scrypt
import Data.Password.Types
import Data.Text (Text)
import Data.Version (showVersion)
import Options.Applicative
import Paths_password_cli (version)
data CLIOptions = CLIOptions
{ quiet :: Bool
-- ^ Prevent any logging to stdout or stderr.
, cmd :: Cmd
-- ^ What command
}
data Cmd
= HashCmd HashOpts
| CheckCmd CheckOpts
| HelpCmd [String]
data FromFileOptions = FromFileOptions
{ fromFile :: FilePath
, parseLiteralContents :: Bool
}
data HashOpts = HashOpts
{ hashPassword :: Maybe FromFileOptions
, hashAlgorithm :: HashAlgorithm
}
data CheckOpts = CheckOpts
{ hash :: Either FromFileOptions Text
, checkPassword :: Maybe FromFileOptions
, checkAlgorithm :: CheckAlgorithm
}
data HashAlgorithm
= PBKDF2HashAlgo PBKDF2.PBKDF2Params
| BcryptHashAlgo Int
| ScryptHashAlgo Scrypt.ScryptParams
| Argon2HashAlgo Argon2.Argon2Params
hashWithAlgorithm :: Password -> HashAlgorithm -> IO Text
hashWithAlgorithm pw = \case
Argon2HashAlgo p ->
unPasswordHash <$> Argon2.hashPasswordWithParams p pw
BcryptHashAlgo p ->
unPasswordHash <$> Bcrypt.hashPasswordWithParams p pw
PBKDF2HashAlgo p ->
unPasswordHash <$> PBKDF2.hashPasswordWithParams p pw
ScryptHashAlgo p ->
unPasswordHash <$> Scrypt.hashPasswordWithParams p pw
data CheckAlgorithm
= PBKDF2CheckAlgo
| BcryptCheckAlgo
| ScryptCheckAlgo
| Argon2CheckAlgo
checkWithAlgorithm :: Password -> Text -> CheckAlgorithm -> PasswordCheck
checkWithAlgorithm pw hashT = \case
Argon2CheckAlgo -> Argon2.checkPassword pw hash
BcryptCheckAlgo -> Bcrypt.checkPassword pw hash
PBKDF2CheckAlgo -> PBKDF2.checkPassword pw hash
ScryptCheckAlgo -> Scrypt.checkPassword pw hash
where
hash :: PasswordHash a
hash = PasswordHash hashT
cliOpts :: ParserInfo CLIOptions
cliOpts =
info cliOptsParser infoMods
where
v = showVersion version
infoMods =
fullDesc
<> header ("Password CLI " <> v)
<> progDesc
"A command line interface to hash and check passwords in the terminal."
cliOptsParser =
CLIOptions
<$> switch (short 'q' <> long "quiet" <> help "Suppress logging to stdout and stderr")
<*> commandsParser
<**> versionOpt v
<**> helper
-- Copied over from 'optparse-applicative-0.17.1.0's 'simpleVersioner'
versionOpt :: String -> Parser (a -> a)
versionOpt v =
infoOption v $
long "version" <> help "Show version information" <> hidden
commandsParser :: Parser Cmd
commandsParser =
hsubparser $
command "hash" hashCmd
<> command "check" checkCmd
<> command "help" helpCmd
where
hashCmd =
info (HashCmd <$> hashOptsParser) $
progDesc
"Hash a password from a file, or via stdin. Outputs to stdout.\
\ (Will add a newline at the end when used interactively on Unix)"
checkCmd =
info (CheckCmd <$> checkOptsParser) $
progDesc
"Verify a hashed password from file or stdin. Returns exit code 0\
\ when successful, and exit code 1 when the password did not match."
helpCmd =
info (HelpCmd <$> many (argument str (metavar "COMMAND"))) $
progDesc "Show help text for the given command(s)"
hashOptsParser :: Parser HashOpts
hashOptsParser =
HashOpts <$> passwordFileOption <*> hashParser
checkOptsParser :: Parser CheckOpts
checkOptsParser =
CheckOpts <$> hashOption <*> passwordFileOption <*> checkParser
where
hashOption =
Right <$> strOption (metavar "HASH" <> long "hash" <> help hashOptionMsg)
<|> Left <$> hashFileOption
hashOptionMsg = "Provide the hash as an option."
fromFileOption :: Bool -> Parser FilePath -> Parser FromFileOptions
fromFileOption acceptLiteral fileOption =
FromFileOptions <$> fileOption <*> literalParseSwitch
where
literalParseSwitch
| acceptLiteral =
switch $ long "literal-contents" <> help helpMsg
| otherwise = pure False
helpMsg =
"Hash all contents of the file. (by default only reads first line)"
passwordFileOption :: Parser (Maybe FromFileOptions)
passwordFileOption =
optional . fromFileOption True $
strOption (metavar "FILE" <> long "password-file" <> help "Hash password from file contents")
-- | We don't want to allow the '--literal-contents' option,
-- because hashes never have newlines and 'readLine' reads up to the first newline.
hashFileOption :: Parser FromFileOptions
hashFileOption =
fromFileOption False $
strOption (metavar "FILE" <> long "hash-file" <> help "Use hash from file contents")
data AlgoParsers a =
AlgoParsers
{ argon2Parser :: Parser a
, bcryptParser :: Parser a
, pbkdf2Parser :: Parser a
, scryptParser :: Parser a
, commandDesc :: String -> String
}
algorithmParser :: AlgoParsers a -> Parser a
algorithmParser AlgoParsers{..} =
hsubparser
( commandGroup "Available algorithms:"
<> command "argon2" (info argon2Parser (progDesc $ commandDesc "Argon2"))
<> command "bcrypt" (info bcryptParser (progDesc $ commandDesc "bcrypt"))
<> command "pbkdf2" (info pbkdf2Parser (progDesc $ commandDesc "PBKDF2"))
<> command "scrypt" (info scryptParser (progDesc $ commandDesc "scrypt"))
)
hashParser :: Parser HashAlgorithm
hashParser =
algorithmParser
AlgoParsers
{ argon2Parser = pure $ Argon2HashAlgo Argon2.defaultParams
, bcryptParser = pure $ BcryptHashAlgo Bcrypt.defaultParams
, pbkdf2Parser = pure $ PBKDF2HashAlgo PBKDF2.defaultParams
, scryptParser = pure $ ScryptHashAlgo Scrypt.defaultParams
, commandDesc = ("Hash a password using " <>)
}
checkParser :: Parser CheckAlgorithm
checkParser =
algorithmParser
AlgoParsers
{ argon2Parser = pure Argon2CheckAlgo
, bcryptParser = pure BcryptCheckAlgo
, pbkdf2Parser = pure PBKDF2CheckAlgo
, scryptParser = pure ScryptCheckAlgo
, commandDesc = \algo -> "Check a " <> algo <> " hash"
}