Summary: Provide basic protection against password sniffing and sessions theft where SSL is an infeasible or inappropriate solution
Categories: Uncategorized


To provide basic protection against password sniffing and sessions theft using simple mechanisms deployed in PHP and javascript, in scenarios where SSL is an infeasible or inappropriate solution. The extent of protection of the presented scheme is to be located somewhere between "no protection at all" and SSL.


public key encryption for transferring password, client-side cookie manipulation to store seeds and temporary keys

Does and Doesn'ts:

  • is solely for guaranteeing authenticity of the user's identity and data to the server
  • does not encrypt general private data and thus does not prevent private data being read by eavesdroppers
  • provided that no tampering with the network infrastructure (man-in-the-middle attacks) takes place:
    • does prevent identity theft and associated theft of privileges / associated damages
    • does prevent injection of commands by an attacker in the name of the user

Assumptions in detail:

  1. No tampering with the network infrastructure takes place:
    The connection between client and server is assumed to provide possibly public, yet unaltered and undisturbed communication, i.e. the data transmitted may be read by a malicious third party, but may neither be tampered with nor interrupted, delayed etc.. (Note that this does not by itself mean that data arriving in the name of the other party is indeed originating from the other party, since that data might in fact arrive from a different source -- alongside the true data!)
  2. Data coming from the server is assumed to be authentic. To this end the client makes sure to read data only from the true server. It is assumed that no man-in-middle attack takes place. (Usually for http requests a connection to the server is started by the client, which is closed upon receiving the http response. It is usually not possible for a third party to inject data packets into this data stream, unless forging with the routers found on the physical data path or by dns spoofing.)
  3. Data coming from the client is a-priori not assumed to be authenticated (obvious). (A sniffer may easily get hold of the session id, so sending requests using this id alone provides no identity guarantee.) Nevertheless we assume the "client wins any transmission races against the sniffer", i.e. data arriving at the sniffer cannot reach (usually modified) the server faster than the true data from the client arrives at the server. ("The moment data arrives at the sniffer it is already too late to precede its arrival at the server with own packets manufactured from it.")
  4. The browser allows cookies to be set only by HTML code coming from the domain the cookie is registered with. (Prevents that foreign sites can read the password salt and current temporary keys.)

Password transmission


a) want to prevent replay
b) want to prevent dictionary attacks
c) want to prevent cryptanalysis aiming at the private key of the public-key-crypto system

a) Requires a changing element coming from the server that is new every time it asks for password
b) Requires an additional element from the client that is salting the password
c) Requires that all public elements are manufactured into the message that is encrypted in a distorted way, or expressed differently, all parts that are encrypted must be unknown to the sniffer.

Incremental development:

Say we encrypt the password p with the public key of the server (sent to the client beforehand from the server). If we sent enc(p) to the server, this would be a static message that could be replayed once picked up by the sniffer to gain client privileges on the server.

Therefore every time the server requests a password, it creates a temporary key k and transmits it to the client. The client could now send enc(p . k) to the server (where the dot denotes concatenation), but to prevent cryptanalysis, it is better to send enc(p . hash(p . k)), as k is open. This message is unique for every password request and can thus not be replayed. On receipt the server checks against the currently activated temporary key(s) whether the received message is indeed generated with the key sent to the client beforehand. (The server holds a list of active temporary keys and makes sure that they expire.) (Note that both parts of the encrypted message are unknown to the sniffer, since they are both dependent on the password.)

Now unfortunately, if the sniffer receives such an encrypted message m_e, he can easily run a dictionary attack against it (as least if the password is weak (*)), since k is known to him and enc() and hash() are fast. Therefore we add another (final) element to the system: Before encrypting the client adds a random salt s to the message which he must keep secret. Thus the client in total sends:

 m_e = enc(p . s . hash(p . k))

where p and s and the hash value are suitably separable by the server after decrypting m_e.

Usually one would avoid unnecessary variation in the encoded messages since this always provides entry points for cryptanalysis. Therefore (and in similarity with usual password salting) one would choose the salt s only once and store it somewhere safe on the client.

Providing repeated authenticity guarantee to the server


a) want to prevent that sniffer can go as client using the session id
b) want to keep the current session id transmission mechanism untouched (since we have to)
c) have no access to the complete request data, so only a token provided by the server can be authenticated => authenticity has to rely on practical speed conditions, since authenticated token can be remanufactured into a forged request.


