Protecting passwords for fun and profit

Last time, we looked at a simple way to use Happstack to store a bunch of user names and passwords. In fact, it was a bit too simple — an attacker with access to the disk we’re storing the data on can trivially recover each user’s password! How can this be, when we were so careful to only store hashes of passwords in our user directory, instead of the passwords themselves?

Before I get to that, let’s spend a modicum of time refactoring the code we’re working with into two modules: one that implements our user directory and one that uses our user directory. The Users module will only expose the interface for the operations we’re providing, hiding the implementation details:

module Users ( UserDirectory
             , AddUser (..)
             , CheckPassword (..)
             , ListUsers (..)
             ) where

Here, the interface only consists of the queries and updates that clients can make, as well as the top-level type of the data being saved in the MACID store. The Main module implements our command loop and imports Users to do the actual work:

module Main ( main
            ) where
 
import Users

One last change: to make it easier for successive versions of the program to share the same files backing our MACID store, we’ll explicitly set the name of the program — Happstack.State names the directory where it stores the data according to this name:

main :: IO ()
main = withProgName "state-demo" $ do
    state <- startSystemState macidProxy
    commandLoop state

For convenience, here’s a tarball containing the complete program. Let’s give it a try:

> list
> add pmk swordfish
User added
> list
pmk (joined Sat Apr 11 14:57:12 EDT 2009)
> add cowboy sourMilk7
User added
> list
cowboy (joined Sat Apr 11 14:57:28 EDT 2009)
pmk (joined Sat Apr 11 14:57:12 EDT 2009)
> login cowboy swordfish
Bad account or password
> login pmk whatever
Bad account or password
> login pmk swordfish
Success
> checkpoint
> quit

Since we named our program state-demo, Happstack.State stores its files in the directory _local/state-demo_state/ under the current directory. If we look at the checkpoint we made, which will contain the contents of the UserDirectory we created, we can see there aren’t any passwords in it, just as we expected:

0000000: 0000 0000 0000 0001 0000 0000 0000 0013  ................
0000010: 5573 6572 732e 5573 6572 4469 7265 6374  Users.UserDirect
0000020: 6f72 7900 0000 0000 0001 8100 0000 0000  ory.............
0000030: 0000 0000 0000 0000 0000 0004 a57b 6cd0  .............{l.
0000040: 6cbc 5d4d 0000 0120 968a e2ad 0000 0000  l.]M... ........
0000050: 0000 0000 0000 0000 0000 0014 3139 3238  ............1928
0000060: 3338 3631 3537 2035 3739 3432 3634 3530  386157 579426450
0000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000080: 0200 0000 0000 0000 0663 6f77 626f 7900  .........cowboy.
0000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000a0: 0000 0000 0000 0000 402f 2709 fa39 c1a1  ........@/'..9..
00000b0: 7f91 1c05 4ae1 f1b3 3260 e5ee 28b8 1963  ....J...2`..(..c
00000c0: a455 122d f04a 8235 1147 fe5c 92a3 0b4f  .U.-.J.5.G.\...O
00000d0: 7540 0754 3be1 939f 2d66 272f 0f09 ffdf  u@.T;...-f'/....
00000e0: 8c9d 05e2 3b8e b5b4 db00 0000 0000 0000  ....;...........
00000f0: 0a23 cde5 ac26 17c4 6019 a700 0000 0000  .#...&..`.......
0000100: 0000 0000 0049 e0e8 1801 0100 0000 0000  .....I..........
0000110: 0000 0500 3cb8 192e 0000 0000 0000 0003  ....<...........
0000120: 706d 6b00 0000 0000 0000 0000 0000 0000  pmk.............
0000130: 0000 0000 0000 0000 0000 0000 4035 6924  ............@5i$
0000140: cb51 f37b f7ae af8d 1953 6d44 e90b 5d4a  .Q.{.....SmD..]J
0000150: 200d 2925 8e2d 4ed7 9aa4 7b59 9d47 ed5d   .)%.-N...{Y.G.]
0000160: 8626 bef6 1e8d 6e9b 1bf7 7689 daeb facb  .&....n...v.....
0000170: a9c0 ab09 ae76 272c 3b26 ce22 de00 0000  .....v',;&."....
0000180: 0000 0000 0af1 4a1c a2dd ef72 1ea8 8e00  ......J....r....
0000190: 0000 0000 0000 0000 0049 e0e8 0801 0100  .........I......
00001a0: 0000 0000 0000 0500 9c69 30dd            .........i0.

