Secure Password Hashing with Bcrypt in Erlang (and Nitrogen)

by jesse
2013-01-03

Introduction

Intended Audience: Erlang developers or Nitrogen developers who've made it through the tuturial. I take this a bit slow for make it easier for folks new to Erlang and Nitrogen. The proficient can probably scroll all the way down to the complete source code and get a feel for how this works.

When storing passwords, the only proper secure method is to run the passwords through some hashing algorithm. A hashing algorithm takes a string of characters or bytes and converts them into a new string which cannot be reversed into the original string. For years, MD5 and SHA were the common hashes used for storing passwords in web application, however due to significant improvements to hardware speed and parallelism, MD5 and SHA (even salted MD5 and SHA) have proven to be completely ineffective in the event of a password table leak.

The main problem with MD5 and SHA is, paradoxically, that they're just too fast, and in cryptography and hashing, fast is bad. Further, with recent high-profile sites leaking their hashed passwords only to have the majority of the passwords cracked within a week, it's more critical than ever to ensure for your users and customers that even their password hashes do leak (website hacked, or you have a laptop with a backup stolen, etc) they can rest assured that their password won't be recovered from the hash in any kind of timeframe that could be summarized as "soon." Indeed, with a proper hashing algorithm, a "common" password (ie, an English word) should take at least days to crack, and an "uncommon" (non-word) password should take years to crack.

So we turn our focus to bcrypt, a hashing algorithm that's intentionally designed to be slow. Further, you can define just how slow you want it to be by setting a "work factor", allowing you to scale the strength of the hash as hardware becomes ever faster. Currently, a work factor of 7 or 8 is generally sufficiently slow, but maybe 10 years down the road, upping the work factor to something like 15 or 20 will be necessary.

I won't go into any more detail about why you should use bcrypt, as someone else already did it better than I could anyway. There is a completely acceptable (and probably superior) alternative to bcrypt called scrypt, which is memory bound instead of CPU bound. I'm unaware, at the moment, of any Erlang bindings for scrypt.

Setting up bcrypt with Erlang

Adding bcrypt to an Erlang application is rather simple.

Add the Dependency

Add the erlang-bcrypt

{deps, [
	{bcrypt, "0.4.*", 
		{git, "git://github.com/smarkets/erlang-bcrypt.git",{tag, "0.4.1"}}}
]}

Start the necessary applications

Start the built-in crypto application, then start bcrypt. You can do this from the console:

application:start(crypto),
application:start(bcrypt)

More likely, however, you want this invoked during the startup of your application. The simple solution is to put it into the launching of the erlang vm:

erl -eval "application:start(crypto)" -eval "application:start(bcrypt)"

If you're running Nitrogen or another reltool generated release of an Erlang application, then you can edit the etc/vm.args file to include the -eval lines above and you'll be good to go.

Using bcrypt with Erlang

Once you've got bcrypt running, you can start hashing away to your heart's content.

First, generate a salt with the desired work factor (any positive integer), then hash your password.

Password = "my test password",
Workfactor = 10,
{ok, Salt} = bcrypt:gen_salt(Workfactor),
{ok, Hash} = bcrypt:hashpw(Password, Salt).

Then you can store hash in your user table (or login or whatever you call it). When the user then tries to log in, you can verify the password by rehashing the it (conveniently, because the hash happens to contain the salt, you can provide the previously hashed password as the Salt argument to the bcrypt:hashpw/2 function).

%% Load the original Password hash from the database
OriginalHash = db_user:get_password_hash(Username),

%% Hash the provided password with the original hash
{ok, NewHash} = bcrypt:hashpw(ProvidedPassword,OriginalHash),

%% Compare the New Hash against he old one. If they match, the password is the same
NewHash == OriginalHash.

That's the meat of using bcrypt with pure Erlang. Now that you know the basics, let's jump right into putting it to use in a Nitrogen application.

Incorporating bcrypt into your Nitrogen-based Webapp

For this, we'll be creating three modules:

  • login: Our page for logging in
  • register: Our page for creating an account
  • db_login: The database interface, and where we hash

Creating an Account

Before we can try to log in, we first need to create an account, so let's make a new page called /register.

We'll start with the registration form:

registration_form() ->
	[
		#label{text="Desired Username"},
		#textbox{id=username},
		#br{},
		#label{text="Desired Password"},
		#password{id=password},
		#br{},
		#label{text="Confirm Password"},
		#password{id=confirm},
		#br{},
		#button{id=registerbutton,text="Register",postback=register}
	].

