packages feed

fernet-0.1.0.0: cli/FernetMain.hs

module Main where

import Options.Applicative
import Data.Monoid ((<>))
import Control.Monad (join, unless, when)
import Data.Time.Clock (NominalDiffTime)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy.Char8 as L8
import qualified Data.ByteArray.Encoding as B (Base(..), convertFromBase)
import Text.Read (readEither)
import System.IO
import Data.Bifunctor (first)
import System.Posix.IO (handleToFd, stdInput)
import System.Posix.Terminal (queryTerminal)

import Network.Fernet

main :: IO ()
main = join . execParser $
  info (helper <*> parser)
  (  fullDesc
  <> header "Fernet Utility"
  <> progDesc (
      "Encrypts/decrypts Fernet tokens. " ++
      "One token written to stdout for each line read from stdin. " ++
      "Use --gen-key to make a key."
  ))
  where
    parser :: Parser (IO ())
    parser =
      (fernet
        <$> (KeyText <$> ( strOption
              ( long "key"
                <> short 'k'
                <> metavar "STRING"
                <> help "Base64-urlsafe-encoded 32 byte encryption key"
              )) <|>
              KeyFile <$> ( strOption
                ( long "key-file"
                  <> metavar "FILENAME"
                  <> help "File containing the encryption key"
                )
              ))
        <*> ( optional
              ( flag' Encrypt
                ( long "encrypt"
                  <> short 'e'
                  <> help "Encryption mode (default: autodetect)" )
              <|>
              flag' Decrypt
                ( long "decrypt"
                  <> short 'd'
                  <> help "Decryption mode (default: autodetect)" )
              )
            )
        <*> option ttl
            ( long "ttl"
            <> metavar "SECONDS"
            <> help "Token lifetime in seconds (default: 1 minute)"
            <> value 60
            )) <|>
      (genKey <$> ( flag' True
                    (long "gen-key"
                    <> short 'g'
                    <> help "Generate a key from the password on standard input"
                    )))

genKey :: Bool -> IO ()
genKey _ = do
  password <- askPassword
  (k, _) <- generateKeyFromPassword iterations password
  S8.hPutStrLn stdout (keyToBase64 k)

askPassword ::  IO ByteString
askPassword = do
  isatty <- queryTerminal stdInput
  when isatty $ do
    hSetEcho stdin False
    S8.hPutStr stderr "Enter password: "
    hFlush stdout
  password <- S8.hGetLine stdin
  when isatty $ do
    hSetEcho stdin True
    S8.hPut stderr "\n"
  return password

ttl :: ReadM NominalDiffTime
ttl = eitherReader (fmap fromInteger . readEither)

data Action = Encrypt | Decrypt
data Keys = KeyText String | KeyFile FilePath
data Password = PasswordText String | PasswordFile FilePath

fernet :: Keys -> Maybe Action -> NominalDiffTime -> IO ()
fernet ks ax ttl = do
  k <- readKeys ks
  L8.hGetContents stdin >>= mapM_ (processLine k ax ttl) . L8.lines

processLine :: Key -> Maybe Action -> NominalDiffTime -> L8.ByteString -> IO ()
processLine k ax ttl s = doLine k ax ttl s >>= uncurry L8.hPutStrLn . output

output :: Either String ByteString -> (Handle, L8.ByteString)
output (Left e)  = (stderr, L8.pack e)
output (Right s) = (stdout, L8.fromStrict s)

doLine :: Key -> Maybe Action -> NominalDiffTime -> L8.ByteString -> IO (Either String ByteString)
doLine k (Just Encrypt) _   s = lineEncrypt k s
doLine k (Just Decrypt) ttl s = lineDecrypt k ttl s
doLine k Nothing        ttl s = doLine k (Just $ sniff s) ttl s

sniff :: BL.ByteString -> Action
sniff s | ver >= "gA" && ver <= "gP" = Decrypt
        | otherwise                  = Encrypt
  where ver = BL.take 2 s

lineEncrypt :: Key -> L8.ByteString -> IO (Either String ByteString)
lineEncrypt k s = Right <$> encrypt k (L8.toStrict s)

lineDecrypt :: Key -> NominalDiffTime -> L8.ByteString -> IO (Either String ByteString)
lineDecrypt k ttl s = first show <$> decrypt k ttl (L8.toStrict s)

readKeys :: Keys -> IO Key
readKeys (KeyText k) = keyFromString k
readKeys (KeyFile f) = readFirstLine f >>= \k -> readKeys (KeyText k)

readPassword :: Password -> IO Key
readPassword (PasswordText p) = fst <$> generateKeyFromPassword iterations (S8.pack p)
readPassword (PasswordFile f) = readFirstLine f >>= \p -> readPassword (PasswordText p)

iterations = 100000 :: Int

readFirstLine :: FilePath -> IO String
readFirstLine f = withFile f ReadMode hGetLine

keyFromString :: String -> IO Key
keyFromString s = case keyFromBase64 (S8.pack s) of
                    Right k -> return k
                    Left e -> fail e

-- | Converts 'Maybe' to 'Either'.
justRight :: e -> Maybe a -> Either e a
justRight _ (Just a) = Right a
justRight e Nothing = Left e