using System;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;

namespace MileageTraker.Web.Utility
{
	public static class Crypto
	{
		private const int Pbkdf2Count = 1000;
		private const int Pbkdf2SubkeyLength = 256/8;
		private const int SaltSize = 128/8;

		/* =======================
         * HASHED PASSWORD FORMATS
         * =======================
         * 
         * Version 0:
         * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
         * (See also: SDL crypto guidelines v5.1, Part III)
         * Format: { 0x00, salt, subkey }
         */

		public static string HashPassword(string password)
		{
			if (password == null)
				throw new ArgumentNullException("password");

			byte[] salt;
			byte[] subkey;
			using (var deriveBytes = new Rfc2898DeriveBytes(password, SaltSize, Pbkdf2Count))
			{
				salt = deriveBytes.Salt;
				subkey = deriveBytes.GetBytes(Pbkdf2SubkeyLength);
			}

			var outputBytes = new byte[1 + SaltSize + Pbkdf2SubkeyLength];
			Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
			Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, Pbkdf2SubkeyLength);
			return Convert.ToBase64String(outputBytes);
		}

		// hashedPassword must be of the format of HashWithPassword (salt + Hash(salt+input)
		public static bool VerifyHashedPassword(string hashedPassword, string password)
		{
			if (hashedPassword == null)
				throw new ArgumentNullException("hashedPassword");
			if (password == null)
				throw new ArgumentNullException("password");

			byte[] hashedPasswordBytes;
			
			try
			{
				hashedPasswordBytes = Convert.FromBase64String(hashedPassword);
			}
			catch (FormatException)
			{
				return false;
			}

			// Verify a version 0 (see comment above) password hash.

			if (hashedPasswordBytes.Length != (1 + SaltSize + Pbkdf2SubkeyLength) || hashedPasswordBytes[0] != 0x00)
			{
				// Wrong length or version header.
				return false;
			}

			var salt = new byte[SaltSize];
			Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize);
			var storedSubkey = new byte[Pbkdf2SubkeyLength];
			Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, storedSubkey, 0, Pbkdf2SubkeyLength);

			byte[] generatedSubkey;
			using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, Pbkdf2Count))
			{
				generatedSubkey = deriveBytes.GetBytes(Pbkdf2SubkeyLength);
			}
			return ByteArraysEqual(storedSubkey, generatedSubkey);
		}
		
		// Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
		[MethodImpl(MethodImplOptions.NoOptimization)]
		private static bool ByteArraysEqual(byte[] a, byte[] b)
		{
			if (ReferenceEquals(a, b))
			{
				return true;
			}

			if (a == null || b == null || a.Length != b.Length)
			{
				return false;
			}

			bool areSame = true;
			for (int i = 0; i < a.Length; i++)
			{
				areSame &= (a[i] == b[i]);
			}
			return areSame;
		}
	}
}