However, when we look at the transaction log from before we made the checkpoint, we’re in for a nasty surprise:

0000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000010: 0000 0000 0000 0000 023f cf48 bc4d aa07  .........?.H.M..
0000020: 1400 0001 2096 8a62 f600 0000 0000 0000  .... ..b........
0000030: 0000 0000 0000 0000 1531 3438 3835 3032  .........1488502
0000040: 3432 3820 3138 3234 3336 3735 3531 0000  428 1824367551..
0000050: 0000 0000 0000 0000 0000 0000 000d 5573  ..............Us
0000060: 6572 732e 4164 6455 7365 7200 0000 0000  ers.AddUser.....
0000070: 0000 2400 0000 0000 0000 0000 0000 0000  ..$.............
0000080: 0000 0370 6d6b 0000 0000 0000 0009 7377  ...pmk........sw
0000090: 6f72 6466 6973 6800 0000 0000 0000 0000  ordfish.........
00000a0: 0000 0000 0000 0000 0000 0000 0000 0004  ................
00000b0: a57b 6cd0 6cbc 5d4d 0000 0120 968a 9e86  .{l.l.]M... ....
00000c0: 0000 0000 0000 0000 0000 0000 0000 0014  ................
00000d0: 3139 3238 3338 3631 3537 2035 3739 3432  1928386157 57942
00000e0: 3634 3530 0000 0000 0000 0000 0000 0000  6450............
00000f0: 0000 000d 5573 6572 732e 4164 6455 7365  ....Users.AddUse
0000100: 7200 0000 0000 0000 2700 0000 0000 0000  r.......'.......
0000110: 0000 0000 0000 0000 0663 6f77 626f 7900  .........cowboy.
0000120: 0000 0000 0000 0973 6f75 724d 696c 6b37  .......sourMilk7

The passwords, in the clear where anyone can see them! What happened?

In retrospect, it’s obvious. Between checkpoints, Happstack.State saves a transaction log of all updates, so that if the program quits prematurely it can recover and not lose any data. While we were careful to hash the passwords in the MACID store itself, the AddUser update operation didn’t, so when those are saved to the transaction log, so are the passwords.

The moral is this: Never pass any arguments to an update that you don’t mind being written to disk.

Now that we’ve learned our lesson, let’s rewrite addUser so that it takes a PasswordHash as an argument, instead of the password itself:

addUser :: MonadState UserDirectory m => String -> PasswordHash -> ClockTime -> m Bool
addUser name passHash when = do
    UserDirectory dir <- get
    if M.member name dir
        then return False
        else do put $ UserDirectory $ M.insert name (UserInfo passHash when) dir
                return True

That’ll work, but we’d rather not force our client to know how passwords get hashed — PasswordHash should be an implementation detail, not part of the interface. So let’s make a function that hashes the password, and then calls the update:

createUser :: MonadIO m => String -> String -> m Bool
createUser name pass = do
    salt <- liftIO newSalt
    now <- liftIO getClockTime
    update $ AddUser name (hashPassword salt pass) now

But wait, since createUser is taking the password as an argument, doesn’t it have the same saving-passwords-to-the-transaction-log flaw that we just tried to fix in AddUser? No, because createUser isn’t an update operation — it’s just a function that calls the update operation. More specifically, it sets up the arguments needed for AddUser (hashing the password and getting the current time), and then invokes AddUser with those arguments. Those are the arguments that get saved to the transaction log, not the ones that createUser gets.

If you aren’t convinced, look at the type signature for createUser again, as compared to the one for addUser:

createUser :: MonadIO m => String -> String -> m Bool
 
addUser :: MonadState UserDirectory m => String -> PasswordHash -> ClockTime -> m Bool

addUser operates in a monad that implements MonadState UserDirectory, which is what makes it an update operation. Our original code used Update UserDirectory as the monad, which is the concrete type of the monad that Happstack.State uses. Really, though, our code doesn’t care what the specific monad is, as long as it lets us read and write a state value of type UserDirectory, which is what MonadState UserDirectory guarantees.

createUser, on the other hand, operates in a monad that implements MonadIO, a generalization of the IO monad that lets Haskell programs interact with the outside world. This is why createUser can call IO operations like getClockTime, which would be illegal inside a query or update, since the Update UserDirectory monad doesn’t implement MonadIO.

