packages feed

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