password-cli (empty) → 0.1.1.0
raw patch · 6 files changed
+492/−0 lines, 6 filesdep +basedep +bytestringdep +optparse-applicative
Dependencies added: base, bytestring, optparse-applicative, password, password-types, text
Files
- ChangeLog.md +27/−0
- LICENSE +30/−0
- README.md +101/−0
- app/Main.hs +93/−0
- app/Options.hs +199/−0
- password-cli.cabal +42/−0
+ ChangeLog.md view
@@ -0,0 +1,27 @@+## 0.1.1.0++- Small refactor and quality of life additions.+ Thanks to [@Vlix](https://github.com/Vlix)+ [#72](https://github.com/cdepillabout/password/pull/72)++- Changes include:+ - More complete explanation of the CLI in the README.+ - Added more description of commands and options.+ - Added option to read literal contents of a file.+ - Hash output now adds a newline when using the CLI interactively. (on Unix)+ - Added `--version` to only output the version of the CLI.++## 0.1.0.0++- First minimal working CLI to hash passwords and verify hashes.+ Thanks to [@blackheaven](https://github.com/blackheaven)+ [#70](https://github.com/cdepillabout/password/pull/70)++- Functionality includes:+ - Hashing (`Argon2`, `bcrypt`, `PBKDF2`, `scrypt`) interactively,+ piped to `stdin`, or from the first line in a provided file.+ - Checking a hash (`Argon2`, `bcrypt`, `PBKDF2`, `scrypt`) that is+ provided through a CLI option, or from a provided file. The password+ can be entered interactively, piped to `stdin` or from the first+ line in a provided file.+ - Option to disable logging to stdout or stderr. `-q|--quiet`
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) Dennis Gosnell, 2019; Felix Paulusma, 2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Dennis Gosnell, Felix Paulusma nor the names+ of other contributors may be used to endorse or promote products+ derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,101 @@+# `password-cli`++[](http://github.com/cdepillabout/password)+[](https://hackage.haskell.org/package/password-cli)+[](http://stackage.org/lts/package/password-cli)+[](http://stackage.org/nightly/package/password-cli)+[](./LICENSE)++This package provides a simple CLI for the [`password`](https://hackage.haskell.org/package/password) package.+As such it supports all the algorithms that the [`password`](https://hackage.haskell.org/package/password)+package supports, which at the time of writing are `Argon2`, `brypt`, `PBKDF2` and `scrypt`.++At the moment, the default settings are used for each algorithm, but this will probably become configurable in+a later version of the CLI.++## Example usage++The following sections give examples of how the CLI can be used.++### Hashing a password interactively++Hashing a password interactively is as easy as+```console+$ password-cli hash bcrypt+Enter password:+```+where the input is then hidden and the hash is printed to the screen, resulting in+```console+$ password-cli hash bcrypt+Enter password:+$2b$10$JuNbIWqVQD2EldT481zEEuaVKROrYhsHXLjM/Tx3e7ahJQxVw7N4y+```++### Hashing a password with pipes++When piping in the password from a file or other program:+```console+$ cat password.txt | password-cli hash pbkdf2+Enter password:+sha512:25000:8ZJ1T55Y0sPRwltXNe/2fA==:aA0BT1WlTg+t2pSr8E6+l2zJW88rmUiDlKeohSOnzS0nLOumDSyK0FfsiNJBvWvWVkB2r6IMxRqelk4LZR33ow==+```+You'll notice the output has no newline, so you can easily pipe the resulting+hash into a file or other program. When piping the result to a file, you'll+probably want to use `--quiet` or `-q` to make sure the `Enter password:` prompt+isn't also saved to the file.+```console+$ cat password.txt | password-cli hash pbkdf2 --quiet > password.hash+$ cat password.hash+sha512:25000:iFYCOgfOgMPp0NuPXhyucw==:XUMDNnqZo2LH08CIZr+1nbTke3N6pE95FcbZA+4A1Ng4dWHnnl4SMUTn3KXFtB0uZRrEhArLatLAH1Oo8brcVw==+```+When piping in the password, the first line of the file (i.e. up to the first newline)+is read and taken as the password. This is also the case if the password is provided+from a file, though you can set the `--literal-contents` flag to use the entire literal+file contents as the password.++### Hashing a password from a file++Instead of piping in the contents of a file, you can also just provide the path+to the file.+```console+$ password-cli hash scrypt --password-file password.txt+14|8|1|mdSECCGuEMf7GQOp9EX5EYLMW9Jwe6Dma7fwbxuNwvs=|KSh5jxOEiQPMjfng2D05/G1baiF2LyluWgg3Cfzh5arJUF3K7irRIBXoKAT/xCO11oPmsgDD7TT6l6FQth9f4g==+```+Here you don't have to pass in the `--quiet` option, since the password is already provided+so the CLI doesn't print `Enter password:` to the screen.++### Verifying a password hash++Just like when hashing a password, you can input the password manually, through pipes, or+by providing a `--password-file`.+```console+$ # Interactively check password+$ password-cli check argon2 --hash 'SOME-HASH'+Enter password:+Password matches provided hash+$ echo $?+0+```+If the provided hash doesn't match the password, `Password does not match provided hash`+will be shown and the exit code will be `1` to indicate a failed match.+```console+$ # Pipe in the password.+$ cat password.txt | password-cli check argon2 --hash 'SOME-HASH' --quiet+$ echo $?+0+$ # Give the WRONG password file.+$ password-cli check argon2 --hash 'SOME-HASH' --password-file password.txt.wrong --quiet+$ echo $?+1+```+NOTE: When giving literal hashes as arguments in the terminal,+it is advised to use single quotes (`'`) instead of double quotes (`"`)+to prevent any accidental interpolation as some hashes use+dollar signs (`$`) in the format.++You can also provide the hash from file contents by providing the path to the `--hash-file`+option. Just like the default of the `--password-file` option, this will only read up to the+first newline.+```console+$ password-cli check argon2 --hash-file password.hash+```
+ app/Main.hs view
@@ -0,0 +1,93 @@+{-# 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
+ app/Options.hs view
@@ -0,0 +1,199 @@+{-# 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"+ }
+ password-cli.cabal view
@@ -0,0 +1,42 @@+cabal-version: 3.0++name: password-cli+version: 0.1.1.0+category: CLI+synopsis: use password from your CLI+description: A simple CLI tool to interact with password+homepage: https://github.com/cdepillabout/password/tree/master/password-cli#readme+bug-reports: https://github.com/cdepillabout/password/issues+author: Dennis Gosnell, Felix Paulusma+maintainer: cdep.illabout@gmail.com, felix.paulusma@gmail.com+copyright: Copyright (c) Dennis Gosnell, 2019; Felix Paulusma, 2020+license: BSD-3-Clause+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ ChangeLog.md++source-repository head+ type: git+ location: https://github.com/cdepillabout/password+++executable password-cli+ -- type: exitcode-stdio-1.0+ main-is: Main.hs+ other-modules:+ Options+ Paths_password_cli+ autogen-modules:+ Paths_password_cli+ hs-source-dirs: app+ ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints+ build-depends:+ base >= 4.15 && < 5+ , bytestring >= 0.9 && < 1+ , password ^>= 3.0.3.0 || ^>= 3.1.0.0+ , password-types ^>= 1.0+ , optparse-applicative >= 0.14.3 && < 0.20+ , text >= 1.2 && < 3+ default-language: Haskell2010