Introduction
The intention of this article is to explore the browser-server RSA interoperability by describing a way that can protect login password between the browser and the server during a typical form based authentication, while still retaining access to the plain (clear) password at the server side � for further processing, such as transferring password for the first time, changing password, performing dynamic impersonation or querying Active Directory.
The mechanism is probably well known (more on this later). The implementation given in this article focuses on the interoperability between a client-side RSA implementation using JavaScript ("RSA In JavaScript", modified in order to interoperate) and the RSA counterpart of Microsoft .NET Framework, used at the server (ASP.NET) side.
Background and scope
There are several common alternatives to protect login password for form based authentication, such as:
- User name and plain password sent over Secure Sockets Layer (SSL).
- Hash password at the browser side and compare the hashed value at the server side.
- Encrypt password at the browser side and decrypt it at the server side.
The hashing/encryption at the client side for (2) or (3) can be done in:
- Java applet/ActiveX object, or
- JavaScript
This article, when tackling (3), would take the JavaScript approach because it appears less obtrusive.
Encryption and decryption algorithms used would certainly be RSA. We will not discuss the plumbing of the RSA algorithm and the complete discussion on how and why RSA works is out of the scope of this article. Interested readers can find many resources available on the Internet, such as RSA Algorithm.
Let us start first by reviewing the steps and characteristics for each of the aforementioned alternatives (1) to (3).
In option (1), the user name and password are sent over the network using SSL encryption, rather than plain text. SSL would work fine though performance can get degraded particularly over slow dialup lines.
The option (2) is quite widely used and one prominent example is the standard login page from Yahoo!. When the form is submitted, the password is hashed and then sent to the server. The hashing is one-way which means it is not possible to derive the original string from the hash value. The server would compare the received value against the hash stored previously. If both match, it implies that the user has typed in the correct password and is authenticated. The hashing in Yahoo!�s case also takes the JavaScript approach, using the library from Paul Johnson. His web page Login system discusses variants that try to provide additional securities.
The option (2) however does not allow access to the raw password string at the server side, which was the motivation for devising option (3).
How it works
In option (3), as implemented in this article, the detailed flow would be:
- The server creates a
RSACryptoServiceProvider
object, loads one pre-generated public and private key (or one among a key pool). It can then be cached for reuse. public void InitCrypto(string keyFileName)
{
CspParameters cspParams = new CspParameters();
cspParams.Flags = CspProviderFlags.UseMachineKeyStore;
_sp = new RSACryptoServiceProvider(cspParams);
string path = keyFileName;
System.IO.StreamReader reader = new StreamReader(path);
string data = reader.ReadToEnd();
_sp.FromXmlString(data);
}
Note that whenever the default constructor of RSACryptoServiceProvider
class is called, it automatically creates a new set of public/private key pair, ready for use. In order to re-use the previously created keys, the class is initialized with a populated CspParameters
object.
- When the user requests for the login page, the server generates a one-time random challenge string (in Base64), and caches it specifying N number of minutes before expiration. The form is then rendered to the browser embedding the JavaScript RSA implementation, the public key information needed for encryption and also the challenge string.
private string CreateChallengeString()
{
System.Random rng = new Random(DateTime.Now.Millisecond);
byte[] salt = new byte[64];
for(int i=0; i<64;)
{
salt[i++] = (byte) rng.Next(65,90);
salt[i++] = (byte) rng.Next(97,122);
}
string challenge = ComputeHashString(salt, param.D);
System.Web.HttpContext.Current.Cache.Insert(
challenge,
salt,
null,
DateTime.Now.AddMinutes(20),
System.Web.Caching.Cache.NoSlidingExpiration);
return challenge;
}
private string ComputeHashString(byte[] salt,
byte[] uniqueKey)
{
byte[] target = new byte [salt.Length + uniqueKey.Length];
System.Buffer.BlockCopy(salt, 0, target, 0, salt.Length);
System.Buffer.BlockCopy(uniqueKey, 0, target,
salt.Length, uniqueKey.Length);
SHA1Managed sha = new SHA1Managed();
return StringHelper.ToBase64(sha.ComputeHash(target));
}
- Upon submission after the user completes the form, the JavaScript routine would first convert the username and the password into Base64 format and then concatenate to the end of challenge string with \ as separator (since \ is not part of Base64 chars).
encryptedString(challenge + "\\" + username + "\\" + password);
Only the encrypted result is posted back to the server.
- On the server side, the
RSACryptoServiceProvider
object would decrypt the postback data with the private key and split the resultant string into three different parts (if everything goes well), that is the challenge string, username and password. The server should first verify that the challenge string exists and remains un-tampered; otherwise the authentication request would be disqualified. Once the challenge string is matched, it would be invalidated and removed from the cache immediately. The server would then forward the decrypted username and password to other routines that would perform the actual validation of username-password pair � one example is to query Active Directory.
- The cache expiration would ensure cleanup of challenge strings that are issued but never used.
To cut the long story short, in order to complete the above design, we need to find a JavaScript implementation of RSA that works at the browser side and ensures that it can talk with Microsoft's RSA implementation in the .NET Framework at the ASP.NET server side.
Interoperability of RSA between JavaScript and the .NET Framework
The JavaScript implementation from Dave "RSA In JavaScript" seems to be working quite well on his demo page, however a test feeding the encrypted result into an RSACryptoServiceProvider
instance sharing the same set of public/private key (of 1024 key length) generates a "bad data" exception.
Inspection of the JavaScript source code reveals that there is no "padding" mechanism employed.
while (a.length % key.chunkSize != 0)
{
a[i++] = 0;
}
The core RSA algorithm is a simple modular exponentiation, or rather, is a simple equation with very big numbers, therefore big numbers are crucial to ensure the strength of the algorithm. Raw RSA encryption without any padding is not secure because the number used could turn out to be small. For this reason Microsoft�s API asks for mandatory padding. Incompatible padding scheme from the JavaScript code would produce the "bad data" exception at the server side.
The JavaScript code therefore needs to implement one of two padding schemes used in the .NET RSA implementation, the first is PKCS#1 v1.5 padding and another is OAEP (PKCS#1 v2) padding.
Quoted from the Microsoft�s documentation:
Direct Encryption (PKCS#1 v1.5)
Microsoft Windows 2000 or later with the high encryption pack installed. Padding: Modulus size - 11. (11 bytes is the minimum padding possible.)
OAEP padding (PKCS#1 v2)
Microsoft Windows XP or later. Padding: Modulus size � 2 � 2*hLen, where hLen is the size of the hash.
More research on the simpler PKCS#1 v1.5 (see here, for 8.1 Encryption-block formatting) shows how padding should be carried out. During encryption, pseudorandom nonzero bytes are generated and the final padded message (before modular exponentiation) should look like this:
0x00 || 0x02 || PseudoRandomNonZeroBytes || 0x00 || Message
After injecting the padding code (omitted for clarity, please refer to the source) to the JavaScript source, the encrypted string can then be successfully decrypted by the RSACryptoServiceProvider
.
These two talked finally!
Points of interest
The technique, i.e. option (3), detailed in this article enables to transmit the username, and the password in encrypted form from the browser to the server, using the asymmetric RSA algorithm. Interoperability is provided by JavaScript implementation at the browser side and RSACryptoServiceProvider
implementation at the server side. The form submission content is encrypted using the public key and only the server knows the corresponding private key in order to decipher the content. This avoids sending them (particularly password) in plain text and prevents man-in-the-middle attack such as eavesdropping as well as passive traffic analysis.
Using encryption alone is susceptible to replay attack. Malicious users can intercept and record the traffic and make a delayed HTTP post to masquerade as the user who entered the password. This is countered by attaching to each authentication request a challenge string. It is used once and thereafter discarded. It has a limited lifespan, those challenge strings that are issued but not used are cleaned up.
However, the mechanism does not prevent phishing. It does not prevent manipulated traffic that can have malicious JavaScript functions injected into the response received by the browser, effectively bypassing the encryption routine.
Miscellaneous
In case of encryption of a Unicode string, the program should use UTF-8 to transform Unicode characters into bytes first and Base64 to transform bytes into printable characters. This is not implemented in the source project.
The RSA interoperability can be used in many other scenarios other than a typical login page like fields on the Web form can selectively decide whether to support encryption/decryption.
History
- Aug 7th 2005 - first draft.