secure-memory-0.0.0.1: lib/Data/SensitiveBytes/IO/Internal/Password.hs
-- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
--
-- SPDX-License-Identifier: MPL-2.0
{-# LANGUAGE CPP #-}
{-# LANGUAGE InterruptibleFFI #-}
-- | Internal utilities for reading passwords.
module Data.SensitiveBytes.IO.Internal.Password
( readPassword
) where
import Control.Exception.Safe (MonadMask, bracket)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Text (Text)
import qualified Data.Text.IO as T
import Foreign.C.Error (eILSEQ, getErrno)
import Foreign.C.Types (CInt (..))
import Foreign.Ptr (Ptr)
import System.IO (Handle, hFlush)
#if defined(mingw32_HOST_OS)
#else
import Data.Coerce (coerce)
import System.Posix.IO (handleToFd)
import System.Posix.Types (Fd (Fd))
import qualified System.Posix.Terminal as Term
#endif
foreign import ccall interruptible "readline_max"
c_readLineMax :: CInt -> Ptr () -> CInt -> IO CInt
-- | A quick wrapper around the C function that turns the Haskell IO
-- 'Handle' into a system-dependent handle/fd.
readLineMax :: Handle -> Ptr () -> CInt -> IO CInt
#if defined(mingw32_HOST_OS)
readLineMax _ bufPtr maxLength = do
c_readLineMax 0 bufPtr maxLength
#else
readLineMax hIn bufPtr maxLength = do
fdIn <- handleToFd hIn
c_readLineMax (coerce fdIn) bufPtr maxLength
#endif
-- | Flush stdout, disable echo, and read user input from stdin.
readPassword
:: Handle -- ^ Input file handle.
-> Handle -- ^ Output file handle.
-> Text -- ^ Prompt.
-> Ptr () -- ^ Target buffer.
-> Int -- ^ Target buffer size.
-> IO Int
readPassword hIn hOut prompt bufPtr allocSize = do
T.hPutStr hOut prompt
withEchoDisabled hIn $ do
hFlush hOut -- need to flush _after_ echo is disabled
-- TODO: Do we also want to install signal handlers?
res <- readLineMax hIn bufPtr (fromIntegral allocSize)
if res >= 0
then do
T.hPutStrLn hOut ""
pure $ fromIntegral res
else do
errno <- getErrno
-- TODO: Maybe return a Maybe or throw a proper exception?
case res of
-1 -> do
if errno == eILSEQ
then error "readPassword: locale/terminal misconfiguration"
else error "readPassword: read error"
_ -> error $ "readPassword: impossible error happened: " <> show res
-- | Run an action with terminal echo off (and then restore it).
withEchoDisabled :: (MonadIO m, MonadMask m) => Handle -> m r -> m r
#if defined(mingw32_HOST_OS)
withEchoDisabled _ = id -- on Windows our @c_readLineMax@ does not echo anyway
#else
withEchoDisabled hIn act = do
fin <- liftIO $ handleToFd hIn
liftIO (Term.queryTerminal fin) >>= \case
False -> act
True -> do
attrs <- liftIO $ Term.getTerminalAttributes fin
let attrsNoEcho = Term.withoutMode attrs Term.EnableEcho
bracket
(liftIO $ Term.setTerminalAttributes fin attrsNoEcho Term.WhenFlushed)
(\_ -> liftIO $ Term.setTerminalAttributes fin attrs Term.Immediately)
(\_ -> act)
#endif