A
recent question in SOPW got me thinking about secure
password access to a CGI program from an HTML form in
systems lacking SSL. The
solutions seem to fall into these categories:
1. Encrypt the password using a one-way hash and store the
result in a file on the server.
To gain access, the user types the password into an HTML form.
The password is then sent as plaintext to the server.
The CGI program (or .htaccess mechanism) encrypts the
text received and compares it to the encryption in the file
for validation.
Advantage: Password is not stored anywhere as
plaintext.
Disadvantage: Password is transmitted in plaintext
and could be sniffed in transit.
2. Send a random key to the client in a hidden form field.
Submit this key and the typed-in password to a JavaScript
one-way hash subroutine included with the form and send the
encrypted result back to the host.
The host also computes the encryption with the same key,
along with the password (stored locally) and compares the
two results for validation.
Advantage: No plaintext password (or repeated
encryptions thereof) are transmitted.
Disadvantage: Plaintext password must be stored on the
server.
3. Use a one-time pad.
This works like #1, except that it uses multiple
random passwords, and no password is used more than once.
Advantage: Very secure.
Disadvantage: Cumbersome to implement due to a
constant need to refresh a large password list.
4. Use a commutative one-way hash. Such a hash has the property that
hash(hash(password, key1), key2) = hash(hash(password, key2), key1)
Thus you can store
hash(password, key1) on the server and send
(the random) key2 to the client, which computes
hash(password, key2) and returns the result.
You then rehash this with key1 and rehash the stored hash with
key2, comparing the results for authentication.
Advantage: No plaintext password is stored or
transmitted.
Disadvantage: See discussion that follows.
To make #4 work in a browser environment, you almost have
to implement the commutative hash as a JavaScript function.
An example of such a function is RSA encryption, whose
commutative properties make digital signatures and the like
possible. But this may be prohibitively complicated to do
in JavaScript.
A function that
has been implemented in Javascript is
MD5.
MD5 is a "digest" function, that takes an arbitrarily large
string and returns a 128-bit (16-byte) result. It also works
with short (password-sized) strings and with its own output.
This function is also implemented through a Perl module,
Digest::MD5 (for the
server CGI side).
But I question whether there's a direct way to force
commutativity. To begin with, MD5 takes a single (string)
argument, instead of an argument and a key. Of course, a
single string could be formed from an argument
concatenated with a key, but there doesn't seem to be
a way to do this and preserve commutativity between two
random keys.
What I propose here is an
indirect way to get
commutativity. Suppose you took your password and recursively
applied MD5 to it
n times and stored the result. We'll
call this new function
repMD5(password, n).
Now, clearly,
repMD5(password, n) = repMD5(repMD5(password, n - m), m)
for any
m < n.
So here's a way to approach the security of #4 above:
5. Encode
the password by recursively applying MD5, say, 1000 times
and store the result on the server. Each time you send the
password entry form to the user, include a number
between 1 and 999 in a hidden field. That's the number of
times the included JavaScript MD5 routine must recursively
hash the typed-in password before sending it back to the
server. The server CGI program then finishes the job by
hashing it the remainder of the 1000 times and compares the
result to the stored string for authentication. Now,
the
number you send back to the client cannot be random. For
maximum security, it always has to be less than the number
you used the prior time. Why? Suppose you used 3 the first
time, and someone intercepted the transmission from the
client. If that person tried to log in and you sent 9, he
could merely take the result he observed from 3 and apply
MD5 six more times to get the valid result for 9. By using
a smaller "key" each time, this is prevented.
So where
does this leave us?
Advantage: This method can be used an arbitrarily
large (but finite) number of times before a new password has
to be assigned. No plaintext is transmitted or stored.
Disadvantage: The function in
MD5.pm computes very quickly,
but the JavaScript implementation is probably pretty
slow. So recursing up to 1000 times, say, on the client side
will doubtlessly try the user's patience. Also the need
to reassign the password after
n uses is similar to
the disadvantage cited for the one-time pad, although
here the password "list" is only one item long.
I haven't tried any of this yet, but it might be an interesting
experiment. What do the other Monks think?