And if you still don’t believe me, observe that we aren’t creating any query or update operations using the template deep magic:

$(mkMethods ''UserDirectory ['addUser, 'checkPassword, 'listUsers])

See? createUser doesn’t appear anywhere in there; it’s just an ordinary function.

Since we want clients to call createUser instead of using AddUser, let’s change the list of exports from our module:

module Users ( UserDirectory
             , createUser
             , CheckPassword (..)
             , ListUsers (..)
             ) where

And, since we changed the interface, we’ll need to update part of the Main module that uses it:

-- inside commandLoop:
      processCommand state ["add", user, pass] = do
                success <- createUser user pass
                putStrLn $ if success then "User added" else "User already exists"
                commandLoop state

Clients can be blissfully unaware of the hashing that’s going on behind the scenes inside createUser.

Here’s a tarball of version 2 of our program, with all the changes above (and a few others) applied to it. Starting with the same state from version 1, let’s try creating a new user and prove once and for all we’ve abolished plaintext passwords:

> list
cowboy (joined Sat Apr 11 14:57:28 EDT 2009)
pmk (joined Sat Apr 11 14:57:12 EDT 2009)
> add bobby nowsecure
User added
> list
bobby (joined Sat Apr 11 15:31:54 EDT 2009)
cowboy (joined Sat Apr 11 14:57:28 EDT 2009)
pmk (joined Sat Apr 11 14:57:12 EDT 2009)
> login bobby nowsecure
Success
> checkpoint
> quit

And the transaction log from this session:

0000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000010: 0000 0000 0000 0000 0656 a5f9 68e9 e949  .........V..h..I
0000020: 8300 0001 2096 aa27 f300 0000 0000 0000  .... ..'........
0000030: 0000 0000 0000 0000 1531 3839 3231 3631  .........1892161
0000040: 3433 3820 3230 3531 3836 3033 3930 0000  438 2051860390..
0000050: 0000 0000 0000 0000 0000 0000 000d 5573  ..............Us
0000060: 6572 732e 4164 6455 7365 7200 0000 0000  ers.AddUser.....
0000070: 0000 9500 0000 0000 0000 0000 0000 0000  ................
0000080: 0000 0562 6f62 6279 0000 0000 0000 0000  ...bobby........
0000090: 0000 0000 0000 0000 40c4 a004 6a7d 0468  ........@...j}.h
00000a0: dd6a aef6 f047 b48d fe4d 03b9 6d42 daf2  .j...G...M..mB..
00000b0: d647 fe57 ba4a c04a c67d a008 5abc 44fe  .G.W.J.J.}..Z.D.
00000c0: 1ee0 d21e 7140 e619 7a13 db8c 0060 4ee2  ....q@..z....`N.
00000d0: 6d05 dd16 7f0f 4493 0000 0000 0000 0000  m.....D.........
00000e0: 0a36 b95d 8dd4 1919 9fcf c200 0000 0000  .6.]............
00000f0: 0000 0000 0049 e0f0 2a01 0100 0000 0000  .....I..*.......
0000100: 0000 0540 e032 c9e7                      ...@.2..

Success! We can clearly see the AddUser for bobby, but can’t see his password. Even the transaction log only stores a hash of the password.

By the way, if you’re wondering why we never had to make any changes to checkPassword, which also takes a plaintext password as an argument, that’s because checkPassword is a query, and queries don’t get saved to the transaction log. That’s because queries are guaranteed not to change the contents of the MACID store, and thus can be ignored for recovery purposes. It’s safe to pass sensitive information as the arguments to a query, but not to an update.

Unfortunately, the no-sensitive-information-in-updates rule is something I only learned after writing The Button, so if you signed up for an account, the password you chose was exposed in the transaction log. I’ve taken the liberty of wiping those files, but in the unlikely event that you specified a password you use for other accounts too, I highly suggest you change those passwords, unless for some reason you trust me not to do anything nefarious with the passwords I at one time had access to.

Of course, there are still ways we could improve our program’s security, namely by enforcing that users pick strong passwords and preventing online brute force attacks. That will lead us into exploring the data migration features of Happstack.State. Stay tuned.

2 Responses

  1. > add cowboy sourMilk7
    Plus epic amounts of win
    …that is all.

  2. Well, it is the password you use for everything, right?

Comments are closed.