Again, since encrypting the session id would be prone to replay, we introduce a random element k_2 generated by the server (transaction number). The client returns this k_2 encrypted in a way that is proven to be possible to be done only by the client. We thus can associate the received request uniquely to the client, even though the uniqueness has a short life. The server therefore makes sure that every transaction number can be used only once for page requests/actions. (Note that by assumption (3.) we exclude the case that the sniffer takes the encrypted TAN and manufactures a request that can reach the server before the original client's one. This assumption is necessary as the encryption process will not (be able to) take the content of the request filed to the server into account.)


with every request to the server the client sends

 hash(s_2 . k_2)

where k_2 is the currently active TAN received by the server just beforehand, and s_2 is a secret that can be communicated to the server in a hidden way at the start of the session, e.g. along with the password, for example by

 m_e = enc(p . s . s_2 . hash(p . k)).

The s_2 should be generated newly regularly, e.g. at every login, since it could be prone to (though very hard) cryptanalysis of the hash function. (Note that we probably encounter many different keys k_2. For this reason it is also useful to use a independent random number generator than for k.) Note that even if s_2 became discovered, this would not jeopardize the integrity of the more important login securing system (***).


  • Back button gets essentially broken
  • What about packet loss? Worst case: loss between sniffer and server => sniffer holds an unused authenticated TAN in hands. In general (with no sniffer): if packet loss on the way to server => no problem, just resend request. If packet loss on the way to client => session breaks, since no new nonce k_2 available. Solution: have more than one nonce k_2 active? Ordering, expiration etc.?

Implementation sketch


  • BigInt.js by, or similar library by
  • A fast off-the-shelf key generator running in native code (used once by the site admin only)
  • a hash algorithm (note that md5 is compromised, though the collision-resistance is not used in this scheme)

On server:

A) One time generation of public/private key pair:
E.g. using the BigInt library inside an HTML document run once on the admin client machine, 
then copying private key into config file on server.

B) Login request code (PHP):
- generate temporary key k, store it locally with expiration time and ip address etc.
- send k and public key parameters (n,e)

On client:
C) Login serving code (javascript, HTML):
- on load of login form:
  - generate s if not existent, 
  - (re)generate s_2 (devalidating anything similar before) 
  - store these as cookie
- on form submit:
  - load s and s_2 from cookie
  - encrypt "p . s . s_2 . hash(p . k)" using the BigInt library, yielding m_e; 
    place this in hidden variable
  - clear cleartext password

On server:
D) Login handling code:
- decrypt message
- compare found and calculated hash value
- check password
- if successful, save s_2 associated to the client

E) Session authenticity check code:
- on receipt, if an authenticated session, retrieve stored s_2 belonging to client 
  and the k_2, and check with it the received hash function value against the calculated
- on response, generate a new k_2, store it, and attach it to the output as hidden variable 
  or cookie

F) Session authenticity support code:
- on load:
  - get stored s_2 from cookie
  - calculate hash function value of s_2 . k_2, where k_2 is from hidden variable or a 
    transmitted cookie sent by server
  - store this as cookie (will be transferred to server by browser in course of the request)

G) Password change code:
In principle one has to make sure that the two passwords (old and new) are transmitted 
encrypted. (The check on equality with the repeated password can be made client-sidely.) 
To use the same salt is ok, since enough randomness is introduced in the message to encrypt 
via the hash function value.

App 1) The encrypt function:
Let n have log(n) bits, and let m be a message of shorter length. Then, make sure that 
gcd(n,m) = 1, e.g. by choosing a different s_2, (otherwise we have factorized n already). 
Then compute m_e := m^e mod n.

App 2) The decrypt function:
For public key (n,e) we have a d with e*d = 1 mod phi(n). With this m' := m_e^d mod n is 
the decrypted message.

Dimensionality considerations

Choose n such that it has at least 1024 bits. A hash function output has usually 128..160 bits. Assuming a password of length of at most 20 characters (8 bit each), one sees there is enough space left for the salt s and the temporary secret s_2. (When choosing their length such that the sum is below 512, we can spare the gcd() calculation in encrypt().)

Deeper key-length considerations

... (comes later). Result: to provide reasonable protection choose key-length such that decryption takes at least some seconds.

(*) For example, according to [(approve links) edit diff], 28 percent of the passwords captured with a fake MySpace login form were lowercase plus one digit; 12 percent were a single dictionary word plus a final digit. [On first guess one could argue: why protect the password on transmission more than the server is protected? Obvious reply: a brute-force attack targetted to a piece of data is faster than targetted to a (remote!) server.]

For the same reason, encryption has to be applied at all: if the client would send only hash(p . k) back to the server upon receiving k, this would be prone to a dictionary attack. (This can't be healed by adding salt to the hash argument as this is not known to the server.) Such a scheme would anyway require the server to store the password in cleartext, which is bad (**).

(**) An alternative scheme in which passwords would be stored as shadow passwords (seed, hash(seed . p)) still is at least prone to dictionary attacks, and has some impracticability:

1. Server sends the seed belonging to the user and a challenge k to the client.
2. Client computes hash(k . hash(seed . p)) and sends it to server.
3. Server compares receipt with calculated value, drawing hash(seed . p) from password database.

Disadvantage: seed sent to client depends on username -> slows login process.
Bad: Prone to dictionary attacks: as k and seed is known to the attacker, p may be discovered.

(***) According to the arguments given in the there preceding section this is not perfectly true: if many such s_2 keys get uncovered, then the login protecting scheme may find itself prone to a (differential) cryptanalysis attack. To heal the situation, it might be better to send

 hash(hash(s_2) . k_2)

to the server. Here even if hash(s_2) would be uncovered, this gives no bijective conclusion to the s_2 (if long enough), and thus no advancement in breaking the login protecting scheme.

ThomasP May 27, 2007, at 07:37 AM