packages feed

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 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`++[![Build Status](https://github.com/cdepillabout/password/workflows/password/badge.svg)](http://github.com/cdepillabout/password)+[![Hackage](https://img.shields.io/hackage/v/password-cli.svg)](https://hackage.haskell.org/package/password-cli)+[![Stackage LTS](http://stackage.org/package/password-cli/badge/lts)](http://stackage.org/lts/package/password-cli)+[![Stackage Nightly](http://stackage.org/package/password-cli/badge/nightly)](http://stackage.org/nightly/package/password-cli)+[![BSD3 license](https://img.shields.io/badge/license-BSD3-blue.svg)](./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