Prompting for a password in Haskell command line application
Asked Answered
E

6

29

The following Haskell program prompts the user for a password in the terminal and continues if he has entered the correct one:

main = do
    putStrLn "Password:"
    password <- getLine

    case hash password `member` database of
        False -> putStrLn "Unauthorized use!"
        True  -> do
                 ...

Unfortunately, the password will appear on the screen as the user types it, which I want to avoid.

How can I read a sequence of characters that the users types without having the show up on the screen? What is the equivalent of getLine for this purpose?

I'm on MacOS X, but I would like this to work on Windows and Linux, too.

Expressionism answered 31/10, 2010 at 18:9 Comment(0)
L
40

Do this:

module Main
where

import System.IO
import Control.Exception

main :: IO ()
main = getPassword >>= putStrLn . ("Entered: " ++)

getPassword :: IO String
getPassword = do
  putStr "Password: "
  hFlush stdout
  pass <- withEcho False getLine
  putChar '\n'
  return pass

withEcho :: Bool -> IO a -> IO a
withEcho echo action = do
  old <- hGetEcho stdin
  bracket_ (hSetEcho stdin echo) (hSetEcho stdin old) action
Lesson answered 31/10, 2010 at 18:31 Comment(0)
V
15

There is a getPassword in System.Console.Haskeline. Probably it's an overkill for your case but someone may find it useful.

An example:

> runInputT defaultSettings $ do {p <- getPassword (Just '*') "pass:"; outputStrLn $ fromJust p}
pass:***
asd
Viewy answered 1/11, 2010 at 6:47 Comment(3)
Any idea why all the Haskeline functions return a Maybe String rather than a String? There's no documentation on it, and it seems to me that it should always return String, like standard getLine.Prothrombin
That's because the standard getLine is a partial function, that will raise exception when receiving an EOT. haskeline will instead return Nothing, allowing you to do something different when you got an empty line, compared to ^DIntertwine
On MSYS2(Windows), hSetEcho doesn't work but this does. Thank you.Monkeypot
N
9

It is possible to disable echoing in the terminal with the System.Posix.Terminal module. However, this requires POSIX support, so may not work on Windows (I didn't check).

import System.Posix.Terminal 
import System.Posix.IO (stdInput)

getPassword :: IO String
getPassword = do
    tc <- getTerminalAttributes stdInput
    setTerminalAttributes stdInput (withoutMode tc EnableEcho) Immediately
    password <- getLine
    setTerminalAttributes stdInput tc Immediately
    return password

main = do
    putStrLn "Password:"
    password <- getPassword
    putStrLn "Name:"
    name <- getLine
    putStrLn $ "Your password is " ++ password ++ " and your name is " ++ name

Note that the stdin is line-buffered, so if you use putStr "Password:" instead of putStrLn, you need to flush the buffer first, otherwise the prompt will be inhibited also.

Neutralization answered 31/10, 2010 at 18:24 Comment(2)
Nice, have an upvote! Looks like the humble hSetEcho from System.IO will do the trick, though. :-)Expressionism
@Heinrich: Ah. Too humble to be noticed :PNeutralization
R
7

withEcho can be written with a little less noise:

withEcho :: Bool -> IO a -> IO a
withEcho echo action =
    bracket (hGetEcho stdin)
            (hSetEcho stdin)
            (const $ hSetEcho stdin echo >> action)
Residuum answered 15/11, 2012 at 23:31 Comment(1)
I think this is a bit too clever for me, I have to look quite hard to figure out how this works. A better fix may be to factor out the 'run action with temporarily changed setting' behaviour into something like withTemp :: a -> IO a -> (a -> IO b) -> IO c -> IO c, withTemp tempValue getter setter action = ... so that you can define withEcho echo = withTemp echo (hGetEcho stdin) (hSetEcho stdin).Crawfish
S
4

As I commented above, I suggest you use haskeline, which is a full prompt library. I've used it happily for LambdaCalculator with no complaints.

Stephine answered 1/11, 2010 at 17:6 Comment(2)
Any idea why all the Haskeline functions return a Maybe String rather than a String? There's no documentation on it, and it seems to me that it should always return String, like standard getLine.Prothrombin
@Prothrombin It isn't immediately obvious to me, no. It would take some digging in the source.Stephine
K
1

I have found this useful when reading passwords:

import Control.Exception
import System.IO

withoutEcho :: IO a -> IO a
withoutEcho action =
  finally (hSetEcho stdin False >> action) (hSetEcho stdin True)
Keck answered 11/6, 2020 at 4:27 Comment(1)
For example Emacs shell will re-echo input with withEcho True.Keck

© 2022 - 2024 — McMap. All rights reserved.