otp-authenticator-0.1.2.0: src/Authenticator/Options.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
-- |
-- Module : Authenticator.Options
-- Description : Options for the @otp-auth@ executable
-- Copyright : (c) Justin Le 2017
-- License : MIT
-- Maintainer : justin@jle.im
-- Stability : unstable
-- Portability : portable
--
-- Load options for the @otp-auth@ executable.
module Authenticator.Options
( Cmd (..),
DumpType (..),
getOptions,
)
where
import Authenticator.Common
import Control.Exception
import Control.Monad
import qualified Crypto.Gpgme as G
import qualified Data.Aeson as J
import qualified Data.ByteString as BS
import Data.Functor
import Data.Maybe
import Data.Monoid
import Data.String
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Yaml as Y
import GHC.Generics (Generic)
import Options.Applicative hiding (str)
import qualified Options.Applicative as O
import qualified System.Console.Haskeline as L
import System.FilePath
import System.IO
import System.IO.Error
import System.Posix.User
import Text.Printf
import Prelude hiding (filter)
-- | Should the data dump be yaml, or json?
data DumpType = DTYaml | DTJSON
-- | A command to exercute. See "Authenticator.Actions".
data Cmd
= Add Bool
| View Bool Bool (Either Int (Maybe T.Text, Maybe T.Text))
| Gen Int
| Edit Int
| Delete Int
| Dump DumpType
data Opts = Opts
{ oFingerprint :: Maybe BS.ByteString,
oVault :: Maybe FilePath,
oConfig :: Maybe FilePath,
oGPG :: FilePath,
oEchoPass :: Bool,
oCmd :: Cmd
}
data Config = Conf
{ cFingerprint :: Maybe T.Text,
cVault :: Maybe FilePath
}
deriving (Generic)
confJsonOpts :: J.Options
confJsonOpts =
J.defaultOptions
{ J.fieldLabelModifier = J.camelTo2 '-' . drop 1
}
instance J.FromJSON Config where
parseJSON = J.genericParseJSON confJsonOpts
instance J.ToJSON Config where
toEncoding = J.genericToEncoding confJsonOpts
toJSON = J.genericToJSON confJsonOpts
parseOpts :: Parser Opts
parseOpts =
Opts
<$> optional
( option
str
( long "fingerprint"
<> short 'p'
<> metavar "KEY"
<> help "Fingerprint of key to use"
)
)
<*> option
(Just <$> str)
( long "vault"
<> short 'v'
<> metavar "PATH"
<> value Nothing
<> showDefaultWith (const "~/.otp-auth.vault")
<> help "Location of vault"
)
<*> option
(Just <$> str)
( long "config"
<> short 'c'
<> metavar "PATH"
<> value Nothing
<> showDefaultWith (const "~/.otp-auth.yaml")
<> help "Location of configuration file"
)
<*> strOption
( long "gnupg"
<> short 'g'
<> metavar "PATH"
<> value "~/.gnupg"
<> showDefaultWith id
<> help ".gnupg file"
)
<*> switch
( long "echo"
<> short 'e'
<> help "Visible (echoing) password entry mode"
)
<*> subparser
( command
"add"
( info
(parseAdd <**> helper)
(progDesc "Add an OTP key")
)
<> command
"view"
( info
(parseView <**> helper)
(progDesc "View keys (with optional filter or specific ID)")
)
<> command
"gen"
( info
(parseGen <**> helper)
(progDesc "Generate code for specific key #, for use with counter-based keys.")
)
<> command
"edit"
( info
(parseEdit <**> helper)
(progDesc "Edit a key")
)
<> command
"delete"
( info
(parseDelete <**> helper)
(progDesc "Delete a key")
)
<> command
"dump"
( info
(parseDump <**> helper)
(progDesc "Dump all data as json")
)
)
where
parseAdd =
Add
<$> switch
( long "uri"
<> short 'u'
<> help "Enter account using secret URI (from QR Code)"
)
parseView =
View
<$> switch
( long "list"
<> short 'l'
<> help "Only list accounts; do not generate any keys."
)
<*> switch
( long "json"
<> short 'j'
<> help "Output as json."
)
<*> ( Left
<$> argument
auto
( metavar "ID"
<> help "Specific ID number of account"
)
<|> Right
<$> ( (,)
<$> optional
( option
str
( long "account"
<> short 'a'
<> metavar "NAME"
<> help "Optional filter by account"
)
)
<*> optional
( option
str
( long "issuer"
<> short 'i'
<> metavar "SITE"
<> help "Optional filter by issuer"
)
)
)
)
parseGen =
Gen
<$> argument
auto
( metavar "ID"
<> help "ID number of account"
)
parseEdit =
Edit
<$> argument
auto
( metavar "ID"
<> help "ID number of account"
)
parseDelete =
Delete
<$> argument
auto
( metavar "ID"
<> help "ID number of account"
)
parseDump =
Dump
<$> flag
DTJSON
DTYaml
( long "yaml"
<> short 'y'
<> help "Yaml output"
)
-- | Return command, visible password entry, vault filepath, and fingerprint
getOptions :: IO (Cmd, Bool, FilePath, Maybe G.Fpr)
getOptions = do
Opts {..} <-
execParser $
info
(parseOpts <**> helper)
( fullDesc
<> progDesc "OTP Viewer"
<> header "otp-authenticator: authenticate me, cap'n"
)
oConfig' <- case oConfig of
Just fp -> return fp
Nothing -> do
ue <- getUserEntryForID =<< getEffectiveUserID
return $ homeDirectory ue </> ".otp-auth.yaml"
(Conf {..}, mkNewConf) <- do
(c0, mkNew) <-
fmap (,False) (Y.decodeFileEither @Config oConfig') `catch` \e ->
if isDoesNotExistError e
then return (Right (Conf Nothing Nothing), True)
else throwIO e
case c0 of
Left e -> do
hPutStrLn stderr "Could not parse configuration file. Ignoring."
hPutStrLn stderr . Y.prettyPrintParseException $ e
return (Conf Nothing Nothing, False)
Right c1 -> return (c1, mkNew)
vault <- case oVault <|> cVault of
Just fp -> return fp
Nothing -> do
ue <- getUserEntryForID =<< getEffectiveUserID
return $ homeDirectory ue </> ".otp-auth.vault"
cFingerprint' <-
if mkNewConf
then do
printf "Config file not found; generating default file at %s\n" oConfig'
fing <- case oFingerprint of
Just p -> return $ Just (T.decodeUtf8 p)
Nothing -> L.runInputT hlSettings $ fmap T.pack <$> L.getInputLine "Fingerprint? "
Y.encodeFile oConfig' $ Conf fing (Just vault)
return fing
else return cFingerprint
let fingerprint = oFingerprint <|> (T.encodeUtf8 <$> cFingerprint')
return (oCmd, oEchoPass, vault, fingerprint)
-- | is str from optparse-applicative 0.14 and above
str :: (IsString s) => ReadM s
str = fromString <$> O.str