We should also put in some validators to ensure the passwords match, and the username is specified:

registration_validators() ->
	%% Verify that the username is specified
	wf:wire(registerbutton, username, #validate{validators=[
		#is_required{text="Required"}}
	]),
	
	%% Verify that a password is present
	wf:wire(registerbutton, password, #validate{validators=[
		#is_required{text="Required"}
	]}),

	%% And verify that the password confirmation matches
	wf:wire(registerbutton, confirm, #validate{validators=[
		#confirm_password{password=password}
	]}).

Now, we need to actually create the account by handing the register postback:

event(register) ->
	%% Get the values from the form
	%% Notice that we don't need to worry about the 
	%% "confirm" field, since that's handled by the
	%% validator
	[Username,Password] = wf:mq([username,password]),

	%% Create the account
	db_login:create(Username, Password),

	%% Notify the user and redirect
	wf:wire(#alert{text="Account Created. Redirecting you to the login page"}),
	wf:redirect("/login").

You see here, we've referenced the db_login:create/2 function. Let's make this function here for demonstration purposes. Note, that we're going to use a module we make up called db which contains some function calls to use SQL. Obviously, you can use your own database, but for the sake of demonstration, we're going to use this db module and SQL.

We need to make the code for db_login:create/2, so let's jump right into that:

create(Username, Password) ->
	WorkFactor = 10,
	{ok, Salt} = bcrypt:gen_salt(WorkFactor),
	{ok, Hash} = bcrypt:hashpw(Password, Salt),
	SQL = "insert into user(username, pwhash) values(?,?)",
	db:insert(SQL, [Username, Hash]).

As you can see, we simply take the Password, hash it, and stick the hash alongside the Username into a user table in the database using our made-up db:insert call.

Did we forget something? Yup.

The observant reader would have noticed that we didn't even check to make sure that the username we're inserting doesn't already exist. While this is not necessarily something that demonstrates the use of bcrypt, it's something that should go into any decent registration form, so we'll do that here.

Let's reverse order here first, and let's make a function to quick check the database if a username exists. We'll do this by creating the function db_login:username_exists/1, which returns a boolean. It is defined as follows:

username_exists(Username) ->
	SQL = "select username from user where username=?",
	db:exists(SQL, [Username]).

(here, db:exists simply returns true if a record is returned, or false if not)

Now we want to add a new custom validator to the username field to make sure that we're not trying to add a user that already exists.

So let's modify the validator above (just the username part) as follows:

registration_validators() ->
	ValidateUser = fun(_Tag, Username) ->
		%% If the username exists, we want the validator to fail,
		%% so we return opposite of the return value
		not(db_login:username_exists(Username))
	end,

	wf:wire(registerbutton, username, #validate{validators=[
		%% Verify that the username is specified
		#is_required{text="Required"}},

		%% Verify that the username is available
		#custom{tag=ignore,text="Username Taken",function=ValidateUser}
	]),
	...

Logging Into Your System

Let's make an initial form for our page. In it, we also include a simple link to the Registration form we've created above.

login_form() ->
	[
		#label{text="Username"},
		#textbox{id=username},
		
		#label{text="Password"},
		#password{id=password},
		
		#br{},
		#button{id=loginbutton,postback=login},
		#link{url="/register",text="Create an Account"}
	].

And let's make an appropriate event for it, which will verify the login against the database.

