You don’t want your passwords to be sent in cleartext over your neighbour’s WiFi, do you?
When developing a network application with a custom unencrypted proprietary protocol that works directly over TCP or UDP, such as is often the case with multiplayer online games, you will more than often have the requirement to implement a password-based authentication mechanism.
This article demonstrates the practical usage of password hashes, asymmetric encryption, and symmetric encryption algorithms.
Because it is quite feasible for a malicious actor to sniff packets sent over an untrusted internet connection, it is necessary to encrypt vulnerable information, such as passwords, in a way it can only be decrypted by a trusted server.
In the case of application development, luckily, this is quite simple. As long as the application can be securely delivered to the user, at least. We can make use of asymmetric encryption. This is essentially a pair of keys, where one key encrypts, and the other one decrypts. The RSA algorithm is currently recommended for this purpose. To keep things as simple as possible, we’ll have the client know the encryption key in advance, and have the server know the private decryption key in advance.
At its most basic, the client encrypts the password using the encryption key, the encrypted password is sent to the server, and the server can then decrypt the password using the private decryption key.
This is functionally close to sending a password over a HTTPS connection without any processing. Except, it really isn’t secure, for all the reasons explained further. Please don’t ever do this.
Keep in mind that this can only work securely when the client application is safely delivered to the user. Otherwise, the application would be susceptible to a man-in-the-middle attack. A malicious third party could technically provide a modified client application which proxies all the traffic through their own service, with different security keys, steal the user’s data, and forward the traffic to your service. Make sure to obfuscate any security keys in your application. While there is no harm in the keys being publicly known, we need to make it more difficult for a third party to provide a modified client application.
At this point, we’ll still have a plaintext password present on the server. We don’t really like to have that happen, even if the password in the database will be a hash. It’s not sufficient to do the hash client-side only, however, since then an attacker would only ever need the hash, and not the password. And hashed password leaks are widely available online. So, what we should do then, is to hash the password once on the client, and hash the password a second time server-side. The hash should be using a different salt in each case, and optimally include the username as part of the salt for added variation. A good hash algorithm for the password is currently scrypt.
The steps for verifying a password thus could be as follows, on the client:
- Hash the password using the username and a fixed value as salt
- Encrypt the hashed password using the encryption key that is known by the client
- Send the encrypted hash to the server
In response, on the server:
- Decrypt the hashed password using the private decryption key
- Hash the hashed password again using the username and another fixed value as salt
- Compare the hash with the known hash in the user database
With this routine in place, you can be reasonably assured that your cleartext password will never leave your computer, and that a leaked password database will not be useful in any way to break into accounts.
However, this mechanism still leaves open the possibility of a replay attack, which would still allow anyone listening in to the connection to authenticate, as the encrypted hash sent by the client is always the same.
To alleviate this issue, a simple solution is to include a random number followed by a timestamp as part of the encrypted data. Keep in mind to put the timestamp before the password hash, to ensure the password portion of the encrypted data changes with it. A requirement must then be added server-side to keep track of the last timestamp for each user, so it must increment on each login, and to ensure that the timestamp is within a reasonable delay of the server timestamp. Alternative solutions involve multiple steps in the authentication exchange, providing randomized challenges, so are more complex than practical for our needs.
In most cases, the server will want to return a secure session cookie to the client. And, of course, the cookie will also need to be transmitted in a manner in which it cannot be intercepted.
Using a known and trusted public key is insufficient to engage in two-way secure communication. From the client’s point of view, it only proves the source and validity of the data. Only communications encrypted using the client’s public key are secure as long as the private key is not compromised. But anyone can decrypt messages from the server that were encrypted using the private key. We can, however, establish secure two-way communication, by first exchanging a dynamically generated symmetric encryption key.
A symmetric encryption algorithm that’s widely in use is AES-256. Because we can securely send a request from the client to the server already, we can just send a randomly generated symmetric key along with the authentication request. The server can then reply with a session cookie that is encrypted with this key, which only the client and the server know, and thus, other than the server, only the client will be able to decrypt the cookie.
What’s more, in an attempt at a replay attack, an attacker will not be able to decrypt the cookie, as the required symmetric key, which was sent by the client in the request, can only be decrypted using the private key that only the server knows.
When using the cookie, it should be treated in the same manner as a password hash, and always encrypted with a random number and a timestamp, either using the known asymmetric encryption key, or using the exchanged symmetric key, depending on how the server backend is designed. Using the symmetric key is more computationally efficient, but does require storing the symmetric key on the server per user session. A separate session identifier will be required, different from the session key, to identify the session.
Alternatively, it is also possible to instead use the cookie in clear, when the data it accompanies is always encrypted using the symmetric key, as then the encrypted data serves as the validation. In this case the cookie may function as session identifier.
A sufficiently secure authentication mechanism can thus be implemented in a single request and response message pair. It secures against leaking cleartext passwords, makes a leaked password database completely useless, is safe against replay attacks, and securely exchanges a session cookie. As long as the server’s private key is not disclosed, and as long as the client application’s public key was not modified, it is also safe against a man-in-the-middle attack.
Even when already using a secure connection, such as HTTPS, it is still necessary to take certain basic security precautions for sensitive data such as passwords. Passwords should in all cases always be hashed on both the client and server. It is strongly discouraged to have the plaintext password present on the server at any time. Passwords and hashes should also be securely erased from RAM on both the client and the server as soon they’re no longer required.
Notably, when using HTTPS through an edge service, such as CloudFlare, the traffic will be unencrypted on their servers. This effectively means that, from CloudFlare’s point of view, your traffic is unencrypted. And consequently means that, unless you fully trust CloudFlare, you should most definitely implement a secure authentication process that does not depend on the potentially compromised encrypted channel provided by the proxied HTTPS transport. You don’t want your passwords to be sent in plaintext over CloudFlare’s network, either.