Add totp authenticator

pull/454/head
Florens Pauwels 2 years ago
parent 4c52ad3a4c
commit d8436e7353

3
.gitignore vendored

@ -13,6 +13,7 @@ TestResults
*.user
*.sln.docstates
.vs
.idea
# Build results
[Dd]ebug/
@ -117,4 +118,4 @@ protobuf/
cryptopp/
# misc
Thumbs.db
Thumbs.db

@ -28,5 +28,6 @@ namespace DepotDownloader
public uint? LoginID { get; set; }
public bool UseQrCode { get; set; }
public string TotpKey { get; set; }
}
}

@ -46,6 +46,7 @@ namespace DepotDownloader
var username = GetParameter<string>(args, "-username") ?? GetParameter<string>(args, "-user");
var password = GetParameter<string>(args, "-password") ?? GetParameter<string>(args, "-pass");
ContentDownloader.Config.TotpKey = GetParameter<string>(args, "-totp-key");
ContentDownloader.Config.RememberPassword = HasParameter(args, "-remember-password");
ContentDownloader.Config.UseQrCode = HasParameter(args, "-qr");
@ -401,6 +402,7 @@ namespace DepotDownloader
Console.WriteLine("\t-max-servers <#>\t\t- maximum number of content servers to use. (default: 20).");
Console.WriteLine("\t-max-downloads <#>\t\t- maximum number of chunks to download concurrently. (default: 8).");
Console.WriteLine("\t-loginid <#>\t\t- a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.");
Console.WriteLine("\t-totp-key <key>\t\t- the TOTP authenticator key for the steam account, to automatically generate 2FA codes.");
}
}
}

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using QRCoder;
@ -508,12 +509,12 @@ namespace DepotDownloader
{
try
{
authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new SteamKit2.Authentication.AuthSessionDetails
authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new AuthSessionDetails
{
Username = logonDetails.Username,
Password = logonDetails.Password,
IsPersistentSession = ContentDownloader.Config.RememberPassword,
Authenticator = new UserConsoleAuthenticator(),
Authenticator = new TotpAuthenticator(ContentDownloader.Config.TotpKey, new UserConsoleAuthenticator())
});
}
catch (TaskCanceledException)

@ -0,0 +1,91 @@
using System;
using System.Security.Cryptography;
using System.Threading.Tasks;
using SteamKit2.Authentication;
namespace DepotDownloader;
/// <summary>
/// Implementation of <see cref="IAuthenticator"/> that uses a TOTP Authenticator key to generate TOTP verification codes.
/// Falls back to the provided fallback authenticator.
/// </summary>
public class TotpAuthenticator : IAuthenticator
{
private readonly string _totpKey;
private readonly IAuthenticator _fallbackAuthenticator;
public TotpAuthenticator(string totpKey, IAuthenticator fallbackAuthenticator)
{
_totpKey = totpKey;
_fallbackAuthenticator = fallbackAuthenticator;
}
/// <inheritdoc />
public Task<string> GetDeviceCodeAsync(bool previousCodeWasIncorrect)
{
if (previousCodeWasIncorrect)
{
return _fallbackAuthenticator.GetDeviceCodeAsync(true);
}
var deviceCode = GetDeviceCode(_totpKey);
return deviceCode != null ? Task.FromResult(deviceCode) : _fallbackAuthenticator.GetDeviceCodeAsync(false);
}
/// <inheritdoc />
public Task<string> GetEmailCodeAsync(string email, bool previousCodeWasIncorrect)
{
return _fallbackAuthenticator.GetEmailCodeAsync(email, previousCodeWasIncorrect);
}
/// <inheritdoc />
public Task<bool> AcceptDeviceConfirmationAsync()
{
return _fallbackAuthenticator.AcceptDeviceConfirmationAsync();
}
// https://github.com/bitwarden/mobile/blob/7a65bf7fd7b44424073201c2c574d45b64b9ec9d/src/Core/Services/TotpService.cs
private static string GetDeviceCode(string key)
{
const int Digits = 5;
const int TotpDefaultTimer = 30;
const string SteamChars = "23456789BCDFGHJKMNPQRTVWXY";
if (string.IsNullOrWhiteSpace(key))
{
return null;
}
var keyBytes = Util.DecodeBase32String(key);
if (keyBytes == null || keyBytes.Length == 0)
{
return null;
}
var time = DateTimeOffset.Now.ToUnixTimeSeconds() / TotpDefaultTimer;
var timeBytes = BitConverter.GetBytes(time);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(timeBytes, 0, timeBytes.Length);
}
var hash = new HMACSHA1(keyBytes).ComputeHash(timeBytes);
if (hash.Length == 0)
{
return null;
}
var offset = hash[^1] & 0xf;
var binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
var otp = string.Empty;
var fullCode = binary & 0x7fffffff;
for (var i = 0; i < Digits; i++)
{
otp += SteamChars[fullCode % SteamChars.Length];
fullCode = (int)Math.Truncate(fullCode / (double)SteamChars.Length);
}
return otp;
}
}

