/*
 * Copyright (C) 2013 Colin Mackie.
 * This software is distributed under the terms of the GNU General Public License.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Serialization;

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Macs;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Paddings;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Generators;

#if NUNIT
using NUnit.Framework;
#endif

#if NETCF
using OpenNETCF.Security.Cryptography;
#endif

namespace WinAuth
{
	/// <summary>
	/// Class that implements Battle.net Mobile Authenticator v1.1.0.
	/// </summary>
	public class BattleNetAuthenticator : Authenticator
	{
		/// <summary>
		/// Size of model string
		/// </summary>
		private const int MODEL_SIZE = 16;

		/// <summary>
		/// String of possible chars we use in our random model string
		/// </summary>
		private const string MODEL_CHARS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890";

		/// <summary>
		/// Buffer size used on Http responses
		/// </summary>
		private const int RESPONSE_BUFFER_SIZE = 64;

		/// <summary>
		/// Expect size of return data from enroll
		/// </summary>
		private const int ENROLL_RESPONSE_SIZE = 45;

		/// <summary>
		/// Expected size of return data from time sync
		/// </summary>
		private const int SYNC_RESPONSE_SIZE = 8;

		/// <summary>
		/// Buffer size used on Restore call
		/// </summary>
		private const int RESTOREINIT_BUFFER_SIZE = 32;

		/// <summary>
		/// Buffer size used on Restore Validation call
		/// </summary>
		private const int RESTOREVALIDATE_BUFFER_SIZE = 20;

		/// <summary>
		/// Number of digits in code
		/// </summary>
		private const int CODE_DIGITS = 8;

		/// <summary>
		/// The public key modulus used to encrypt our data
		/// </summary>
		private const string ENROLL_MODULUS =
			"955e4bd989f3917d2f15544a7e0504eb9d7bb66b6f8a2fe470e453c779200e5e" +
			"3ad2e43a02d06c4adbd8d328f1a426b83658e88bfd949b2af4eaf30054673a14" +
			"19a250fa4cc1278d12855b5b25818d162c6e6ee2ab4a350d401d78f6ddb99711" +
			"e72626b48bd8b5b0b7f3acf9ea3c9e0005fee59e19136cdb7c83f2ab8b0a2a99";

		/// <summary>
		/// Public key exponent used to encrypt our data
		/// </summary>
		private const string ENROLL_EXPONENT =
			"0101";

		/// <summary>
		/// Number of minutes to ignore syncing if network error
		/// </summary>
		private const int SYNC_ERROR_MINUTES = 5;

		private const string BATTLENET_ISSUER = "BattleNet";

		/// <summary>
		/// URLs for all mobile services
		/// </summary>
		private static string REGION_US = "US";
		private static string REGION_EU = "EU";
		private static string REGION_KR = "KR";
		private static string REGION_CN = "CN";
		public static Dictionary<string, string> MOBILE_URLS = new Dictionary<string, string>
		{
			{REGION_US, "http://mobile-service.blizzard.com"},
			{REGION_EU, "http://mobile-service.blizzard.com"},
			{REGION_KR, "http://mobile-service.blizzard.com"},
			{REGION_CN, "http://mobile-service.battlenet.com.cn"}
		};
		private static string ENROLL_PATH = "/enrollment/enroll2.htm";
		private static string SYNC_PATH = "/enrollment/time.htm";
		private static string RESTORE_PATH = "/enrollment/initiatePaperRestore.htm";
		private static string RESTOREVALIDATE_PATH = "/enrollment/validatePaperRestore.htm";

		/// <summary>
		/// Set of ISO3166 EU countries
		/// </summary>
		private static List<string> EU_COUNTRIES = new List<string> {
			"AL", "AD", "AM", "AT", "AZ", "BY", "BE", "BA", "BG", "HR",
			"CY", "CZ", "DK", "EE", "FI", "FR", "GE", "DE", "GR", "HU",
			"IS", "IE", "IT", "KV", "XK", "LV", "LI", "LT", "LU", "MK",
			"MT", "MD", "MC", "ME", "NL", "NO", "PL", "PT", "RO", "RU",
			"SM", "RS", "SK", "ES", "SE", "CH", "TR", "UA", "UK", "GB",
			"VA"
		};

		/// <summary>
		/// Set of ISO3166 KR countries
		/// </summary>
		private static List<string> KR_COUNTRIES = new List<string> {
			"KR", "KP", "TW", "HK", "MO"
		};

		/// <summary>
		/// URL for GEO IP lookup to determine region
		/// </summary>
		private static string GEOIPURL = "http://geoiplookup.wikimedia.org";

		/// <summary>
		/// Time of last Sync error
		/// </summary>
		private static DateTime _lastSyncError = DateTime.MinValue;

		#region Authenticator data

		/// <summary>
		/// Region for authenticator taken from first 2 chars of serial
		/// </summary>
		public string Region
		{
			get
			{
				return (string.IsNullOrEmpty(Serial) == false ? Serial.Substring(0, 2) : string.Empty);
			}
		}

		public string Serial { get; set; }

		/// <summary>
		/// Get/set the combined secret data value
		/// </summary>
		public override string SecretData
		{
			get
			{
				// for Battle.net, this is the key + serial
				return base.SecretData + "|" + Authenticator.ByteArrayToString(Encoding.UTF8.GetBytes(Serial));
			}
			set
			{
				// for Battle.net, extract key + serial
				if (string.IsNullOrEmpty(value) == false)
				{
					string[] parts = value.Split('|');
					if (parts.Length <= 1)
					{
						// old WinAuth2 version with secretdata + serial
						SecretKey = Authenticator.StringToByteArray(value.Substring(0, 40));
						Serial = Encoding.UTF8.GetString(Authenticator.StringToByteArray(value.Substring(40)));
					}
					else if (parts.Length == 3) // alpha 3.0.6
					{
						// secret|script|serial
						base.SecretData = value;
						Serial = (parts.Length > 2 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[2])) : null);
					}
					else
					{
						// secret|serial
						base.SecretData = value;
						Serial = (parts.Length > 1 ? Encoding.UTF8.GetString(Authenticator.StringToByteArray(parts[1])) : null);
					}
				}
				else
				{
					SecretKey = null;
					Serial = null;
				}
			}
		}

		/// <summary>
		/// We can check if the restore code is valid and rememeber so don't have to do it again
		/// </summary>
		public bool RestoreCodeVerified { get; set; }

		/// <summary>
		/// Get the restore code for an authenticator used to recover a lost authenticator along with the serial number.
		/// </summary>
		/// <returns>restore code (10 chars)</returns>
		public string RestoreCode
		{
			get
			{
				return BuildRestoreCode();
			}
		}

		#endregion

		/// <summary>
		/// Create a new Authenticator object
		/// </summary>
		public BattleNetAuthenticator()
			: base(CODE_DIGITS)
		{
			Issuer = BATTLENET_ISSUER;
		}

		/// <summary>
		/// Enroll the authenticator with the server.
		/// </summary>
		public void Enroll()
		{
			// default to US
			string region = REGION_US;
			string country = REGION_US;

			// Battle.net does a GEO IP lookup anyway so there is no need to pass the region
			// however China has its own URL so we must still do our own GEO IP lookup to find the country
			HttpWebRequest georequest = (HttpWebRequest)WebRequest.Create(GEOIPURL);
			georequest.Method = "GET";
			georequest.ContentType = "application/json";
			georequest.Timeout = 10000;
			// get response
			string responseString = null;
			try
			{
				using (HttpWebResponse georesponse = (HttpWebResponse)georequest.GetResponse())
				{
					// OK?
					if (georesponse.StatusCode == HttpStatusCode.OK)
					{
						using (MemoryStream ms = new MemoryStream())
						{
							using (Stream bs = georesponse.GetResponseStream())
							{
								byte[] temp = new byte[RESPONSE_BUFFER_SIZE];
								int read;
								while ((read = bs.Read(temp, 0, RESPONSE_BUFFER_SIZE)) != 0)
								{
									ms.Write(temp, 0, read);
								}
								responseString = Encoding.UTF8.GetString(ms.ToArray());
							}
						}
					}
				}
			}
			catch (Exception) { }
			if (string.IsNullOrEmpty(responseString) == false)
			{
				// not worth a full json parser, just regex it
				Match match = Regex.Match(responseString, ".*\"country\":\"([^\"]*)\".*", RegexOptions.IgnoreCase);
				if (match.Success == true)
				{
					// match the correct region
					country = match.Groups[1].Value.ToUpper();

					if (EU_COUNTRIES.Contains(country) == true)
					{
						region = REGION_EU;
					}
					else if (KR_COUNTRIES.Contains(country) == true)
					{
						region = REGION_KR;
					}
					else if (country == REGION_CN)
					{
						region = REGION_CN;
					}
					else
					{
						region = REGION_US;
					}
				}
			}

			// allow override of country for CN using US from app.config
			System.Configuration.AppSettingsReader config = new System.Configuration.AppSettingsReader();
			try
			{
				string configcountry = config.GetValue("BattleNetAuthenticator.Country", typeof(string)) as string;
				if (string.IsNullOrEmpty(configcountry) == false)
				{
					country = configcountry;
				}
			}
			catch (InvalidOperationException ) { }
			try
			{
				string configregion = config.GetValue("BattleNetAuthenticator.Region", typeof(string)) as string;
				if (string.IsNullOrEmpty(configregion) == false)
				{
					region = configregion;
				}
			}
			catch (InvalidOperationException ) {}

			// generate byte array of data:
			//  00 byte[20] one-time key used to decrypt data when returned;
			//  20 byte[2] country code, e.g. US, GB, FR, KR, etc
			//  22 byte[16] model string for this device;
			//	38 END
			byte[] data = new byte[38];
			byte[] oneTimePad = CreateOneTimePad(20);
			Array.Copy(oneTimePad, data, oneTimePad.Length);
			// add country
			byte[] countrydata = Encoding.UTF8.GetBytes(country);
			Array.Copy(countrydata, 0, data, 20, Math.Min(countrydata.Length, 2));
			// add model name
			byte[] model = Encoding.UTF8.GetBytes(GeneralRandomModel());
			Array.Copy(model, 0, data, 22, Math.Min(model.Length, 16));

			// encrypt the data with BMA public key
			RsaEngine rsa = new RsaEngine();
			rsa.Init(true, new RsaKeyParameters(false, new Org.BouncyCastle.Math.BigInteger(ENROLL_MODULUS, 16), new Org.BouncyCastle.Math.BigInteger(ENROLL_EXPONENT, 16)));
			byte[] encrypted = rsa.ProcessBlock(data, 0, data.Length);

			// call the enroll server
			byte[] responseData = null;
			try
			{
				HttpWebRequest request = (HttpWebRequest)WebRequest.Create(GetMobileUrl(region) + ENROLL_PATH);
				request.Method = "POST";
				request.ContentType = "application/octet-stream";
				request.ContentLength = encrypted.Length;
				request.Timeout = 10000;
				Stream requestStream = request.GetRequestStream();
				requestStream.Write(encrypted, 0, encrypted.Length);
				requestStream.Close();
				using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
				{
					// OK?
					if (response.StatusCode != HttpStatusCode.OK)
					{
						throw new InvalidEnrollResponseException(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription));
					}

					// load back the buffer - should only be a byte[45]
					using (MemoryStream ms = new MemoryStream())
					{
						//using (BufferedStream bs = new BufferedStream(response.GetResponseStream()))
						using (Stream bs = response.GetResponseStream())
						{
							byte[] temp = new byte[RESPONSE_BUFFER_SIZE];
							int read;
							while ((read = bs.Read(temp, 0, RESPONSE_BUFFER_SIZE)) != 0)
							{
								ms.Write(temp, 0, read);
							}
							responseData = ms.ToArray();

							// check it is correct size
							if (responseData.Length != ENROLL_RESPONSE_SIZE)
							{
								throw new InvalidEnrollResponseException(string.Format("Invalid response data size (expected 45 got {0})", responseData.Length));
							}
						}
					}
				}
			}
			catch (Exception ex)
			{
				throw new InvalidEnrollResponseException("Cannot contact Battle.net servers: " + ex.Message);
			}
			// return data:
			// 00-07 server time (Big Endian)
			// 08-24 serial number (17)
			// 25-44 secret key encrpyted with our pad
			// 45 END

			// extract the server time
			byte[] serverTime = new byte[8];
			Array.Copy(responseData, serverTime, 8);
			if (BitConverter.IsLittleEndian == true)
			{
				Array.Reverse(serverTime);
			}
			// get the difference between the server time and our current time
			ServerTimeDiff = BitConverter.ToInt64(serverTime, 0) - CurrentTime;

			// get the secret key
			byte[] secretKey = new byte[20];
			Array.Copy(responseData, 25, secretKey, 0, 20);
			// decrypt the initdata with a simple xor with our key
			for (int i = oneTimePad.Length-1; i >= 0; i--)
			{
				secretKey[i] ^= oneTimePad[i];
			}
			SecretKey = secretKey;

			// get the serial number
			Serial = Encoding.Default.GetString(responseData, 8, 17);
		}

#if DEBUG
		/// <summary>
		/// Debug version of enroll that just returns a known test authenticator
		/// </summary>
		/// <param name="testmode"></param>
		public void Enroll(bool testmode)
		{
			if (!testmode)
			{
				Enroll();
			}
			else
			{
				ServerTimeDiff = 0;
				SecretKey = new byte[20] { 0x7B, 0x0B, 0xFA, 0x82, 0x30, 0xE5, 0x44, 0x24, 0xAB, 0x51, 0x77, 0x7D, 0xAD, 0xBF, 0xD5, 0x37, 0x41, 0x43, 0xE3, 0xB0 };
				Serial = "US-1306-2525-4376"; // Restore: CR24KPKF51
			}
		}
#endif


		/// <summary>
		/// Synchronise this authenticator's time with server time. We update our data record with the difference from our UTC time.
		/// </summary>
		public override void Sync()
		{
			// check if data is protected
			if (this.SecretKey == null && this.EncryptedData != null)
			{
				throw new EncryptedSecretDataException();
			}

			// don't retry for 5 minutes
			if (_lastSyncError >= DateTime.Now.AddMinutes(0 - SYNC_ERROR_MINUTES))
			{
				return;
			}

			try
			{
				// create a connection to time sync server
				HttpWebRequest request = (HttpWebRequest)WebRequest.Create(GetMobileUrl(this.Region) + SYNC_PATH);
				request.Method = "GET";
				request.Timeout = 5000;

				// get response
				byte[] responseData = null;
				using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
				{
					// OK?
					if (response.StatusCode != HttpStatusCode.OK)
					{
						throw new ApplicationException(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription));
					}

					// load back the buffer - should only be a byte[8]
					using (MemoryStream ms = new MemoryStream())
					{
						// using (BufferedStream bs = new BufferedStream(response.GetResponseStream()))
						using (Stream bs = response.GetResponseStream())
						{
							byte[] temp = new byte[RESPONSE_BUFFER_SIZE];
							int read;
							while ((read = bs.Read(temp, 0, RESPONSE_BUFFER_SIZE)) != 0)
							{
								ms.Write(temp, 0, read);
							}
							responseData = ms.ToArray();

							// check it is correct size
							if (responseData.Length != SYNC_RESPONSE_SIZE)
							{
								throw new InvalidSyncResponseException(string.Format("Invalid response data size (expected " + SYNC_RESPONSE_SIZE + " got {0}", responseData.Length));
							}
						}
					}
				}

				// return data:
				// 00-07 server time (Big Endian)

				// extract the server time
				if (BitConverter.IsLittleEndian == true)
				{
					Array.Reverse(responseData);
				}
				// get the difference between the server time and our current time
				long serverTimeDiff = BitConverter.ToInt64(responseData, 0) - CurrentTime;

				// update the Data object
				ServerTimeDiff = serverTimeDiff;
				LastServerTime = DateTime.Now.Ticks;

				// clear any sync error
				_lastSyncError = DateTime.MinValue;
			}
			catch (WebException)
			{
				// don't retry for a while after error
				_lastSyncError = DateTime.Now;

				// set to zero to force reset
				ServerTimeDiff = 0;
			}
		}

		/// <summary>
		/// Restore an authenticator from the serial number and restore code.
		/// </summary>
		/// <param name="serial">serial code, e.g. US-1234-5678-1234</param>
		/// <param name="restoreCode">restore code given on enroll, 10 chars.</param>
		public void Restore(string serial, string restoreCode)
		{
			// get the serial data
			byte[] serialBytes = Encoding.UTF8.GetBytes(serial.ToUpper().Replace("-", string.Empty));

			// send the request to the server to get our challenge
			HttpWebRequest request = (HttpWebRequest)WebRequest.Create(GetMobileUrl(serial) + RESTORE_PATH);
			request.Method = "POST";
			request.ContentType = "application/octet-stream";
			request.ContentLength = serialBytes.Length;
			Stream requestStream = request.GetRequestStream();
			requestStream.Write(serialBytes, 0, serialBytes.Length);
			requestStream.Close();
			byte[] challenge = null;
			try
			{
				using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
				{
					// OK?
					if (response.StatusCode != HttpStatusCode.OK)
					{
						throw new InvalidRestoreResponseException(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription));
					}

					// load back the buffer - should only be a byte[32]
					using (MemoryStream ms = new MemoryStream())
					{
						using (Stream bs = response.GetResponseStream())
						{
							byte[] temp = new byte[RESPONSE_BUFFER_SIZE];
							int read;
							while ((read = bs.Read(temp, 0, RESPONSE_BUFFER_SIZE)) != 0)
							{
								ms.Write(temp, 0, read);
							}
							challenge = ms.ToArray();

							// check it is correct size
							if (challenge.Length != RESTOREINIT_BUFFER_SIZE)
							{
								throw new InvalidRestoreResponseException(string.Format("Invalid response data size (expected 32 got {0})", challenge.Length));
							}
						}
					}
				}
			}
			catch (WebException we)
			{
				int code = (int)((HttpWebResponse)we.Response).StatusCode;
				if (code >= 500 && code < 600)
				{
					throw new InvalidRestoreResponseException(string.Format("No response from server ({0}). Perhaps maintainence?", code));
				}
				else
				{
					throw new InvalidRestoreResponseException(string.Format("Error communicating with server: {0} - {1}", code, ((HttpWebResponse)we.Response).StatusDescription));
				}
			}

			// only take the first 10 bytes of the restore code and encode to byte taking count of the missing chars
			byte[] restoreCodeBytes = new byte[10];
			char[] arrayOfChar = restoreCode.ToUpper().ToCharArray();
			for (int i = 0; i < 10; i++)
			{
				restoreCodeBytes[i] = ConvertRestoreCodeCharToByte(arrayOfChar[i]);
			}

			// build the response to the challenge
			HMac hmac = new HMac(new Sha1Digest());
			hmac.Init(new KeyParameter(restoreCodeBytes));
			byte[] hashdata = new byte[serialBytes.Length + challenge.Length];
			Array.Copy(serialBytes, 0, hashdata, 0, serialBytes.Length);
			Array.Copy(challenge, 0, hashdata, serialBytes.Length, challenge.Length);
			hmac.BlockUpdate(hashdata, 0, hashdata.Length);
			byte[] hash = new byte[hmac.GetMacSize()];
			hmac.DoFinal(hash, 0);

			// create a random key
			byte[] oneTimePad = CreateOneTimePad(20);

			// concatanate the hash and key
			byte[] hashkey = new byte[hash.Length + oneTimePad.Length];
			Array.Copy(hash, 0, hashkey, 0, hash.Length);
			Array.Copy(oneTimePad, 0, hashkey, hash.Length, oneTimePad.Length);

			// encrypt the data with BMA public key
			RsaEngine rsa = new RsaEngine();
			rsa.Init(true, new RsaKeyParameters(false, new Org.BouncyCastle.Math.BigInteger(ENROLL_MODULUS, 16), new Org.BouncyCastle.Math.BigInteger(ENROLL_EXPONENT, 16)));
			byte[] encrypted = rsa.ProcessBlock(hashkey, 0, hashkey.Length);

			// prepend the serial to the encrypted data
			byte[] postbytes = new byte[serialBytes.Length + encrypted.Length];
			Array.Copy(serialBytes, 0, postbytes, 0, serialBytes.Length);
			Array.Copy(encrypted, 0, postbytes, serialBytes.Length, encrypted.Length);

			// send the challenge response back to the server
			request = (HttpWebRequest)WebRequest.Create(GetMobileUrl(serial) + RESTOREVALIDATE_PATH);
			request.Method = "POST";
			request.ContentType = "application/octet-stream";
			request.ContentLength = postbytes.Length;
			requestStream = request.GetRequestStream();
			requestStream.Write(postbytes, 0, postbytes.Length);
			requestStream.Close();
			byte[] secretKey = null;
			try {
				using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
				{
					// OK?
					if (response.StatusCode != HttpStatusCode.OK)
					{
						throw new InvalidRestoreResponseException(string.Format("{0}: {1}", (int)response.StatusCode, response.StatusDescription));
					}

					// load back the buffer - should only be a byte[32]
					using (MemoryStream ms = new MemoryStream())
					{
						using (Stream bs = response.GetResponseStream())
						{
							byte[] temp = new byte[RESPONSE_BUFFER_SIZE];
							int read;
							while ((read = bs.Read(temp, 0, RESPONSE_BUFFER_SIZE)) != 0)
							{
								ms.Write(temp, 0, read);
							}
							secretKey = ms.ToArray();

							// check it is correct size
							if (secretKey.Length != RESTOREVALIDATE_BUFFER_SIZE)
							{
								throw new InvalidRestoreResponseException(string.Format("Invalid response data size (expected " + RESTOREVALIDATE_BUFFER_SIZE + " got {0})", secretKey.Length));
							}
						}
					}
				}
			}
			catch (WebException we)
			{
				int code = (int)((HttpWebResponse)we.Response).StatusCode;
				if (code >= 500 && code < 600)
				{
					throw new InvalidRestoreResponseException(string.Format("No response from server ({0}). Perhaps maintainence?", code));
				}
				else if (code >= 600 && code < 700)
				{
					throw new InvalidRestoreCodeException("Invalid serial number or restore code.");
				}
				else
				{
					throw new InvalidRestoreResponseException(string.Format("Error communicating with server: {0} - {1}", code, ((HttpWebResponse)we.Response).StatusDescription));
				}
			}

			// xor the returned data key with our pad to get the actual secret key
			for (int i = oneTimePad.Length - 1; i >= 0; i--)
			{
				secretKey[i] ^= oneTimePad[i];
			}

			// set the authenticator data
			SecretKey = secretKey;
			if (serial.Length == 14)
			{
				Serial = serial.Substring(0, 2).ToUpper() + "-" + serial.Substring(2, 4) + "-" + serial.Substring(6, 4) + "-" + serial.Substring(10, 4);
			}
			else
			{
				Serial = serial.ToUpper();
			}
			// restore code is ok
			RestoreCodeVerified = true;
			// sync the time
			ServerTimeDiff = 0L;
			Sync();
		}

		/// <summary>
		/// Read any extra tags from the Xml
		/// </summary>
		/// <param name="reader">XmlReader</param>
		/// <param name="name">name of tag</param>
		/// <returns>true if read and processed the tag</returns>
		public override bool ReadExtraXml(XmlReader reader, string name)
		{
			switch (name)
			{
				case "restorecodeverified":
					RestoreCodeVerified = reader.ReadElementContentAsBoolean();
					return true;
				default:
					return false;
			}
		}

		/// <summary>
		/// Add extra tags into the XmlWriter
		/// </summary>
		/// <param name="writer">XmlWriter to write data</param>
		protected override void WriteExtraXml(XmlWriter writer)
		{
			if (RestoreCodeVerified == true)
			{
				writer.WriteStartElement("restorecodeverified");
				writer.WriteString(bool.TrueString.ToLower());
				writer.WriteEndElement();
			}
		}

		/// <summary>
		/// Get the base mobil url based on the region
		/// </summary>
		/// <param name="region">two letter region code, i.e US or CN</param>
		/// <returns>string of Url for region</returns>
		private static string GetMobileUrl(string region)
		{
			string upperregion = region.ToUpper();
			if (upperregion.Length > 2)
			{
				upperregion = upperregion.Substring(0,2);
			}
			if (MOBILE_URLS.ContainsKey(upperregion) == true)
			{
				return MOBILE_URLS[upperregion];
			}
			else
			{
				return MOBILE_URLS[REGION_US];
			}
		}

		/// <summary>
		/// Calculate the restore code for an authenticator. This is taken from the last 10 bytes of a digest of the serial and secret key,
		/// which is then specially encoded to alphanumerics.
		/// </summary>
		/// <returns>restore code for authenticator (always 10 chars)</returns>
		protected string BuildRestoreCode()
    {
			// return if not set
			if (string.IsNullOrEmpty(Serial) == true || SecretKey == null)
			{
				return string.Empty;
			}

			// get byte array of serial
			byte[] serialdata = Encoding.UTF8.GetBytes(Serial.ToUpper().Replace("-", string.Empty));
			byte[] secretdata = SecretKey;

			// combine serial data and secret data
			byte[] combined = new byte[serialdata.Length + secretdata.Length];
			Array.Copy(serialdata, 0, combined, 0, serialdata.Length);
			Array.Copy(secretdata, 0, combined, serialdata.Length, secretdata.Length);

			// create digest of combined data
			IDigest digest = new Sha1Digest();
			digest.BlockUpdate(combined, 0, combined.Length);
			byte[] digestdata = new byte[digest.GetDigestSize()];
			digest.DoFinal(digestdata, 0);

			// take last 10 chars of hash and convert each byte to our encoded string that doesn't use I,L,O,S
			StringBuilder code = new StringBuilder();
			int startpos = digestdata.Length - 10;
			for (int i = 0; i < 10; i++)
			{
				code.Append(ConvertRestoreCodeByteToChar(digestdata[startpos + i]));
			}

			return code.ToString();
    }

		/// <summary>
		/// Create a random Model string for initialization to armor the init string sent over the wire
		/// </summary>
		/// <returns>Random model string</returns>
		private static string GeneralRandomModel()
		{
			// seed a new RNG
			RNGCryptoServiceProvider randomSeedGenerator = new RNGCryptoServiceProvider();
			byte[] seedBuffer = new byte[4];
			randomSeedGenerator.GetBytes(seedBuffer);
			Random random = new Random(BitConverter.ToInt32(seedBuffer, 0));

			// create a model string with available characters
			StringBuilder model = new StringBuilder(MODEL_SIZE);
			for (int i = MODEL_SIZE; i > 0; i--)
			{
				model.Append(MODEL_CHARS[random.Next(MODEL_CHARS.Length)]);
			}

			return model.ToString();
		}

		#region Utility functions

		/// <summary>
		/// Convert a char to a byte but with appropriate mapping to exclude I,L,O and S. E.g. A=10 but J=18 not 19 (as I is missing)
		/// </summary>
		/// <param name="c">char to convert.</param>
		/// <returns>byte value of restore code char</returns>
		private static byte ConvertRestoreCodeCharToByte(char c)
		{
			if (c >= '0' && c <= '9')
			{
				return (byte)(c - '0');
			}
			else
			{
				byte index = (byte)(c + 10 - 65);
				if (c >= 'I')
				{
					index--;
				}
				if (c >= 'L')
				{
					index--;
				}
				if (c >= 'O')
				{
					index--;
				}
				if (c >= 'S')
				{
					index--;
				}

				return index;
			}
		}

		/// <summary>
		/// Convert a byte to a char but with appropriate mapping to exclude I,L,O and S.
		/// </summary>
		/// <param name="b">byte to convert.</param>
		/// <returns>char value of restore code value</returns>
		private static char ConvertRestoreCodeByteToChar(byte b)
		{
			int index = b & 0x1f;
			if (index <= 9)
			{
				return (char)(index + 48);
			}
			else
			{
				index = (index + 65) - 10;
				if (index >= 73)
				{
					index++;
				}
				if (index >= 76)
				{
					index++;
				}
				if (index >= 79)
				{
					index++;
				}
				if (index >= 83)
				{
					index++;
				}
				return (char)index;
			}
		}

		#endregion

	}
}
