diff --git a/DepotDownloaderMod/AccountSettingsStore.cs b/DepotDownloaderMod/AccountSettingsStore.cs index c6c9a2510..b1c4e13d7 100644 --- a/DepotDownloaderMod/AccountSettingsStore.cs +++ b/DepotDownloaderMod/AccountSettingsStore.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -20,13 +20,18 @@ class AccountSettingsStore [ProtoMember(3, IsRequired = false)] public Dictionary LoginKeys { get; private set; } + // Member 3 was a Dictionary for LoginKeys. + + [ProtoMember(4, IsRequired = false)] + public Dictionary LoginTokens { get; private set; } + string FileName; AccountSettingsStore() { SentryData = new Dictionary(); ContentServerPenalty = new ConcurrentDictionary(); - LoginKeys = new Dictionary(); + LoginTokens = new Dictionary(); } static bool Loaded diff --git a/DepotDownloaderMod/ContentDownloader.cs b/DepotDownloaderMod/ContentDownloader.cs index 5f0595a42..e93e8fdc8 100644 --- a/DepotDownloaderMod/ContentDownloader.cs +++ b/DepotDownloaderMod/ContentDownloader.cs @@ -24,7 +24,7 @@ static class ContentDownloader public const uint INVALID_APP_ID = uint.MaxValue; public const uint INVALID_DEPOT_ID = uint.MaxValue; public const ulong INVALID_MANIFEST_ID = ulong.MaxValue; - public const string DEFAULT_BRANCH = "Public"; + public const string DEFAULT_BRANCH = "public"; public static DownloadConfig Config = new DownloadConfig(); @@ -247,9 +247,9 @@ static ulong GetSteam3DepotManifest(uint depotId, uint appId, string branch) if (manifests.Children.Count == 0 && manifests_encrypted.Children.Count == 0) return INVALID_MANIFEST_ID; - var node = manifests[branch].Children.Count > 0 ? manifests[branch]["gid"] : manifests[branch]; + var node = manifests[branch]["gid"]; - if (branch != "Public" && node == KeyValue.Invalid) + if (node == KeyValue.Invalid && !string.Equals(branch, DEFAULT_BRANCH, StringComparison.OrdinalIgnoreCase)) { var node_encrypted = manifests_encrypted[branch]; if (node_encrypted != KeyValue.Invalid) @@ -261,35 +261,23 @@ static ulong GetSteam3DepotManifest(uint depotId, uint appId, string branch) Config.BetaPassword = password = Console.ReadLine(); } - var encrypted_v1 = node_encrypted["encrypted_gid"]; - var encrypted_v2 = node_encrypted["encrypted_gid_2"]; + var encrypted_gid = node_encrypted["gid"]; - if (encrypted_v1 != KeyValue.Invalid) + if (encrypted_gid == KeyValue.Invalid) { - var input = Util.DecodeHexString(encrypted_v1.Value); - var manifest_bytes = CryptoHelper.VerifyAndDecryptPassword(input, password); - - if (manifest_bytes == null) - { - Console.WriteLine("Password was invalid for branch {0}", branch); - return INVALID_MANIFEST_ID; - } - - return BitConverter.ToUInt64(manifest_bytes, 0); + encrypted_gid = node_encrypted["encrypted_gid_2"]; } - if (encrypted_v2 != KeyValue.Invalid) + if (encrypted_gid != KeyValue.Invalid) { // Submit the password to Steam now to get encryption keys steam3.CheckAppBetaPassword(appId, Config.BetaPassword); - if (!steam3.AppBetaPasswords.ContainsKey(branch)) { Console.WriteLine("Password was invalid for branch {0}", branch); return INVALID_MANIFEST_ID; } - - var input = Util.DecodeHexString(encrypted_v2.Value); + var input = Util.DecodeHexString(encrypted_gid.Value); byte[] manifest_bytes; try { @@ -300,20 +288,15 @@ static ulong GetSteam3DepotManifest(uint depotId, uint appId, string branch) Console.WriteLine("Failed to decrypt branch {0}: {1}", branch, e.Message); return INVALID_MANIFEST_ID; } - return BitConverter.ToUInt64(manifest_bytes, 0); } - Console.WriteLine("Unhandled depot encryption for depotId {0}", depotId); return INVALID_MANIFEST_ID; } - return INVALID_MANIFEST_ID; } - if (node.Value == null) return INVALID_MANIFEST_ID; - return UInt64.Parse(node.Value); } @@ -328,11 +311,11 @@ static string GetAppName(uint depotId, uint appId) public static bool InitializeSteam3(string username, string password) { - string loginKey = null; + string loginToken = null; if (username != null && Config.RememberPassword) { - _ = AccountSettingsStore.Instance.LoginKeys.TryGetValue(username, out loginKey); + _ = AccountSettingsStore.Instance.LoginTokens.TryGetValue(username, out loginToken); } @@ -346,9 +329,9 @@ public static bool InitializeSteam3(string username, string password) { SentryFileHash = sentryFileHash, Username = username, - Password = loginKey == null ? password : null, + Password = loginToken == null ? password : null, ShouldRememberPassword = Config.RememberPassword, - LoginKey = loginKey, + AccessToken = loginToken, LoginID = Config.LoginID ?? 0x534B32, // "SK2" } ); @@ -359,9 +342,9 @@ public static bool InitializeSteam3(string username, string password) new SteamUser.LogOnDetails { Username = username, - Password = loginKey == null ? password : null, + Password = loginToken == null ? password : null, ShouldRememberPassword = Config.RememberPassword, - LoginKey = loginKey, + AccessToken = loginToken, LoginID = Config.LoginID ?? 0x534B32, // "SK2" } ); @@ -392,7 +375,6 @@ public static void ShutdownSteam3() if (steam3 == null) return; - steam3.TryWaitForLoginKey(); steam3.Disconnect(); } @@ -636,10 +618,10 @@ static DepotDownloadInfo GetDepotInfo(uint depotId, uint appId, ulong manifestId if (manifestId == INVALID_MANIFEST_ID) { manifestId = GetSteam3DepotManifest(depotId, appId, branch); - if (manifestId == INVALID_MANIFEST_ID && branch != "public") + if (manifestId == INVALID_MANIFEST_ID && !string.Equals(branch, DEFAULT_BRANCH, StringComparison.OrdinalIgnoreCase)) { - Console.WriteLine("Warning: Depot {0} does not have branch named \"{1}\". Trying public branch.", depotId, branch); - branch = "public"; + Console.WriteLine("Warning: Depot {0} does not have branch named \"{1}\". Trying {2} branch.", depotId, branch, DEFAULT_BRANCH); + branch = DEFAULT_BRANCH; manifestId = GetSteam3DepotManifest(depotId, appId, branch); } diff --git a/DepotDownloaderMod/DepotDownloaderMod.csproj b/DepotDownloaderMod/DepotDownloaderMod.csproj index 88463abcb..c75b73120 100644 --- a/DepotDownloaderMod/DepotDownloaderMod.csproj +++ b/DepotDownloaderMod/DepotDownloaderMod.csproj @@ -13,6 +13,7 @@ + diff --git a/DepotDownloaderMod/DownloadConfig.cs b/DepotDownloaderMod/DownloadConfig.cs index 2c643f5f2..b1841569b 100644 --- a/DepotDownloaderMod/DownloadConfig.cs +++ b/DepotDownloaderMod/DownloadConfig.cs @@ -22,12 +22,13 @@ class DownloadConfig public int MaxServers { get; set; } public int MaxDownloads { get; set; } - public string SuppliedPassword { get; set; } public bool RememberPassword { get; set; } // A Steam LoginID to allow multiple concurrent connections public uint? LoginID { get; set; } + public bool UseQrCode { get; set; } + public bool UseManifestFile { get; set; } public string ManifestFile { get; set; } } diff --git a/DepotDownloaderMod/Program.cs b/DepotDownloaderMod/Program.cs index 534d31f1a..111f30fd8 100644 --- a/DepotDownloaderMod/Program.cs +++ b/DepotDownloaderMod/Program.cs @@ -68,7 +68,7 @@ static async Task MainAsync(string[] args) ContentDownloader.Config.RememberPassword = HasParameter(args, "-remember-password"); - + ContentDownloader.Config.UseQrCode = HasParameter(args, "-qr"); ContentDownloader.Config.DownloadManifestOnly = HasParameter(args, "-manifest-only"); var cellId = GetParameter(args, "-cellid", -1); @@ -312,32 +312,32 @@ ex is ContentDownloaderException static bool InitializeSteam(string username, string password) { - if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginKeys.ContainsKey(username))) + if (!ContentDownloader.Config.UseQrCode) { - do + if (username != null && password == null && (!ContentDownloader.Config.RememberPassword || !AccountSettingsStore.Instance.LoginTokens.ContainsKey(username))) { - Console.Write("Enter account password for \"{0}\": ", username); - if (Console.IsInputRedirected) - { - password = Console.ReadLine(); - } - else + do { - // Avoid console echoing of password - password = Util.ReadPassword(); - } + Console.Write("Enter account password for \"{0}\": ", username); + if (Console.IsInputRedirected) + { + password = Console.ReadLine(); + } + else + { + // Avoid console echoing of password + password = Util.ReadPassword(); + } - Console.WriteLine(); - } while (string.Empty == password); - } - else if (username == null) - { - Console.WriteLine("No username given. Using anonymous account with dedicated server subscription."); + Console.WriteLine(); + } while (string.Empty == password); + } + else if (username == null) + { + Console.WriteLine("No username given. Using anonymous account with dedicated server subscription."); + } } - // capture the supplied password in case we need to re-use it after checking the login key - ContentDownloader.Config.SuppliedPassword = password; - return ContentDownloader.InitializeSteam3(username, password); } @@ -419,7 +419,7 @@ static void PrintUsage() Console.WriteLine("\t-app <#>\t\t\t\t- the AppID to download."); Console.WriteLine("\t-depot <#>\t\t\t\t- the DepotID to download."); Console.WriteLine("\t-manifest \t\t\t- manifest id of content to download (requires -depot, default: current for branch)."); - Console.WriteLine("\t-beta \t\t\t- download from specified branch if available (default: Public)."); + Console.WriteLine($"\t-beta \t\t\t- download from specified branch if available (default: {ContentDownloader.DEFAULT_BRANCH})."); Console.WriteLine("\t-betapassword \t\t- branch password if applicable."); Console.WriteLine("\t-all-platforms\t\t\t- downloads all platform-specific depots when -app is used."); Console.WriteLine("\t-os \t\t\t\t- the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)"); diff --git a/DepotDownloaderMod/Steam3Session.cs b/DepotDownloaderMod/Steam3Session.cs index 624380b71..2e9a18ca2 100644 --- a/DepotDownloaderMod/Steam3Session.cs +++ b/DepotDownloaderMod/Steam3Session.cs @@ -4,9 +4,11 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; +using QRCoder; using System.Threading; using System.Threading.Tasks; using SteamKit2; +using SteamKit2.Authentication; using SteamKit2.Internal; namespace DepotDownloader @@ -58,6 +60,7 @@ public ReadOnlyCollection Licenses int connectionBackoff; int seq; // more hack fixes DateTime connectTime; + AuthSession authSession; // input readonly SteamUser.LogOnDetails logonDetails; @@ -72,14 +75,13 @@ public Steam3Session(SteamUser.LogOnDetails details) { this.logonDetails = details; - this.authenticatedUser = details.Username != null; + this.authenticatedUser = details.Username != null || ContentDownloader.Config.UseQrCode; this.credentials = new Credentials(); this.bConnected = false; this.bConnecting = false; this.bAborted = false; this.bExpectingDisconnectRemote = false; this.bDidDisconnect = false; - this.bDidReceiveLoginKey = false; this.seq = 0; this.AppTokens = new Dictionary(); @@ -111,11 +113,10 @@ public Steam3Session(SteamUser.LogOnDetails details) this.callbacks.Subscribe(SessionTokenCallback); this.callbacks.Subscribe(LicenseListCallback); this.callbacks.Subscribe(UpdateMachineAuthCallback); - this.callbacks.Subscribe(LoginKeyCallback); Console.Write("Connecting to Steam3..."); - if (authenticatedUser) + if (details.Username != null) { var fi = new FileInfo(String.Format("{0}.sentryFile", logonDetails.Username)); if (AccountSettingsStore.Instance.SentryData != null && AccountSettingsStore.Instance.SentryData.ContainsKey(logonDetails.Username)) @@ -475,7 +476,6 @@ private void ResetConnectionFlags() bExpectingDisconnectRemote = false; bDidDisconnect = false; bIsConnectionRecovery = false; - bDidReceiveLoginKey = false; } void Connect() @@ -484,6 +484,7 @@ void Connect() bConnected = false; bConnecting = true; connectionBackoff = 0; + authSession = null; ResetConnectionFlags(); @@ -522,23 +523,6 @@ private void Reconnect() steamClient.Disconnect(); } - public void TryWaitForLoginKey() - { - if (logonDetails.Username == null || !credentials.LoggedOn || !ContentDownloader.Config.RememberPassword) return; - - var totalWaitPeriod = DateTime.Now.AddSeconds(3); - - while (true) - { - var now = DateTime.Now; - if (now >= totalWaitPeriod) break; - - if (bDidReceiveLoginKey) break; - - callbacks.RunWaitAllCallbacks(TimeSpan.FromMilliseconds(100)); - } - } - private void WaitForCallbacks() { callbacks.RunWaitCallbacks(TimeSpan.FromSeconds(1)); @@ -552,11 +536,17 @@ private void WaitForCallbacks() } } - private void ConnectedCallback(SteamClient.ConnectedCallback connected) + private async void ConnectedCallback(SteamClient.ConnectedCallback connected) { Console.WriteLine(" Done!"); bConnecting = false; bConnected = true; + + // Update our tracking so that we don't time out, even if we need to reconnect multiple times, + // e.g. if the authentication phase takes a while and therefore multiple connections. + connectTime = DateTime.Now; + connectionBackoff = 0; + if (!authenticatedUser) { Console.Write("Logging anonymously into Steam3..."); @@ -564,7 +554,101 @@ private void ConnectedCallback(SteamClient.ConnectedCallback connected) } else { - Console.Write("Logging '{0}' into Steam3...", logonDetails.Username); + if (logonDetails.Username != null) + { + Console.WriteLine("Logging '{0}' into Steam3...", logonDetails.Username); + } + + if (authSession is null) + { + if (logonDetails.Username != null && logonDetails.Password != null && logonDetails.AccessToken is null) + { + try + { + authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new SteamKit2.Authentication.AuthSessionDetails + { + Username = logonDetails.Username, + Password = logonDetails.Password, + IsPersistentSession = ContentDownloader.Config.RememberPassword, + Authenticator = new UserConsoleAuthenticator(), + }); + } + catch (TaskCanceledException) + { + return; + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to authenticate with Steam: " + ex.Message); + Abort(false); + return; + } + } + else if (logonDetails.AccessToken is null && ContentDownloader.Config.UseQrCode) + { + Console.WriteLine("Logging in with QR code..."); + + try + { + var session = await steamClient.Authentication.BeginAuthSessionViaQRAsync(new AuthSessionDetails + { + IsPersistentSession = ContentDownloader.Config.RememberPassword, + Authenticator = new UserConsoleAuthenticator(), + }); + + authSession = session; + + // Steam will periodically refresh the challenge url, so we need a new QR code. + session.ChallengeURLChanged = () => + { + Console.WriteLine(); + Console.WriteLine("The QR code has changed:"); + DisplayQrCode(session.ChallengeURL); + }; + + // Draw initial QR code immediately + DisplayQrCode(session.ChallengeURL); + } + catch (TaskCanceledException) + { + return; + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to authenticate with Steam: " + ex.Message); + Abort(false); + return; + } + } + } + + if (authSession != null) + { + try + { + var result = await authSession.PollingWaitForResultAsync(); + + logonDetails.Username = result.AccountName; + logonDetails.Password = null; + logonDetails.AccessToken = result.RefreshToken; + + AccountSettingsStore.Instance.LoginTokens[result.AccountName] = result.RefreshToken; + AccountSettingsStore.Save(); + } + catch (TaskCanceledException) + { + return; + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to authenticate with Steam: " + ex.Message); + Abort(false); + return; + } + + authSession = null; + } + steamUser.LogOn(logonDetails); } } @@ -573,6 +657,7 @@ private void DisconnectedCallback(SteamClient.DisconnectedCallback disconnected) { bDidDisconnect = true; + DebugLog.WriteLine(nameof(Steam3Session), $"Disconnected: bIsConnectionRecovery = {bIsConnectionRecovery}, UserInitiated = {disconnected.UserInitiated}, bExpectingDisconnectRemote = {bExpectingDisconnectRemote}"); // When recovering the connection, we want to reconnect even if the remote disconnects us if (!bIsConnectionRecovery && (disconnected.UserInitiated || bExpectingDisconnectRemote)) { @@ -609,14 +694,14 @@ private void LogOnCallback(SteamUser.LoggedOnCallback loggedOn) { var isSteamGuard = loggedOn.Result == EResult.AccountLogonDenied; var is2FA = loggedOn.Result == EResult.AccountLoginDeniedNeedTwoFactor; - var isLoginKey = ContentDownloader.Config.RememberPassword && logonDetails.LoginKey != null && loggedOn.Result == EResult.InvalidPassword; + var isAccessToken = ContentDownloader.Config.RememberPassword && logonDetails.AccessToken != null && loggedOn.Result == EResult.InvalidPassword; // TODO: Get EResult for bad access token - if (isSteamGuard || is2FA || isLoginKey) + if (isSteamGuard || is2FA || isAccessToken) { bExpectingDisconnectRemote = true; Abort(false); - if (!isLoginKey) + if (!isAccessToken) { Console.WriteLine("This account is protected by Steam Guard."); } @@ -629,23 +714,15 @@ private void LogOnCallback(SteamUser.LoggedOnCallback loggedOn) logonDetails.TwoFactorCode = Console.ReadLine(); } while (String.Empty == logonDetails.TwoFactorCode); } - else if (isLoginKey) + else if (isAccessToken) { - AccountSettingsStore.Instance.LoginKeys.Remove(logonDetails.Username); + AccountSettingsStore.Instance.LoginTokens.Remove(logonDetails.Username); AccountSettingsStore.Save(); - logonDetails.LoginKey = null; - - if (ContentDownloader.Config.SuppliedPassword != null) - { - Console.WriteLine("Login key was expired. Connecting with supplied password."); - logonDetails.Password = ContentDownloader.Config.SuppliedPassword; - } - else - { - Console.Write("Login key was expired. Please enter your password: "); - logonDetails.Password = Util.ReadPassword(); - } + // TODO: Handle gracefully by falling back to password prompt? + Console.WriteLine("Access token was rejected."); + Abort(false); + return; } else { @@ -736,7 +813,7 @@ private void UpdateMachineAuthCallback(SteamUser.UpdateMachineAuthCallback machi var hash = Util.SHAHash(machineAuth.Data); - Console.WriteLine("Got Machine Auth: {0} {1} {2} {3}", machineAuth.FileName, machineAuth.Offset, machineAuth.BytesToWrite, machineAuth.Data.Length, hash); + Console.WriteLine("Got Machine Auth: {0} {1} {2} {3}", machineAuth.FileName, machineAuth.Offset, machineAuth.BytesToWrite, machineAuth.Data.Length); AccountSettingsStore.Instance.SentryData[logonDetails.Username] = machineAuth.Data; AccountSettingsStore.Save(); @@ -762,16 +839,16 @@ private void UpdateMachineAuthCallback(SteamUser.UpdateMachineAuthCallback machi steamUser.SendMachineAuthResponse(authResponse); } - private void LoginKeyCallback(SteamUser.LoginKeyCallback loginKey) + private static void DisplayQrCode(string challengeUrl) { - Console.WriteLine("Accepted new login key for account {0}", logonDetails.Username); - - AccountSettingsStore.Instance.LoginKeys[logonDetails.Username] = loginKey.LoginKey; - AccountSettingsStore.Save(); - - steamUser.AcceptNewLoginKey(loginKey); - - bDidReceiveLoginKey = true; + // Encode the link as a QR code + using var qrGenerator = new QRCodeGenerator(); + var qrCodeData = qrGenerator.CreateQrCode(challengeUrl, QRCodeGenerator.ECCLevel.L); + using var qrCode = new AsciiQRCode(qrCodeData); + var qrCodeAsAsciiArt = qrCode.GetGraphic(1, drawQuietZones: false); + + Console.WriteLine("Use the Steam Mobile App to sign in with this QR code:"); + Console.WriteLine(qrCodeAsAsciiArt); } } }