-- Copyright (C) 2011 Robert Helgesson <robert@rycee.net>
--
-- This program is free software: you can redistribute it and/or
-- modify it under the terms of the GNU General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful, but
-- WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-- General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see
-- <http://www.gnu.org/licenses/>.
module Main (main) where
import Paths_pwstore_cli (version)
import Control.Monad (when, unless)
import Crypto.PasswordStore ( genSaltIO
, makeSalt
, makePasswordSalt
, isPasswordFormatValid
, verifyPassword
, strengthenPassword
)
import Data.Text (Text)
import Data.ByteString (ByteString)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.IO as T
import Data.Version (showVersion)
import System.Console.CmdArgs
import System.Environment (getProgName)
import System.Exit (exitFailure)
import System.IO (stdin, stdout, hFlush, hIsTerminalDevice, hSetEcho)
-- | The modes of operations supported by this program.
data Modes = Generate { strength :: Int
, salt :: Maybe String
}
| Verify { hash :: String }
| Strengthen { hash :: String
, strength :: Int
}
deriving (Show, Data, Typeable)
-- | Build the parser for the generator mode options.
optGen :: Modes
optGen =
Generate
{ strength = 12
&= help "Strength value, default is 12"
, salt = Nothing
&= help "Hash salt, a random salt is used if missing"
&= typ "SALT"
&= explicit
&= name "salt"
}
&= auto
&= help "Generates a password hash. The password will be read from\
\ standard input. Note, passwords and salts will be\
\ converted to UTF-8 before hashing."
-- | Build the parser for the verify mode options.
optVerify :: Modes
optVerify =
Verify
{ hash = def
&= typ "HASH"
&= argPos 0
}
&= help "Verify a given hash against a password."
-- | Build the parser for the strengthen mode options.
optStrengthen :: Modes
optStrengthen =
Strengthen
{ strength = 12
&= help "The new hash strength"
, hash = def
&= typ "HASH"
&= argPos 0
}
&= help "Strengthens the given hash."
-- | Command line options for the pwstore command.
optAll :: String -> Modes
optAll progname =
modes [optGen, optVerify, optStrengthen]
&= program progname
&= summary (progname ++ " " ++ showVersion version)
&= help "The program provides a simple command line interface\
\ to Peter Scott's pwstore library."
-- | Prompts without echoing the response.
silentPrompt :: String -> IO Text
silentPrompt p =
do putStr p
hFlush stdout
hSetEcho stdin False
l <- T.getLine
hSetEcho stdin True
putStrLn ""
return l
-- | IO action that reads a password from standard input. Terminal
-- echoing is disabled while reading interactively. If not reading
-- interactively we simply read without prompting.
getPassword :: Bool -> IO Text
getPassword verify =
do isTerminal <- hIsTerminalDevice stdin
case (isTerminal, verify) of
(True, True) -> readVerify
(True, False) -> silentPrompt "Password: "
(False, _) -> T.getLine
where
readVerify :: IO Text
readVerify =
do a <- silentPrompt "Password: "
b <- silentPrompt "Repeat password: "
if a /= b
then putStrLn "Try again!" >> readVerify
else return a
-- | Converts a string to a hash byte string. If the given string is
-- not in a valid format then an error message will be printed and the
-- program exited.
strToHash :: String -> IO ByteString
strToHash hash =
do let hash' = T.encodeUtf8 . T.pack $ hash
unless (isPasswordFormatValid hash') $
putStrLn "Invalid password hash format." >> exitFailure
return hash'
-- | Performs the appropriate actions based on the given options
-- argument.
runMode :: Modes -> IO ()
runMode Generate {..} =
do when (strength < 1) $
putStrLn "Strength must be positive." >> exitFailure
when (maybe False ((< 8) . length) salt) $
putStrLn "The salt must be 8 characters or longer." >> exitFailure
password <- fmap T.encodeUtf8 $ getPassword True
salt' <- maybe genSaltIO (return . makeSalt . T.encodeUtf8 . T.pack) salt
T.putStrLn . T.decodeUtf8 $ makePasswordSalt password salt' strength
runMode Verify {..} =
do hash' <- strToHash hash
password <- getPassword False
if verifyPassword (T.encodeUtf8 password) hash'
then putStrLn "good password"
else putStrLn "bad password"
runMode Strengthen {..} =
do hash' <- strToHash hash
T.putStrLn . T.decodeUtf8 $ strengthenPassword hash' strength
main :: IO ()
main = getProgName >>= cmdArgs . optAll >>= runMode