Files
rmbgame 798355e342 更新目录结构
新增反代脚本只对Steam内置浏览器生效的选项
2021-01-06 04:30:56 +08:00

913 lines
27 KiB
C#

/*
* 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
}
}