event(login) ->
	%% Get the results from the form
	[Username,Password] = wf:mq([username,password]),

	%% Attempt our login
	case db_login:attempt_login(Username,Password) of
		fail -> 
			%% Our login failed, so let's show a message
			wf:flash("Invalid login information");
		success -> 
			%% Our login worked, so let's tell Nitrogen we're logged in,
			%% Flash a quick message, then redirect to the page we were at
			%% or "/" if there is none.
			wf:user(Username),
			wf:wire(#alert{text="Login Successful"}),
			wf:redirect_from_login("/")
	end.

So far, our login page is looking pretty simple. All we're doing is making a form, and getting the form values on login attempt, then verifying the provided info with db_login:attempt_login/2. From there, we do something depending on the result of the login attempt. If the attempt fails, let's show a message.

You'll notice bcrypt has not yet been seen. It's abstracted away in the db_login:attempt_login/2 function, which returns merely the atoms fail or success, and does nothing else.

Let's see how we would implement the db_login:attempt_login/2 function.

Note: I'm going to rely on an implied call db:first_record/2, which prepares and returns a SQL query from the database. You're free to use whichever database system you prefer. Note, this db module does not exist in Erlang, and is merely an abstraction for the purposes of demonstration.

attempt_login(Username, Password) ->
	%% Let's get the existing password hash from the database
	%% for the username specified
	SQL = "select pwhash from user where username=?"
	[ExistingHash] = db:first_record(SQL,[Username]),
	
	%% Hash the provided password with the original hash
	{ok, ProvidedHash} = bcrypt:hashpw(Password, ExistingHash),

	%% Compare the newly hashed password against the old one,
	%% returning the expected atoms depending on the comparison
	case ProvidedHash == ExistingHash of
		true -> success;
		false -> fail
	end.

Here we finally see bcrypt in use, and its use is simple. We've simply retrieved the existing hash from the database, rehashed the provided password, and compared the hashes.

Conclusion

As demonstrated, we've shown how to use the bcrypt application with Erlang. There is also an application out there that provides a "thin wrapper" for the Erlang bcrypt application called erlpass that is worth looking into. I leave converting the code above to erlpass as an exercise for the reader.

Follow-up

The hypothetical db module referenced throughout this article is a variation of one that I use for my own projects, and serves as a basic wrapper for the erlang mysql driver, along with a simple method of preparing SQL statements. I will eventually be writing an article about it, as well as releasing the source code for it on Github.

The complete modules:

Here are the complete modules for your coding convenience.

login.erl

-module(login).
-compile(export_all).
-include_lib("nitrogen_core/include/wf.hrl").

main() -> #template{file="./site/templates/bare.html"}.

title() -> "Login".

body() -> login_form().

login_form() ->
	[
		#label{text="Username"},
		#textbox{id=username},
		
		#label{text="Password"},
		#password{id=password},
		
		#br{},
		#button{id=loginbutton, postback=login, text="Login"},
		#link{url="/register", text="Create an Account"}
	].

event(login) ->
	%% Get the results from the form
	[Username,Password] = wf:mq([username,password]),

	%% Attempt our login
	case db_login:attempt_login(Username,Password) of
		fail -> 
			wf:flash("Invalid login information");
		success -> 
			wf:user(Username),
			wf:wire(#alert{text="Login Successful"}),
			wf:redirect_from_login("/")
	end.

register.erl

-module(register).
-compile(export_all).
-include_lib("nitrogen_core/include/wf.hrl").

main() -> #template{file="./site/templates/bare.html"}.

title() -> "Create an Account".

body() ->
	registration_validators(),
	regitration_form().

registration_form() ->
	[
		#label{text="Desired Username"},
		#textbox{id=username},
		#br{},
		#label{text="Desired Password"},
		#password{id=password},
		#br{},
		#label{text="Confirm Password"},
		#password{id=confirm},
		#br{},
		#button{id=registerbutton,text="Register",postback=register}
	].

registration_validators() ->
	ValidateUser = fun(_Tag, Username) ->
		not(db_login:username_exists(Username))
	end,

	wf:wire(registerbutton, username, #validate{validators=[
		#is_required{text="Required"}},
		#custom{tag=ignore,text="Username Taken",function=ValidateUser}
	]),
	
	wf:wire(registerbutton, password, #validate{validators=[
		#is_required{text="Required"}
	]}),

	wf:wire(registerbutton, confirm, #validate{validators=[
		#confirm_password{password=password}
	]}).

event(register) ->
	[Username,Password] = wf:mq([username,password]),

	db_login:create(Username, Password),

	wf:wire(#alert{text="Account Created. Redirecting you to the login page"}),
	wf:redirect("/login").

db_login.erl

-module(db_login).
-export([
	create/2,
	attempt_login/2,
	username_exists/1
]).

create(Username, Password) ->
	WorkFactor = 10,
	{ok, Salt} = bcrypt:gen_salt(WorkFactor),
	{ok, Hash} = bcrypt:hashpw(Password, Salt),
	SQL = "insert into user(username, pwhash) values(?,?)",
	db:insert(SQL, [Username, Hash]).


attempt_login(Username, Password) ->
	SQL = "select pwhash from user where username=?"
	[ExistingHash] = db:first_record(SQL,[Username]),
	
	{ok, ProvidedHash} = bcrypt:hashpw(Password, ExistingHash),

	case ProvidedHash == ExistingHash of
		true -> success;
		false -> fail
	end.

username_exists(Username) ->
	SQL = "select username from user where username=?",
	db:exists(SQL, [Username]).