Tahin-0.1.2: src/Main.hs
-- Tahin
-- Copyright (C) 2015, 2016 Moritz Schulte <mtesseract@silverratio.net>
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified System.Console.Haskeline as HL
import qualified Data.Map as M
import System.Exit
import Options.Applicative
import Control.Exception
import Control.Monad.Identity (runIdentity)
import Control.Monad.Reader
import qualified Data.ByteString as BS
import qualified Crypto.Hash.SHA1 as SHA1
import qualified Crypto.Hash.SHA224 as SHA224
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Crypto.Hash.SHA384 as SHA384
import qualified Crypto.Hash.SHA512 as SHA512
import qualified Crypto.Hash.Tiger as Tiger
import qualified Crypto.Hash.Whirlpool as Whirlpool
import Data.Maybe
import Data.Version
import Data.Char
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Data.Text (Text)
import Data.Typeable
import Paths_Tahin
import Crypto.Tahin
-- | The type of a hash function; a hash function maps a ByteString to
-- a ByteString.
type HashFunction = BS.ByteString -> BS.ByteString
-- | A Hash object contains a name and a HashFunction.
data Hash =
Hash { hashString :: String -- ^ Name of this hashing algorithm
, hashFunction :: HashFunction -- ^ The hash function
}
-- | A TahinEnv contains the environment we pass down using the
-- ReaderT monad transformer.
data TahinEnv = TahinEnv { tahinEnvOptions :: TahinOptions }
-- | The name of the program.
programName :: String
programName = "Tahin"
-- | The version of the program.
programVersion :: String
programVersion = showVersion version
-- | Short description of the program.
programDescriptionShort :: String
programDescriptionShort = "(Simple & Stupid) Password Generator"
-- | Short description of the program.
programDescription :: String
programDescription = "Tahin generates a password by concatenating a 'master password' \
\with an 'identifier', transforming this string with a hash \
\function and finally base64-encode the resulting binary string."
-- Define Tahin exceptions
-- | Exception type used in Tahin.
data TahinException =
TahinExceptionNone -- ^ Exception value representing no
-- exception
| TahinExceptionString String -- ^ Exception value holding an error
-- message
deriving (Show, Typeable)
-- | A 'NokeeException' is an 'Exception'.
instance Exception TahinException
-- | Supported hashes.
hashes :: [Hash]
hashes = [ Hash "SHA1" SHA1.hash
, Hash "SHA224" SHA224.hash
, Hash "SHA256" SHA256.hash
, Hash "SHA384" SHA384.hash
, Hash "SHA512" SHA512.hash
, Hash "TIGER" Tiger.hash
, Hash "WHIRLPOOL" Whirlpool.hash ]
-- | Supported hashes as a map, mapping their names to the respective
-- Hash values.
hashesMap :: M.Map String Hash
hashesMap = M.fromList $ map (\ hash@(Hash name _) -> (name, hash)) hashes
-- | The default hash function used by Nokee.
defaultHash :: String
defaultHash = "SHA256"
-- | The default maximum length of the base64 encoded bytestring.
defaultLength :: Int
defaultLength = 20
-- | Master password prompt.
defaultPromptMaster1 :: Text
defaultPromptMaster1 = "Master Password"
-- | Master password prompt (retype).
defaultPromptMaster2 :: Text
defaultPromptMaster2 = "Master Password (retype)"
-- | Identifier prompt.
defaultPromptIdentifier :: Text
defaultPromptIdentifier = "Identifier"
-- | Displays a prompt and tries to read a password from the
-- terminal. Returns Maybe a String wrapped in IO.
readPassword :: Text -> ReaderT TahinEnv IO (Maybe Text)
readPassword prompt = liftIO $
HL.runInputT HL.defaultSettings $
fmap T.pack <$> HL.getPassword (Just '*') (T.unpack (T.concat [prompt, ": "]))
-- | Displays a prompt and tries to read a password from the
-- terminal. On failure, throw an exception.
readPassword' :: Text -> String -> ReaderT TahinEnv IO Text
readPassword' prompt errMsg = do
maybePassword <- readPassword prompt
case maybePassword of
Just password -> return password
Nothing -> throw (TahinExceptionString errMsg)
-- | Command Dispatcher.
commandDispatcher :: ReaderT TahinEnv IO ()
commandDispatcher = do
cmds <- mapReaderT (return . runIdentity) extractCommands
let cmd = case cmds of
[] -> runTahin -- Default command is run Tahin.
[c] -> c -- If exactly one command is given, execute it.
_ ->
-- Fail, if multiple commands are given.
throw (TahinExceptionString "Multiple commands specified")
cmd
where -- | This function usees commandSpec to compute the list of
-- specified commands.
extractCommands = do
maybeCmds <- forM commandSpec
(\ (cmdTest, cmdFunc) -> do
testRes <- cmdTest
if testRes
then return $ Just cmdFunc
else return Nothing)
return $ catMaybes maybeCmds
-- | This defines the commands supported (besides the default
-- command) along with suitable test functions, which check if
-- the respective command is given.
commandSpec =
[ (optsVersion . tahinEnvOptions <$> ask, printVersion)
, (optsListHashes . tahinEnvOptions <$> ask, listHashes) ]
-- | This function is just a wrapper around the function 'runTahin', adding
-- exception handling. It gets called after arguments have been
-- parsed.
main' :: TahinOptions -> IO ()
main' opts = do
let tahinEnv = TahinEnv { tahinEnvOptions = opts }
catch (runReaderT commandDispatcher tahinEnv)
(\ e -> case (e :: TahinException) of
TahinExceptionString s -> do putStrLn $ "Error: " ++ s
exitFailure
TahinExceptionNone -> return ())
-- | Print version information to stdout.
printVersion :: ReaderT TahinEnv IO ()
printVersion = liftIO $ putStrLn $ programName ++ " " ++ programVersion
-- | Print supported hashes to stdout.
listHashes :: ReaderT TahinEnv IO ()
listHashes = do
let hashNames = map fst (M.toList hashesMap)
mapM_ (liftIO . putStrLn) hashNames
-- | Try to lookup a hashing function by its name.
lookupHash :: String -> Maybe Hash
lookupHash hashName = M.lookup hashName' hashesMap
where hashName' = toUpperCase hashName
toUpperCase = map toUpper
-- | May throw exceptions of type TahinException
lookupHash' :: String -> Hash
lookupHash' hashName =
let maybeHash = lookupHash hashName
in fromMaybe (throw (TahinExceptionString ("Unknown hash: " ++ hashName)))
maybeHash
-- | This function implements the main program logic. May throw
-- TahinExceptions, they will be handled in the caller.
runTahin :: ReaderT TahinEnv IO ()
runTahin = do
opts <- tahinEnvOptions <$> ask
let len = optsLength opts
hashName = map Data.Char.toUpper (optsHash opts)
hash = lookupHash' hashName
-- Force evaluation in order to trigger hash-not-found exception now
-- in case the specified hash could not be found:
_ <- liftIO $ evaluate hash
infoMessage $ "Using hash function " ++ hashName
infoMessage $ "Length is at most " ++ show len
passwdMaster <- retrieveMasterPassword
passwdIdentifier <- readPassword' defaultPromptIdentifier errMsgRetrieveIdentifier
let tahinPasswd = tahin (hashFunction hash) len passwdMaster passwdIdentifier
liftIO $ TIO.putStrLn tahinPasswd
where errMsgRetrieveMasterPasswd = "Failed to retrieve master password"
errMsgRetrieveIdentifier = "Failed to retrieve password identifier"
verifyPasswords pw1 pw2 =
if pw1 == pw2
then return pw1
else throw (TahinExceptionString "Password mismatch")
retrieveMasterPassword = do
twice <- optsTwice . tahinEnvOptions <$> ask
if twice
then retrieveMasterPasswordTwice
else retrieveMasterPasswordOnce
retrieveMasterPasswordOnce =
readPassword' defaultPromptMaster1 errMsgRetrieveMasterPasswd
retrieveMasterPasswordTwice = do
master1 <- readPassword' defaultPromptMaster1 errMsgRetrieveMasterPasswd
master2 <- readPassword' defaultPromptMaster2 errMsgRetrieveMasterPasswd
verifyPasswords master1 master2
-- | Type holding the information about parsed arguments.
data TahinOptions = TahinOptions
{ optsVersion :: Bool
, optsListHashes :: Bool
, optsVerbose :: Bool
, optsHash :: String
, optsLength :: Int
, optsTwice :: Bool
}
-- | Print an informational if verbose is activated.
infoMessage :: String -> ReaderT TahinEnv IO ()
infoMessage msg = do
opts <- tahinEnvOptions <$> ask
when (optsVerbose opts) $
liftIO $ putStrLn ("[ " ++ msg ++ " ]")
-- | The argument parser.
tahinOptions :: Parser TahinOptions
tahinOptions = TahinOptions
<$> switch
(long "version"
<> help "Display version information")
<*> switch
(long "list-hashes"
<> help "List supported hashes")
<*> switch
(long "verbose"
<> help "Enable verbose mode")
<*> strOption
(long "hash"
<> short 'h'
<> value defaultHash
<> metavar "HASH"
<> help "Specify which hash to use")
<*> option auto
(long "length"
<> value defaultLength
<> short 'l'
<> metavar "LENGTH"
<> help "Specify maximum length of the password to generate")
<*> switch
(long "twice"
<> short 't'
<> help "Ask twice for the master password (e.g. for setting new passwords)")
-- | Main entry point.
main :: IO ()
main = execParser opts >>= main'
where opts = info (helper <*> tahinOptions)
(fullDesc
<> progDesc programDescription
<> header (programName ++ " - " ++ programDescriptionShort))