using JetBrains.Annotations; using Newtonsoft.Json.Linq; using SteamLib.Authentication; using SteamLib.Core; using SteamLib.Core.Enums; using SteamLib.Core.Interfaces; using SteamLib.Core.StatusCodes; using SteamLib.Exceptions; using SteamLib.Exceptions.Mobile; using SteamLib.ProtoCore; using SteamLib.ProtoCore.Enums; using SteamLib.ProtoCore.Exceptions; using SteamLib.ProtoCore.Services; using SteamLib.SteamMobile; using SteamLib.SteamMobile.AuthenticatorLinker; namespace SteamLib.Api.Mobile; [PublicAPI] public static class SteamAuthenticatorLinkerApi { private const string PHONE_REQ = SteamConstants.STEAM_COMMUNITY + "steamguard/phoneajax"; #region Global public static Task HasPhone(HttpClient client, string accessToken) { const string uri = SteamConstants.STEAM_API + "IPhoneService/AccountPhoneStatus/v1"; var reqUri = AddAccessToken(uri, accessToken); return client.PostProto(reqUri, request: null); } /// /// /// /// /// /// /// /// /// public static async Task LinkRequest(HttpClient client, ISessionData data, string deviceId) { const string uri = SteamConstants.STEAM_API + "ITwoFactorService/AddAuthenticator/v1"; data.EnsureValidated(); var reqUri = AddAccessToken(uri, data.GetAccessToken()); var req = new AddAuthenticator_Request { SteamId = (ulong)data.SteamId.Steam64.Id, AuthenticatorType = 1, DeviceIdentifier = deviceId, Version = 2 }; var resp = await client.PostProtoMsg(reqUri, req); if (resp is { Result: EResult.InvalidState, ResponseMsg.Status: 2 }) { throw new AuthenticatorLinkerException(AuthenticatorLinkerError.InvalidStateWithStatus2); } return resp.GetResponseEnsureSuccess(); } public static async Task FinalizeLink(HttpClient client, string confirmationCode, ISessionData data, byte[] sharedSecret, bool validateSmsCode) { const string uri = SteamConstants.STEAM_API + "ITwoFactorService/FinalizeAddAuthenticator/v1"; data.EnsureValidated(); var reqUri = AddAccessToken(uri, data.GetAccessToken()); var time = await TimeAligner.GetSteamTimeAsync(); if (validateSmsCode) { var validateSmsReq = new FinalizeAddAuthenticator_Request { SteamId = data.SteamId.Steam64.ToUlong(), AuthenticatorTime = (ulong)time, ConfirmationCode = confirmationCode, ValidateConfirmationCode = true }; var validateResp = await client.PostProto(reqUri, validateSmsReq); if (validateResp.Success == false || validateResp.Status != 2) { if (validateResp.Status == 89) throw new AuthenticatorLinkerException(AuthenticatorLinkerError.BadConfirmationCode); throw new AuthenticatorLinkerException("Can't accept sms code. Status: " + validateResp.Status); } } var tries = 0; while (tries < 30) { tries++; time = await TimeAligner.GetSteamTimeAsync(); var code = SteamGuardCodeGenerator.GenerateCode(sharedSecret, time); var req = new FinalizeAddAuthenticator_Request { SteamId = data.SteamId.Steam64.ToUlong(), AuthenticatorCode = code, AuthenticatorTime = (ulong)time, ConfirmationCode = confirmationCode, }; var resp = await client.PostProto(reqUri, req); if (resp.Success && resp.WantMore == false) { return new LinkResult(); } if (resp.Status == 89) { return new LinkResult(LinkError.BadConfirmationCode); } if (resp.Status == 88 && tries >= 30) { return new LinkResult(LinkError.UnableToGenerateCorrectCodes); } } return new LinkResult(LinkError.GeneralFailure); } public static async Task IsValidPhoneNumber(HttpClient client, long phoneNumber, string sessionId) { return await CheckPhoneNumber(client, phoneNumber, sessionId) == CheckPhoneResult.Valid; } public static async Task CheckPhoneNumber(HttpClient client, long phoneNumber, string sessionId) { var phone = '+' + phoneNumber.ToString(); var data = new Dictionary { {"phoneNumber", phone}, {"sessionID", sessionId} }; var reqContent = new FormUrlEncodedContent(data); var req = new HttpRequestMessage(HttpMethod.Post, SteamConstants.STEAM_STORE + "phone/validate"); req.Content = reqContent; req.Headers.Referrer = new Uri("https://store.steampowered.com/phone/add"); var resp = await client.SendAsync(req); var content = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); var j = JObject.Parse(content); if (j["success"]!.Value() == false) return CheckPhoneResult.GeneralFailure; if (j["is_voip"]!.Value()) return CheckPhoneResult.VoIp; return j["is_valid"]!.Value() ? CheckPhoneResult.Valid : CheckPhoneResult.NotValid; } public static async Task AttachPhone(HttpClient client, long phoneNumber, string accessToken) { const string uri = SteamConstants.STEAM_API + "IPhoneService/SetAccountPhoneNumber/v1"; var phone = '+' + phoneNumber.ToString(); var req = new SetAccountPhoneNumber_Request { PhoneNumber = phone }; var reqUri = AddAccessToken(uri, accessToken); var resp = await client.PostProtoMsg(reqUri, req); return resp.Result == EResult.Pending; } public static async Task CheckEmailConfirmation(HttpClient client, string accessToken) { const string uri = SteamConstants.STEAM_API + "IPhoneService/IsAccountWaitingForEmailConfirmation/v1"; var reqUri = AddAccessToken(uri, accessToken); var i = 0; while (i < 5) { i++; var resp = await client.PostProto(reqUri, new EmptyMessage()); if (resp.IsWaiting == false) return true; await Task.Delay(resp.SecondsToWait * 1000); } return false; } public static Task SendSmsCode(HttpClient client, string accessToken) { const string uri = SteamConstants.STEAM_API + "IPhoneService/SendPhoneVerificationCode/v1"; var reqUri = AddAccessToken(uri, accessToken); return client.PostProto(reqUri, new SendPhoneVerificationCode_Request()); } public static async Task CheckSmsCode(HttpClient client, int smsCode, string sessionId) { var data = new Dictionary { {"op", "check_sms_code"}, {"arg", smsCode.ToString()}, {"checkfortos", "0"}, {"skipvoip", "1"}, {"sessionid", sessionId} }; var content = new FormUrlEncodedContent(data); var resp = await client.PostAsync(PHONE_REQ, content); var respContent = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); var success = SteamStatusCode.Translate(respContent, out _).Equals(SteamStatusCode.Ok); if (success) return true; await Task.Delay(5000); var hasPhone = await HasPhone(client, sessionId); return hasPhone.HasPhone; } private static FormUrlEncodedContent CreateAjaxContent(string op, string arg, string sessionId) { var data = new Dictionary { {"op",op}, {"arg", arg}, {"sessionid", sessionId} }; return new FormUrlEncodedContent(data); } public static string GenerateDeviceId() => "android:" + Guid.NewGuid(); private static string AddAccessToken(string uri, string accessToken) => uri + "?access_token=" + accessToken; #endregion #region ForLinker internal static Task LinkRequest(this SteamAuthenticatorLinker linker) { if (linker.SessionData == null) throw new InvalidOperationException("SessionData is null"); return LinkRequest(linker.Client, linker.SessionData, linker.DeviceId ??= GenerateDeviceId()); } internal static Task IsValidPhoneNumber(this SteamAuthenticatorLinker linker, long phoneNumber) { if (linker.SessionData == null) throw new InvalidOperationException("SessionData is null"); return IsValidPhoneNumber(linker.Client, phoneNumber, linker.SessionData.SessionId); } internal static Task HasPhone(this SteamAuthenticatorLinker linker) { if (linker.SessionData == null) throw new InvalidOperationException("SessionData is null"); return HasPhone(linker.Client, linker.SessionData.GetAccessToken()); } internal static Task AttachPhone(this SteamAuthenticatorLinker linker, long phoneNumber) { if (linker.SessionData == null) throw new InvalidOperationException("SessionData is null"); return AttachPhone(linker.Client, phoneNumber, linker.SessionData.GetAccessToken()); } internal static Task CheckEmailConfirmation(this SteamAuthenticatorLinker linker) { if (linker.SessionData == null) throw new InvalidOperationException("SessionData is null"); return CheckEmailConfirmation(linker.Client, linker.SessionData.GetAccessToken()); } internal static Task SendSmsCode(this SteamAuthenticatorLinker linker) { if (linker.SessionData == null) throw new InvalidOperationException("SessionData is null"); return SendSmsCode(linker.Client, linker.SessionData.GetAccessToken()); } internal static Task CheckSmsCode(this SteamAuthenticatorLinker linker, int smsCode) { if (linker.SessionData == null) throw new InvalidOperationException("SessionData is null"); return CheckSmsCode(linker.Client, smsCode, linker.SessionData.SessionId); } internal static Task FinalizeLink(this SteamAuthenticatorLinker linker, string confirmationCode, byte[] sharedSecret, bool validateSmsCode) { if (linker.SessionData == null) throw new InvalidOperationException("SessionData is null"); return FinalizeLink(linker.Client, confirmationCode, linker.SessionData, sharedSecret, validateSmsCode); } #endregion private static string GetAccessToken(this ISessionData s) => s.GetToken(SteamDomain.Community)?.Token ?? throw new SessionInvalidException("Access token was null. MobileEndpoints requires valid AccessToken"); }