The end of unsecure authentications

When I’m logging up on the web, on unsecure connections, I’m always thinking that anybody can discover my password. The attacker just needs to listen to the network using some Ethereal-like software.

One solution is to use the HTTPS protocol instead of HTTP. Unfortunately too few web sites use it because it requires an SSL certificate.

To get a certificate you have two possibilities. You can buy a certificate or generate one for yourself. Even if this isn’t really hard, too many web sites don’t encrypt passwords between the client and the server. We need a solution which doesn’t include HTTPS’ constraints. Here is an answer which doesn’t need any configuration one the web server.

First we have to admit that good web sites don’t store passwords in plain text in their database. Passwords have to be stored as a hash sum mixed with a unique seed to each user. It should look like this.

password_sum = md5(password + seed)

We only store password_sum in the database. Thus if an attacker gets a copy of the database he can’t guess the passwords.

For each login attempt the web site will generate a temporary seed. This seed enables to fluctuate the bytes representing the passwords. This way, any sniffer attacks are countered.

The client will compute the following formula before sending the connection request.

encoded_password = md5(md5(password + seed) + temp_seed)
And the server will compute this one to check the connection attempt.

encoded_password = md5(password_sum + temp_seed)
If both values are equal, then the user typed in the correct password.

I’m over with the therory part. We will build our own secure authentication system with Ruby on Rails.

The form will contain the following fields: login, password, seed, temp_seed and encoded_password.

The main difficulty is to get the seed of the specified user. Indeed the seed is unique for each user. It’s not possible to provide it when the form is created, because we don’t know the user’s login. Fortunately Rails (actually prototype) has an AJAX helper method called observer_field. It enables to send a request when a field is edited. Thus when the login field is modified, a request is sent in the background to retrieve the user’s seed into the seed field. The form looks like this.

<%= javascript_include_tag 'md5' %>
<%= javascript_include_tag 'connection' %>
<%= javascript_include_tag 'prototype' %>
<p style="color: red"><%= flash[:error] %></p>
<%= start_form_tag 'check' %>
  Login: <%= text_field_tag :login %> <br/>
  Password: <%= password_field_tag :password %> <br/>
  Seed: <span id='seed_container'><%= text_field_tag :seed %></span> <br/>
  Temp seed: <%= text_field_tag :tmp_seed, @tmp_seed %> <br/>
  Encoded password: <%= text_field_tag :encoded_password %> <br/>
  <%= submit_tag 'Connection', :onClick => "hash_password('password', 'seed',   'tmp_seed', 'encoded_password')" %> <br/>
  <%= observe_field :login, :frequency => 0.5, :url => {:action => 'seed'}, :update => 'seed_container' %>
<%= end_form_tag %>

Finaly we need to compute the password sum before sending the form. As you can notice in the source code the function hash_password (in connection.js) is called when the user clicks on the submit button.

function hash_password(pwd_id, pwd_seed_id, tmp_seed_id, pwd_sum_id) {
    var pwd = document.getElementById(pwd_id);
    var pwd_seed = document.getElementById(pwd_seed_id);
    var tmp_seed = document.getElementById(tmp_seed_id);
    var pwd_sum = document.getElementById(pwd_sum_id);
    // Compute md5(md5(password + seed) + temp_seed)
    // And don't forget to blank the original password field
    pwd_sum.value = md5_hex(md5_hex(pwd.value + pwd_seed.value) + tmp_seed.value);
    pwd.value = '';
}

This function is quite simple. The two last lines are the most important ones. The first computes the sum as explained before. And the last blanks the password field to not send it over the network. As you may notice I included an md5 file which isn’t mine and can be found here. You also can use sha-1 instead of md5.

The connection attempt is checked in the ‘check’ action of the controller connection.

def check
  tmp_seed = session[:tmp_seed]
  raise if tmp_seed.nil? or tmp_seed != params[:tmp_seed]
  @session[:tmp_seed] = nil
  usr = get_user(params[:login])
  raise if usr.nil?
  client_password = params[:encoded_password]
  server_password = hash_sum(usr.password_sum + tmp_seed)
  raise if client_password != server_password
  flash[:notice] = "Connected as #{usr.login}"
rescue => e
  flash[:error] = 'Bad login'
  redirect_to(:action => :open)
end

I hope that this note will help you have your own secure authentication and/or made you realize the importance of having clear passwords over the internet.


Home