mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-01 06:00:46 +00:00
295 lines
8.5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|