Introduction
What I try to in this article is to create a login page that authenticate client
side (via Ajax requests) and secure the data transmition process form client to
server without using SSL.
Secure Socket Layers and Transport Layer Security (SSL/TLS) is the foundation of
Web security. Banks, travel booking sites, social networks like Facebook and
Twitter, email services and a plethora of other industries built their security
based on the fact that it is very hard to crack SSL. SSL encryption protects
data in transit from the client to the server. This communication happens very
rapidly and the encryption effectively makes a secure tunnel for information.
As using SSL is very efficent and robust way to secure authentication process
it's not free (and sometimes expensive). so what if I want to have a secure site
and can't spend a lot of money.
This is what we will discuss in this article.
Background
The basic idea in this procedure is to encrypt data in client and send it to
server. we use Public-key cryptography
for the encrypting and RSA
algorithm for this (as SSL/TSL use this algorithm).
the public key will be passed to client. client get the entered username and
apssword and encrypt them using the public key and send them to the server (via
Ajax request). server (Web service) decrypt recived data with private key and
validate username and password and pass the result to client.
so if some hacker sniff all data we passed between client and server, he/she will
has these data:
- public key
- encrypted username and password
and with these data he can't get any useful data (except he bruteforce data to
find private key but he should wait about 100 years!)
Using the code
First of all we need an RSA javascript library. I used the library provided by
Tom Wu. I combine
amnd minify them to create RSA.min.js.
after that we should have a web service to validate username and password.
remember that we want to authenticate in client so we shoul add attribute
[System.Web.Script.Services.ScriptService] to our web service:
[System.Web.Script.Services.ScriptService]
public class LoginService : WebService
{
[WebMethod(CacheDuration = 24 * 60 * 60)]
public byte[] GetPublicKey()
{
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
HttpRuntime.Cache["KeyPair"]= rsa.ToXmlString(true);
RSAParameters param = rsa.ExportParameters(false);
string keyToSend= ToHexString(param.Exponent) + "," +
ToHexString(param.Modulus);
byte[] encrypted;
using (RijndaelManaged myRijndael = new RijndaelManaged())
{
string[] key = File.ReadAllLines(Server.MapPath("~/App_Data/AESKey.txt"));
myRijndael.Key = Convert.FromBase64String(key[0]);
myRijndael.IV = Convert.FromBase64String(key[1]);
ICryptoTransform encryptor = myRijndael.CreateEncryptor();
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(keyToSend);
}
encrypted = msEncrypt.ToArray();
}
}
}
return encrypted;
}
[WebMethod(EnableSession=true)]
public bool ValidateUser(string encUsername, string encPassword,bool rememberMe)
{
if(!ActionValidator.IsValid(ActionValidator.ActionTypeEnum.FirstVisit))
return false;
string domainKey = (string)HttpRuntime.Cache["KeyPair"];
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.FromXmlString(domainKey);
string username = Encoding.UTF8.GetString(rsa.Decrypt(ToHexByte(encUsername), false));
string password = Encoding.UTF8.GetString(rsa.Decrypt(ToHexByte(encPassword), false));
if(Membership.ValidateUser(username, password))
{
FormsAuthentication.SetAuthCookie(username, rememberMe);
return true;
}
return false;
}
public static string ToHexString(byte[] byteValue)
{
...
}
public static byte[] ToHexByte(string str)
{
...
}
}
The first method generates key pair and return Public Key with proper format to client. Also it adds key pair to cache to be used by ValidateUser method (Second method). in WebMethod attribute I assign CacheDuration to a day; so any user in current day will get the same key from cache and tomorrow we will have a new key.
before returning the public key I encrypt to with AES algorithm to our process of sending key be safe from Man-in-the-Middle attack (as the previous version was vulnerable to it).
Second method get's encrypted username and password an returns result of validating these.
Before I request data from DB I checked that this ip don't exceed the number of allowed request numbers using the way that Omar Alzabir described. I also use his ActionValidator class without change. with this checking it will be safe to Denial-of-Service attack.
Last two methods "ToHexString" and "ToHexByte" are to convert the encrypted data to a format that RSA js library understand.
Now that we have web service, we should create a Login page:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login"
Async="true" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Login</title>
<script src="Scripts/jquery.js" type="text/javascript"></script>
<script src="Scripts/RSA.min.js" type="text/javascript"></script>
</head>
<body>
<form id="form1" runat="server">
<div>
...
</div>
</form>
<script type="text/javascript">
....
function validateUser() {
var pkey = $('#<%=txtKey.ClientID %>').val().split(',');
var rsa = new RSAKey();
rsa.setPublic(pkey[1], pkey[0]);
var username = rsa.encrypt($('#<%=UserName.ClientID %>').val());
var pass = rsa.encrypt($('#<%=Password.ClientID %>').val());
$('.FailureText').css('color', 'blue');
$('.FailureText div').text('Checking User/Pass....');
$.ajax({
type: "POST",
url: "Services/LoginService.asmx/ValidateUser",
data: "{'encUsername':'" + username + "','encPassword':'" + pass + "','rememberMe':'" + $('#<%=chbRememberMe.ClientID %>').attr('checked') + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(data, status) { OnSuccessLogin(data, status); },
error: OnErrorLogin
});
}
function OnSuccessLogin(data, status) {
if (data.d) {
$('#FailureText').css('color', 'green');
$('#FailureText div').text('Authentication was successfull. you will redirect to previous page');
var firstPage = getParameterByName("ReturnUrl");
if (firstPage)
window.location.href = firstPage;
}
else {
$('#FailureText').css('color', 'red');
$('#FailureText div').text('username / password is invalid');
}
}
function OnErrorLogin(request, status, error) {
$('.FailureText div').text('There is an error in aythenticating process');
}
.....
</script>
</body>
</html>
and in the Page_Load:
protected void Page_Load(object sender, EventArgs e)
{
LoginService service = new LoginService();
byte[] data = service.GetPublicKey();
using (RijndaelManaged rijAlg = new RijndaelManaged())
{
string[] key = File.ReadAllLines(Server.MapPath("~/App_Data/AESKey.txt"));
rijAlg.Key = Convert.FromBase64String(key[0]);
rijAlg.IV = Convert.FromBase64String(key[1]);
ICryptoTransform decryptor = rijAlg.CreateDecryptor(rijAlg.Key, rijAlg.IV);
try
{
using (MemoryStream msDecrypt = new MemoryStream(data))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
txtKey.Text = srDecrypt.ReadToEnd();
}
}
}
}
catch (Exception)
{
ScriptManager.RegisterStartupScript(Page,Page.GetType(),"","alert('Public key is invalid.')",true);
}
}
...
}
In the Page_Load of Login page I get the encrypted public, decrypt it and insert it to a textbox in the page.
Then within javascript block I simply read it from the textbox and encrypt username and password with it. then I call the web service via JSON request to validate them and return the result. also if authentication is successful it redirects page to coming page.
Points of Interest
Also I used 1024 keypair size because I found it optimum point on security vs performance. you can use smaller keys for better performance or larger keys for more security. I used textbox for keeping key that is unsafe but simpler to implement, you can use hiddenfield (input type="hidden") for safety.
Another thing is that you can seperate javascript from aspx, so the js file will be cachable. and you can get the public key asynchronously for better performance.
History
In this version I resolve the problem of Main-in-The-Middle attack (with special thanks to the kind reviewers that mentioned it), make web service safe of Denial-of-Service attack and make public key safer with changing it daily.