@ -144,6 +144,69 @@ namespace DepotDownloader
).ToString();
}
public static byte[] DecodeBase32String(string input)
{
const string Base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
input = input.ToUpperInvariant();
var cleanedInput = string.Empty;
foreach (var c in input)
{
if (Base32Chars.IndexOf(c) < 0)
{
continue;
}
cleanedInput += c;
}
input = cleanedInput;
if (input.Length == 0)
{
return Array.Empty<byte>();
}
var output = new byte[input.Length * 5 / 8];
var bitIndex = 0;
var inputIndex = 0;
var outputBits = 0;
var outputIndex = 0;
while (outputIndex < output.Length)
{
var byteIndex = Base32Chars.IndexOf(input[inputIndex]);
if (byteIndex < 0)
{
throw new FormatException();
}
var bits = Math.Min(5 - bitIndex, 8 - outputBits);
output[outputIndex] <<= bits;
output[outputIndex] |= (byte)(byteIndex >> (5 - (bitIndex + bits)));
bitIndex += bits;
if (bitIndex >= 5)
{
inputIndex++;
bitIndex = 0;
}
outputBits += bits;
if (outputBits >= 8)
{
outputIndex++;
outputBits = 0;
}
}
return output;
}
public static async Task InvokeAsync(IEnumerable<Func<Task>> taskFactories, int maxDegreeOfParallelism)
{
if (taskFactories == null) throw new ArgumentNullException(nameof(taskFactories));

@ -27,32 +27,33 @@ For example: `dotnet DepotDownloader.dll -app 730 -ugc 770604181014286929`
## Parameters
Parameter | Description
--------- | -----------
-app \<#> | the AppID to download.
-depot \<#> | the DepotID to download.
-manifest \<id> | manifest id of content to download (requires -depot, default: current for branch).
-ugc \<#> | the UGC ID to download.
-beta \<branchname> | download from specified branch if available (default: Public).
-betapassword \<pass> | branch password if applicable.
-all-platforms | downloads all platform-specific depots when -app is used.
-os \<os> | the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on)
-osarch \<arch> | the architecture for which to download the game (32 or 64, default: the host's architecture)
-all-languages | download all language-specific depots when -app is used.
-language \<lang> | the language for which to download the game (default: english)
-lowviolence | download low violence depots when -app is used.
-pubfile \<#> | the PublishedFileId to download. (Will automatically resolve to UGC id)
-username \<user> | the username of the account to login to for restricted content.
-password \<pass> | the password of the account to login to for restricted content.
-remember-password | if set, remember the password for subsequent logins of this user. (Use -username <username> -remember-password as login credentials)
-dir \<installdir> | the directory in which to place downloaded files.
-filelist \<file.txt> | a list of files to download (from the manifest). Prefix file path with `regex:` if you want to match with regex.
-validate | Include checksum verification of files already downloaded
-manifest-only | downloads a human readable manifest for any depots that would be downloaded.
-cellid \<#> | the overridden CellID of the content server to download from.
-max-servers \<#> | maximum number of content servers to use. (default: 20).
-max-downloads \<#> | maximum number of chunks to download concurrently. (default: 8).
-loginid \<#> | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently.
| Parameter | Description |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| -app \<#> | the AppID to download. |
| -depot \<#> | the DepotID to download. |
| -manifest \<id> | manifest id of content to download (requires -depot, default: current for branch). |
| -ugc \<#> | the UGC ID to download. |
| -beta \<branchname> | download from specified branch if available (default: Public). |
| -betapassword \<pass> | branch password if applicable. |
| -all-platforms | downloads all platform-specific depots when -app is used. |
| -os \<os> | the operating system for which to download the game (windows, macos or linux, default: OS the program is currently running on) |
| -osarch \<arch> | the architecture for which to download the game (32 or 64, default: the host's architecture) |
| -all-languages | download all language-specific depots when -app is used. |
| -language \<lang> | the language for which to download the game (default: english) |
| -lowviolence | download low violence depots when -app is used. |
| -pubfile \<#> | the PublishedFileId to download. (Will automatically resolve to UGC id) |
| -username \<user> | the username of the account to login to for restricted content. |
| -password \<pass> | the password of the account to login to for restricted content. |
| -remember-password | if set, remember the password for subsequent logins of this user. (Use -username <username> -remember-password as login credentials) |
| -dir \<installdir> | the directory in which to place downloaded files. |
| -filelist \<file.txt> | a list of files to download (from the manifest). Prefix file path with `regex:` if you want to match with regex. |
| -validate | Include checksum verification of files already downloaded |
| -manifest-only | downloads a human readable manifest for any depots that would be downloaded. |
| -cellid \<#> | the overridden CellID of the content server to download from. |
| -max-servers \<#> | maximum number of content servers to use. (default: 20). |
| -max-downloads \<#> | maximum number of chunks to download concurrently. (default: 8). |
| -loginid \<#> | a unique 32-bit integer Steam LogonID in decimal, required if running multiple instances of DepotDownloader concurrently. |
| -totp-key \<key> | the TOTP authenticator key for the steam account, to automatically generate 2FA codes. |
## Frequently Asked Questions

Loading…
Cancel
Save