Jon Watte gameauth (at) mindcontrol.org
|
0. Summary
Authentication for games where untrusted clients connect to one
or more trusted servers is an interesting special case of general
authentication. This article presents some alternatives that can
go into designing authentication for such as system, and proposes
one particular set of internally cohesive design choices.
|
Change History
- 2010-12-21: Updated description about hashed passwords,
adding section on rainbow tables and hashing functions.
|
1. Introduction
Authentication is the process of making sure that someone says who
they say they are. For computer games, this comes in two flavors:
- Game login
given a user name and password, match the information against a
database of allowed players.
- Game sessions
given a network packet, make sure that it's sent by the logged in
player it says it came from.
Note that Authentication, the ability to determine who sent a specific
message, does not have much to do with Encryption, the ability to hide a
message from unintended recipients, except one kind of cryptosystem
(public/private key systems) have the ability to provide both functions
in one go. Unfortunately, these cryptosystems are usually very computationally
expensive, so they're not a great match for real-time, online services
like computer games.
|
2. Game Login
To secure game login, you need to worry about a few kinds of problems:
- Insecure passwords
Players may have a password that's a common word (like "secret"),
the player name itself, or even blank. Your password setting mechanism
should detect weak passwords and require a better password. The bare
minimum is six characters or more, with at least one letter, and at
least one non-letter character.
- Insecure password storage
Are your servers secure? Someone might break into them. If they can
read the password in clear text, well, that's a problem. Also, are the
operators of your system trustworthy? Even after you just let them
know that their services will no longer be needed? It's safer to store
passwords in a way that means you can't easily read them back.
- Sniffed passwords
If you don't use Secure Sockets or some similar encrypted protocol,
it's possible that someone can use a packet sniffer to read a password
sent in clear text, and then impersonate the user in question.
- Multiple logins
The same user shouldn't be allowed to be logged in more than once, or
what will happen is that a single player will pay for the game, and
share the log-in with all his friends. While this is a small bit of
lost revenue, the bigger problem comes when you have to ban the account
because some of those "friends" didn't play by the rules. This is a
customer service nightmare.
|
Let's examine security in your client/server design a little closer:
The main trade-off you have to make is whether you want the
passwords to be recoverable from the database or not. Depending on this
choice, you have the following two options:
- Recoverable passwords
If the password is recoverable, then you can use the
Challenge Hash Authentication scheme.
This is a major benefit. However, the passwords are
more vulnerable when they are recoverable in the database.
Don't store the passwords in cleartext in the database. At least scramble
them using some key that you build into the code, to make it harder for
the casual inspector to "accidentally" see the password. There's still a
danger that the passwords can be compromised, so be vigilant against
human factors that can compromise your data.
- One-way hash passwords
When setting a password, you calculate a one-way hash of the password
(such as a SHA-256 checksum), and store the checksum. When the user tries to
log in the next time, he gives you a password, and you calculate a SHA-256
checksum of that, and compare to the stored checksum. If they match, you
assume the password was correct.
The main benefit here is security on the system side; reading an SHA-256
checksum will not immediately let anyone know what the actual password is.
Finding another password that generates the same checksum is computationally
very hard. However, you must use Secret Exchange
Authentication.
Also, when using hashed password storage, common passwords like "password"
or "123456" will be easily found, for example using a rainbow table. Even
less common, but short, passwords can be brute-forced. One defense against
this kind of attack is to "salt" the hashed value with some data that
changes per entry, so that two entries storing the same password don't have
the same hashed value. However, anyone recovering the salt will still be
able to recover the password using guessing, unless you have a way of
making the salt a lot harder to come by than the password database. This
weakness means that you might just decide to store passwords recoverable
in the first place, and implement good security practices to avoid leaking
the password database in the first place.
|
2.1. Challenge Hash Authentication
In Challenge Hash Authentication, the server issues some random number,
called a "challenge", to the client. The client computes a hash of this
random number and the client-side entered password, and sends the hash
value back to the server. The server then computes a hash of the remembered
challenge value and the stored (plain-text) password, and compares to what
the client submitted. If the hashes match, the right password was supplied.
There are three main properties of this system:
- Passwords are not transmitted
Thus, someone sniffing the regular login traffic will not be able to
determine what the password is.
- The challenge is specific to each login attempt
Thus, if you sniff the connection, you cannot remember what the hash is
and then just re-supply the same hash value later to log in, because the
random challenge generated by the server for a specific login attempt
will be different than the last.
- The server has the clear-text password
This is a security problem if the server side becomes compromised, but
the clear-text password, which is a secret shared by both sides of the
communication, can be used to encrypt any data coming to/from that
particular client. Care has to be taken to use an encryption algorithm
from which the key cannot be recovered, of course -- XOR would not be
appropriate!
|
2.2. Secret Exchange Authentication
In Secret Exchange Authentication, the server stores a hash of the password.
The client submits a plaintext password, and the server hashes this plaintext
password, and compares the hash to the stored hash. If they match, the right
password was supplied.
There is one strength and two weaknesses in this system:
- The server doesn't store the plain-text password
If someone breaks in and steals the password file, it doesn't matter,
because you can't guess what a password is just by knowing its cryptographic
strength hash. On old UNIX machines, the strength of the cryptography is
not that high, so you should still keep your /etc/shadow file secure, but
with a 256 bit SHA-256 hash, you should be pretty safe. If you can't trust your
backup operators, or if you get hacked, then this is a major benefit!
- The password is transmitted on each login attempt
If someone can sniff the connection, they could recover the password. Thus,
you have to secure the login attempt using some kind of encryption -- but
it's not clear what you should use as a key to achieve good security. The
most secure way involves a Diffie-Hellman key exchange, which is fairly
tricky code to implement right, but will provide for a secure, encrypted
channel between two endpoints, without prior exchange of keys. If you
wanted to protect against a sophisticated attacker inserting themselves
in the middle of the network, you would additionally have to introduce a
public key based cryptographic authentification system, but computer games
by and large do not have to worry about this kind of attack.
- The server has the clear-text password
Because the client sends the clear-text password, the server has at least
temporary access to the clear-text password, and can use this as a key for
future communication encryption, after the initial log-in. Unfortunately,
this means that if someone can impersonate your server, or read the memory
of your server process, they can still recover plain-text passwords, even
if the password storage file itself is secure.
|
3. Game Sessions
Once the player has logged in, your troubles are not over. You often need
to transfer a player from one machine to another, or to allow the player
to disconnect from the server (perhaps through crashing) and then re-
connect, resuming where he left off. You clearly can't just let the client
say whom it is, and have the server blindly trust that, because it would
be trivial for another player to suddenly impersonate another player.
Instead, you have to use one of three techniques:
Identity by IP Address,
Identity by Autentication Token or
Identity by Cryptography.
3.1. Identity by IP Address
In this method, the server looks at the source IP address and port
number of the arriving packet, and internally have a table that tells
you which player sends on which address/port pair. This is secure, as
long as you know that the player will keep sending from the same port,
and as long as you trust that the internet will not accept spoofed
addresses in packets -- or, if a packet is spoofed, that some round-trip
confirmation with the real client can take place.
Such round-trip confirmation can come in the form of explicit
acknowledge of particularly suspect commands ("surrender game" for
example), or implicitly by using a rotating sequence number starting
from a negotiated, random initial starting point.
Sadly, if you use TCP for your connections, or if you need to hand
connections off between servers, then the port part of the client's
address will not necessarily stay the same. TCP allocates a new port for
each connection for each machine it connects to, and even UDP can suffer
port renumbering when you switch destination machines, if it's behind a
non-friendly NAT gateway (although most home NAT routers don't impose
this limitation).
3.2. Idenfity by Authentication Token
When the player logs in, the server determines a duration for which the
connection is good; for example, one hour. The server then calculates a
hash of a few pieces of data: the client ID, the expiration time of the
login session, and a secret number that only the servers know. The server
then sends a token to the client, which contains the client ID, the
expiration time, and the hash of the three pieces of data.
When the client sends data, it precedes the data with this token. The
server picks apart the identity, time, and hash parts, and re-computes
the hash with its internal secret number. If the hash matches the hash
in the supplied token, the server knows that the packet comes from the
player who initially authenticated with the server (barring any sniffing
or man-in-the-middle attacks throughout the session).
If the client crashes and then re-connects, it could read the cookie from
disk and re-supply it, and as long as the session is still valid, it
would be fine. If game sessions last more than an hour, the server that
the player is currently talking to would extend the cookie by half an
hour each time the cookie is at least half an hour old by re-generating
a new token based on client ID, new expiration time, and a hash of those
entities and the server secret number. That way, a client can crash and
then keep playing as long as it re-connects within half an hour, without
having to log in again.
3.3. Identity by Cryptography
If you use a shared secret between the server and the client, such as a
plain-text password, then you can use that secret as key. Each packet
sent by the client contains the client ID in plaintext, followed by the
packet data, encrypted by the shared secret, followed by a checksum of
the (unencrypted) data.
When the server receives a packet, it looks up the client password in an
internal table, decrypts the message, and verifies the checksum. If the
checksum doesn't match, then the data was not encrypted with the right
password, and thus the packet did not come from the right client.
Best practice says that part of the encrypted data should be a sequence
number, so that successive identical packets will still encrypt
differently, and so that capturing and re-playing a packet will have
a low likelyhood of being accepted for real.
|
4. Conclusion
If you are reading this, it's a good sign -- you care about security, and
want to do it right!
A good encryption algorithm to use when both sides know the key (such
as when using Secret Exchange authentication
and Identity by Cryptography identification)
is the Tiny Encryption Algorithm, which
is easy to implement, yet cryptographically strong. However, if you can
find an already-implemented library implementing the current standard
encryption algorithm (AES), it's usually better to use that. For C++, a
good encryption library is the crypto++ library, available from SourceForge.
SHA-256 is a commonly used hashing (digest) function, and is currently considered
secure. However, to deny a brute force attack, you may want to use a library like
bcrypt, with a high load factor. Just make sure to scale your sign-on servers to
keep up with the added load.
A sufficient implementation of authentification and identity for a
cluster of collaborating trusted servers (such as for an MMORPG) would
look something like this:
- As setup, all servers in the cluster share a large random
number, known as the cluster
secret(*).
- Client connects to login server.
- Using Diffie-Hellman key exchange, client and server negotiates
keys for login.
- The server issues a challenge to the client, consisting of a
128 bit random number.
- Client calculates a hash of this number concatenated with the
password the user enters, and supplies the hash to the server.
- Server verifies that the hash of the challenge and stored
password matches what the client supplied, and issues an
authentication ticket consisting of user id, ticket expiration
time, and hash of a cluster secret combined with these two items,
and supplies to the client. This ticket may also contain the IP address of
the client (part of the has as well, in that case).
- Login server also generates a random key for use by this
client during this sessions, and supplies to the client. It also
records the key for the user, and the expiry time of the ticket.
- Client connects to arbitrary server that is part of the game
server cluster.
- Client starts connection by sending authentication ticket.
- The new server verifies that the ticket has not expired, and that
the hash is correct. Using the user id in the ticket, the new server
retrieves the encryption key for the user from the login server.
- The new server and the client also negotiate sequence numbers for
future communications at this point.
- Once authenticated, the new server and client exchange data
encrypted with the session key, where the encrypted data includes
a hash of the data proper, and a sequence number. Each of these
packets only need to have the client ID and ticket identifier (a small
integer) as header, not the full authentication cookie.
- Periodically, the server that the client is currently connected
to checks whether the session authentication ticket is about to
expire; if this is the case, re-contact login server to get a new
ticket and forward it to the client.
If you are using TCP for your transport layer, you might want to use
a SSL/TLS library instead, which solves the Diffie-Hellman and session
key encryption problem, but you still have to implement a ticketing
solution of some sort.
This scheme will protect against the dangers of someone sniffing your
passwords on the open internet, and against the dangers of someone
trying to use a technical flaw to impersonate another player. However,
it will still not protect against the danger of a sophisticated
man-in-the-middle attack, for which you would have to add public-key
certificates. This technique also does not protect against a user
looking at all the data sent to his client -- he controls the machine
running the client, so he could always inspect the data in memory, so
your game design has to be cheat-proof, or provide incentives for
users not to cheat to get around that problem.
Recent reports from independent studies show that the newer voting
machines replacing old punch-card machines do not actually follow even
these simple practices; if you follow them, your game has a chance of
being more secure than the process that selects the next leader of the
free world. Whether this is a good thing or not is up to you to decide.
|
(*)Cluster shared secrets are also known as cluster keys.
Cluster keys can periodically change while the cluster
stays up, for extra security against compromise. The idea is to keep
a "current key" and a "next key", with a time at which the next key
becomes the current key. Tickets issued with an expiry time at or
after the roll-over time use the "next key"; tickets issued with an
expiry time before that use the "current key". When validating tickets,
the correct key for the expiry time supplied in the ticket should be
used. If the key rotation time is less than or equal to the maximum
ticket expiry time, then you also need to keep one or more "old" cluster
keys to be able to validate tickets that have not yet expired. However,
rolling over cluster keys every two hours, in a system with a one-hour
session ticket expiry time, should be quite secure enough for most
current games.
|
| |
|