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.
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
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) 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();
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
private string CreateChallengeString()
System.Random rng = new Random(DateTime.Now.Millisecond);
byte salt = new byte;
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);
challenge, salt, null,
private string ComputeHashString(byte salt,
byte target = new byte [salt.Length + uniqueKey.Length];
System.Buffer.BlockCopy(salt, 0, target, 0, salt.Length);
System.Buffer.BlockCopy(uniqueKey, 0, target,
SHA1Managed sha = new SHA1Managed();
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.
RSACryptoServiceProvider instance sharing the same set of public/private key (of 1024 key length) generates a "bad data" exception.
while (a.length % key.chunkSize != 0)
a[i++] = 0;
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
These two talked finally!
Points of interest
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.
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.
- Aug 7th 2005 - first draft.