For a personal project, I am building a Rails site that has an administration section. Of course, I don't want any nefarious person who snoops my network traffic to be able to login. SSL isn't an easy option because (1) my site is on a shared host, (2) I don't want to pay for an SSL certificate, and (3) I would prefer that my users do not need to accept a self-signed certificate.
Given these conditions, I felt that a private/public key pair would successfully obfuscate login credentials without SSL. At a high level, my Rails application generates a 1024-bit RSA key on the fly and shares a public version with the client. The client utilizes an open source RSA library for JavaScript to encrypt the credentials on the client before sending them back to the server, which then uses the private key to decrypt them. I'm not an encryption expert, but I think the worst that could happen is that someone could decrypt the credentials for the one request they capture (feel free to correct me though).
Let's get to the code. To set the situation, I am following REST conventions for authentication, so I have a SessionsController
with a new
action and a create
action. The former is responsible for setting up the login and the latter for processing the user's input.
First, the "new" action, which creates the RSA key, provides the public components to the view template, and stores the key (in PEM format) in session:
def new key = OpenSSL::PKey::RSA.new(1024) @public_modulus = key.public_key.n.to_s(16) @public_exponent = key.public_key.e.to_s(16) session[:key] = key.to_pem end
Then in the view template ("new.html.erb"), we provide the public modulus and exponent (the necessary component of the public key) as well as input forms for the username and password:
<%= javascript_include_tag('rsa/jsbn', 'rsa/prng4', 'rsa/rng', 'rsa/rsa', 'rsa/base64', :cache => true) %> <% form_tag session_path, :id => 'login' do -%> <fieldset> <legend>Please Login</legend> <label for="login" class="required">Login</label> <%= text_field_tag :username, params[:username] %><br /> <label for="password" class="required">Password</label> <%= password_field_tag :upassword, params[:upassword] %><br /> <%= hidden_field_tag :password, '' %> </fieldset> <%= submit_tag 'Log in' %> <% end -%> <%= hidden_field_tag :public_modulus, @public_modulus %> <%= hidden_field_tag :public_exponent, @public_exponent %>
Two things to note here. First, we are including the four necessary JavaScript libraries on this page only. Second, we use a hidden field to store/commit the password - this field is populate via JavaScript.
My application utilizes jQuery, so attaching a function to encrypt the password before form submission is straightforward:
$(document).ready(function() { $("form#login").submit(function() { var rsa = new RSAKey(); rsa.setPublic($('#public_modulus').val(), $('#public_exponent').val()); var res = rsa.encrypt($('#upassword').val()); if (res) { $('#password').val(hex2b64(res)); $('#upassword').val(''); return true; } return false; }) });
Before submission occurs, we encrypt the value of the "upassword" field, store an encrypted Base64 version in "password," and clear "upassword." If there is a problem, the form is not submitted.
On the server-side, this form is submitted to the SessionsController#create
action:
def create key = OpenSSL::PKey::RSA.new(session[:key]) password = key.private_decrypt(Base64.decode64(params[:password])) user = User.authenticate(params[:username], password) if user reset_session # reset session after login session[:user_id] = user.id flash[:notice] = "Welcome back, #{user.username}" redirect_to admin_url else flash[:error] = 'Invalid username/password entered' new and render :action => 'new' end end
Here, we pull the key out of session and use it to decrypt the form input before attempting to authenticate the user. It is important to note the the private_decrypt
method wants binary data, so we need decode the Base64 text passed in the request (using Base64 seemed more appropriate than binary data here). After the authenticate method is called, things proceed as usual.
So far, this is working fairly well. There are a few options for improvement - perhaps a before_filter
to preprocess any encrypted data. I'd be interested in hearing other ideas on this topic as well.
References