Secure User Authentication

From GTA Network Wiki
Jump to: navigation, search

Secure User Authentication




dialog-warning.pngRework required!

The contents of this page are possibly invalid or incomplete. Please treat the contents of this page with caution!

If you are able to, please modify the page so that it's factually correct.


Introduction

This page will explain how to safely store passwords and utilize them in authenticating a user.

How do we safely store passwords?

Passwords must not be stored directly, instead they should be hashed with a key derivation function that also salts them.

A key derivation function is a one way function, it can map a plain-text password onto a secret key but not the inverse, moreover, such function should be computationally intensive/slow enough to hinder brute-forcing without hindering the experience of legitimate users. Note that basic hash functions such as MD5 or SHA1 alone aren't recommended for password derivation, as they're designed to be fast to compute and hence fast to brute-force, especially with modern CPUs and GPUs.

Most key derivation functions accept a salt, which is a randomly generated value that mustn't be reused between passwords. A KDF(key derivation function) fed with the same plaintext password but a different salt, will generate different hashes. This way, if one of the hashes is compromised (you find out which password it belongs to), it wouldn't compromise the rest of the hashes. Not salting your passwords makes them vulnerable to dictionary attacks and rainbow table attacks.

How can we do that?

It is generally a bad idea to implement your own crypto, hence why we'll use the official .NET implementation of the PBKDF2 algorithm to hash a password and verify one.

First, you'd want to add the following imports to your file:

using System;
using System.Linq;
using System.Security.Cryptography;

Then, put this class into your project's namespace.

    // All this logic is encapsulated in two static class methods.
    public static class PasswordDerivation
    {
        // Here we define some constants that are used within the generation of the password.
        // You may increase defaultIterations to make the computation slower(and thus harder to brute-force), which would be needed in the future when hardware gets better..
        public const int defaultSaltSize = 16;
        public const int defaultKeySize = 16;
        public const int defaultIterations = 10000;

        // This function takes our plain text password, and hashes it, salting it on the way, 
        // returning the salted hashed password, the salt itself, and some other computation parameters, encoded in a single string.
        public static string Derive(string plainPassword, int saltSize = defaultSaltSize, int iterations = defaultIterations, int keySize = defaultKeySize)
        {
            // The key derivation class automatically generates the salt using the given parameter
            using (var derive = new Rfc2898DeriveBytes(plainPassword, saltSize: saltSize, iterations: iterations))
            {
                // These functions generate raw byte arrays, we encode them as base-64 strings so that they can easily be stored in most databases.
                var b64Pwd = Convert.ToBase64String(derive.GetBytes(keySize));
                var b64Salt = Convert.ToBase64String(derive.Salt);
                // note that we also include the iteration count and key size, so when verifying the password, we'd use them, instead of the class constant values which we may change in the future.
                return string.Join(":", b64Salt, iterations.ToString(), keySize.ToString(), b64Pwd);
            }
        }
        public static bool Verify(string saltedPassword, string plainPassword)
        {

            var passwordParts = saltedPassword.Split(':');
            // we convert our strings back into raw bytes/integers.
            var salt = Convert.FromBase64String(passwordParts[0]);
            var iters = int.Parse(passwordParts[1]);
            var keySize = int.Parse(passwordParts[2]);
            var pwd = Convert.FromBase64String(passwordParts[3]);
            // we generate a salted hashed password with the user input 'plainPassword', using the salt and the computation constants of the original password.
            using (var derive = new Rfc2898DeriveBytes(plainPassword, salt: salt, iterations: iters))
            {
                var hashedInput = derive.GetBytes(keySize);
                // we ensure that the resulting salted hash is equal to our original hash, if so, the two passwords match.
                return hashedInput.SequenceEqual(pwd);
            }
        }
    }

If you look at the code, you'll see that we barely touched any crypto stuff, as it is done by the Rfc2898DeriveBytes class. Most of the work was in bundling the salt, the hash and the computation parameters in a string and unbundling them.

Implementing user authentication

TODO show a more fully fledged example which uses a database With these two functions, the process of registering or authenticating a user is very simple.

While registering a user, we'd hash his password (via the 'Derive' function) and store it in a file or a database. While logging into a user, we'd verify that the input password matches the hashed one (via the 'Verify' function), then continue with our operations, otherwise we'd present the user with an error.