Files
ArchiSteamFarm/ArchiSteamFarm/MobileAuthenticator.cs
2016-06-19 12:20:12 +02:00

295 lines
8.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.JSON;
using HtmlAgilityPack;
using Newtonsoft.Json;
namespace ArchiSteamFarm {
internal sealed class MobileAuthenticator {
internal sealed class Confirmation {
internal readonly uint ID;
internal readonly ulong Key;
internal readonly Steam.ConfirmationDetails.EType Type;
internal Confirmation(uint id, ulong key, Steam.ConfirmationDetails.EType type) {
if ((id == 0) || (key == 0) || (type == Steam.ConfirmationDetails.EType.Unknown)) {
throw new ArgumentNullException(nameof(id) + " || " + nameof(key) + " || " + nameof(type));
}
ID = id;
Key = key;
Type = type;
}
}
private static readonly byte[] TokenCharacters = { 50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89 };
private static readonly SemaphoreSlim TimeSemaphore = new SemaphoreSlim(1);
private static short SteamTimeDifference;
internal bool HasDeviceID => !string.IsNullOrEmpty(DeviceID);
[JsonProperty(PropertyName = "shared_secret", Required = Required.DisallowNull)]
private string SharedSecret;
[JsonProperty(PropertyName = "identity_secret", Required = Required.DisallowNull)]
private string IdentitySecret;
[JsonProperty(PropertyName = "device_id")]
private string DeviceID;
private Bot Bot;
internal static MobileAuthenticator LoadFromSteamGuardAccount(ObsoleteSteamGuardAccount sga) {
if (sga != null) {
return new MobileAuthenticator {
SharedSecret = sga.SharedSecret,
IdentitySecret = sga.IdentitySecret,
DeviceID = sga.DeviceID
};
}
Logging.LogNullError(nameof(sga));
return null;
}
private MobileAuthenticator() {
}
internal void Init(Bot bot) {
if (bot == null) {
throw new ArgumentNullException(nameof(bot));
}
Bot = bot;
}
internal void CorrectDeviceID(string deviceID) {
if (string.IsNullOrEmpty(deviceID)) {
Logging.LogNullError(nameof(deviceID), Bot.BotName);
return;
}
DeviceID = deviceID;
}
internal async Task<bool> HandleConfirmation(Confirmation confirmation, bool accept) {
if (confirmation == null) {
Logging.LogNullError(nameof(confirmation), Bot.BotName);
return false;
}
uint time = await GetSteamTime().ConfigureAwait(false);
if (time == 0) {
Logging.LogNullError(nameof(time), Bot.BotName);
return false;
}
string confirmationHash = GenerateConfirmationKey(time, "conf");
if (!string.IsNullOrEmpty(confirmationHash)) {
return await Bot.ArchiWebHandler.HandleConfirmation(DeviceID, confirmationHash, time, confirmation.ID, confirmation.Key, accept).ConfigureAwait(false);
}
Logging.LogNullError(nameof(confirmationHash), Bot.BotName);
return false;
}
internal async Task<Steam.ConfirmationDetails> GetConfirmationDetails(Confirmation confirmation) {
if (confirmation == null) {
Logging.LogNullError(nameof(confirmation), Bot.BotName);
return null;
}
uint time = await GetSteamTime().ConfigureAwait(false);
if (time == 0) {
Logging.LogNullError(nameof(time), Bot.BotName);
return null;
}
string confirmationHash = GenerateConfirmationKey(time, "conf");
if (string.IsNullOrEmpty(confirmationHash)) {
Logging.LogNullError(nameof(confirmationHash), Bot.BotName);
return null;
}
Steam.ConfirmationDetails response = await Bot.ArchiWebHandler.GetConfirmationDetails(DeviceID, confirmationHash, time, confirmation.ID).ConfigureAwait(false);
if ((response == null) || !response.Success) {
return null;
}
return response;
}
internal async Task<string> GenerateToken() {
uint time = await GetSteamTime().ConfigureAwait(false);
if (time != 0) {
return GenerateTokenForTime(time);
}
Logging.LogNullError(nameof(time), Bot.BotName);
return null;
}
internal async Task<HashSet<Confirmation>> GetConfirmations() {
uint time = await GetSteamTime().ConfigureAwait(false);
if (time == 0) {
Logging.LogNullError(nameof(time), Bot.BotName);
return null;
}
string confirmationHash = GenerateConfirmationKey(time, "conf");
if (string.IsNullOrEmpty(confirmationHash)) {
Logging.LogNullError(nameof(confirmationHash), Bot.BotName);
return null;
}
HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetConfirmations(DeviceID, confirmationHash, time).ConfigureAwait(false);
if (htmlDocument == null) {
return null;
}
HtmlNodeCollection confirmationNodes = htmlDocument.DocumentNode.SelectNodes("//div[@class='mobileconf_list_entry']");
if (confirmationNodes == null) {
return null;
}
HashSet<Confirmation> result = new HashSet<Confirmation>();
foreach (HtmlNode confirmationNode in confirmationNodes) {
string idString = confirmationNode.GetAttributeValue("data-confid", null);
if (string.IsNullOrEmpty(idString)) {
Logging.LogNullError(nameof(idString), Bot.BotName);
continue;
}
uint id;
if (!uint.TryParse(idString, out id) || (id == 0)) {
Logging.LogNullError(nameof(id), Bot.BotName);
continue;
}
string keyString = confirmationNode.GetAttributeValue("data-key", null);
if (string.IsNullOrEmpty(keyString)) {
Logging.LogNullError(nameof(keyString), Bot.BotName);
continue;
}
ulong key;
if (!ulong.TryParse(keyString, out key) || (key == 0)) {
Logging.LogNullError(nameof(key), Bot.BotName);
continue;
}
HtmlNode descriptionNode = confirmationNode.SelectSingleNode(".//div[@class='mobileconf_list_entry_description']/div");
if (descriptionNode == null) {
Logging.LogNullError(nameof(descriptionNode), Bot.BotName);
continue;
}
Steam.ConfirmationDetails.EType type;
string description = descriptionNode.InnerText;
if (description.Equals("Sell - Market Listing")) {
type = Steam.ConfirmationDetails.EType.Market;
} else if (description.StartsWith("Trade with ", StringComparison.Ordinal)) {
type = Steam.ConfirmationDetails.EType.Trade;
} else {
type = Steam.ConfirmationDetails.EType.Other;
}
result.Add(new Confirmation(id, key, type));
}
return result;
}
internal async Task<uint> GetSteamTime() {
if (SteamTimeDifference != 0) {
return (uint) (Utilities.GetUnixTime() + SteamTimeDifference);
}
await TimeSemaphore.WaitAsync().ConfigureAwait(false);
if (SteamTimeDifference == 0) {
uint serverTime = Bot.ArchiWebHandler.GetServerTime();
if (serverTime != 0) {
SteamTimeDifference = (short) (serverTime - Utilities.GetUnixTime());
}
}
TimeSemaphore.Release();
return (uint) (Utilities.GetUnixTime() + SteamTimeDifference);
}
private string GenerateTokenForTime(long time) {
if (time == 0) {
Logging.LogNullError(nameof(time), Bot.BotName);
return null;
}
byte[] sharedSecretArray = Convert.FromBase64String(SharedSecret);
byte[] timeArray = new byte[8];
time /= 30L;
for (int i = 8; i > 0; i--) {
timeArray[i - 1] = (byte) time;
time >>= 8;
}
byte[] hashedData;
using (HMACSHA1 hmacGenerator = new HMACSHA1(sharedSecretArray, true)) {
hashedData = hmacGenerator.ComputeHash(timeArray);
}
byte b = (byte) (hashedData[19] & 0xF);
int codePoint = ((hashedData[b] & 0x7F) << 24) | ((hashedData[b + 1] & 0xFF) << 16) | ((hashedData[b + 2] & 0xFF) << 8) | (hashedData[b + 3] & 0xFF);
byte[] codeArray = new byte[5];
for (int i = 0; i < 5; ++i) {
codeArray[i] = TokenCharacters[codePoint % TokenCharacters.Length];
codePoint /= TokenCharacters.Length;
}
return Encoding.UTF8.GetString(codeArray);
}
private string GenerateConfirmationKey(uint time, string tag = null) {
if (time == 0) {
Logging.LogNullError(nameof(time), Bot.BotName);
return null;
}
byte[] b64Secret = Convert.FromBase64String(IdentitySecret);
int bufferSize = 8;
if (string.IsNullOrEmpty(tag) == false) {
bufferSize += Math.Min(32, tag.Length);
}
byte[] buffer = new byte[bufferSize];
byte[] timeArray = BitConverter.GetBytes((long) time);
if (BitConverter.IsLittleEndian) {
Array.Reverse(timeArray);
}
Array.Copy(timeArray, buffer, 8);
if (string.IsNullOrEmpty(tag) == false) {
Array.Copy(Encoding.UTF8.GetBytes(tag), 0, buffer, 8, bufferSize - 8);
}
byte[] hash;
using (HMACSHA1 hmac = new HMACSHA1(b64Secret, true)) {
hash = hmac.ComputeHash(buffer);
}
return Convert.ToBase64String(hash, Base64FormattingOptions.None);
}
}
}