mirror of
https://github.com/BeyondDimension/SteamTools.git
synced 2026-10-18 22:50:50 +08:00
798355e342
新增反代脚本只对Steam内置浏览器生效的选项
913 lines
27 KiB
C#
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
|
|
|
|
}
|
|
}
|