Implement 2 additional crypto methods for Steam password

Inspiration by @legendofmiracles
This commit is contained in:
Archi 2021-11-23 21:50:33 +01:00
parent b030755eb6
commit e68210cf2e
No known key found for this signature in database
GPG key ID: 6B138B4C64555AEA
4 changed files with 97 additions and 50 deletions

View file

@ -24,9 +24,11 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks;
using ArchiSteamFarm.Core; using ArchiSteamFarm.Core;
using ArchiSteamFarm.Localization; using ArchiSteamFarm.Localization;
using CryptSharp.Utility; using CryptSharp.Utility;
@ -57,7 +59,7 @@ public static class ArchiCryptoHelper {
private static byte[] EncryptionKey = Encoding.UTF8.GetBytes(nameof(ArchiSteamFarm)); private static byte[] EncryptionKey = Encoding.UTF8.GetBytes(nameof(ArchiSteamFarm));
internal static string? Decrypt(ECryptoMethod cryptoMethod, string encryptedString) { internal static async Task<string?> Decrypt(ECryptoMethod cryptoMethod, string encryptedString) {
if (!Enum.IsDefined(typeof(ECryptoMethod), cryptoMethod)) { if (!Enum.IsDefined(typeof(ECryptoMethod), cryptoMethod)) {
throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod)); throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
} }
@ -67,8 +69,10 @@ public static class ArchiCryptoHelper {
} }
return cryptoMethod switch { return cryptoMethod switch {
ECryptoMethod.PlainText => encryptedString,
ECryptoMethod.AES => DecryptAES(encryptedString), ECryptoMethod.AES => DecryptAES(encryptedString),
ECryptoMethod.EnvironmentVariable => Environment.GetEnvironmentVariable(encryptedString)?.Trim(),
ECryptoMethod.File => await ReadFromFile(encryptedString).ConfigureAwait(false),
ECryptoMethod.PlainText => encryptedString,
ECryptoMethod.ProtectedDataForCurrentUser => DecryptProtectedDataForCurrentUser(encryptedString), ECryptoMethod.ProtectedDataForCurrentUser => DecryptProtectedDataForCurrentUser(encryptedString),
_ => throw new ArgumentOutOfRangeException(nameof(cryptoMethod)) _ => throw new ArgumentOutOfRangeException(nameof(cryptoMethod))
}; };
@ -84,8 +88,10 @@ public static class ArchiCryptoHelper {
} }
return cryptoMethod switch { return cryptoMethod switch {
ECryptoMethod.PlainText => decryptedString,
ECryptoMethod.AES => EncryptAES(decryptedString), ECryptoMethod.AES => EncryptAES(decryptedString),
ECryptoMethod.EnvironmentVariable => decryptedString,
ECryptoMethod.File => decryptedString,
ECryptoMethod.PlainText => decryptedString,
ECryptoMethod.ProtectedDataForCurrentUser => EncryptProtectedDataForCurrentUser(decryptedString), ECryptoMethod.ProtectedDataForCurrentUser => EncryptProtectedDataForCurrentUser(decryptedString),
_ => throw new ArgumentOutOfRangeException(nameof(cryptoMethod)) _ => throw new ArgumentOutOfRangeException(nameof(cryptoMethod))
}; };
@ -141,6 +147,13 @@ public static class ArchiCryptoHelper {
} }
} }
internal static bool HasTransformation(this ECryptoMethod cryptoMethod) =>
cryptoMethod switch {
ECryptoMethod.AES => true,
ECryptoMethod.ProtectedDataForCurrentUser => true,
_ => false
};
internal static string? RecoverSteamParentalCode(byte[] passwordHash, byte[] salt, EHashingMethod hashingMethod) { internal static string? RecoverSteamParentalCode(byte[] passwordHash, byte[] salt, EHashingMethod hashingMethod) {
if ((passwordHash == null) || (passwordHash.Length == 0)) { if ((passwordHash == null) || (passwordHash.Length == 0)) {
throw new ArgumentNullException(nameof(passwordHash)); throw new ArgumentNullException(nameof(passwordHash));
@ -276,10 +289,34 @@ public static class ArchiCryptoHelper {
} }
} }
private static async Task<string?> ReadFromFile(string filePath) {
if (string.IsNullOrEmpty(filePath)) {
throw new ArgumentNullException(nameof(filePath));
}
if (!File.Exists(filePath)) {
return null;
}
string text;
try {
text = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return null;
}
return text.Trim();
}
public enum ECryptoMethod : byte { public enum ECryptoMethod : byte {
PlainText, PlainText,
AES, AES,
ProtectedDataForCurrentUser ProtectedDataForCurrentUser,
EnvironmentVariable,
File
} }
public enum EHashingMethod : byte { public enum EHashingMethod : byte {

View file

@ -125,7 +125,7 @@ public sealed class BotController : ArchiController {
} }
if (!request.BotConfig.IsSteamPasswordSet && bot.BotConfig.IsSteamPasswordSet) { if (!request.BotConfig.IsSteamPasswordSet && bot.BotConfig.IsSteamPasswordSet) {
request.BotConfig.DecryptedSteamPassword = bot.BotConfig.DecryptedSteamPassword; request.BotConfig.SetDecryptedSteamPassword(await bot.BotConfig.GetDecryptedSteamPassword().ConfigureAwait(false));
} }
if (!request.BotConfig.IsSteamParentalCodeSet && bot.BotConfig.IsSteamParentalCodeSet) { if (!request.BotConfig.IsSteamParentalCodeSet && bot.BotConfig.IsSteamParentalCodeSet) {

View file

@ -180,7 +180,7 @@ public sealed class Bot : IAsyncDisposable {
/// <remarks> /// <remarks>
/// Login keys are not guaranteed to be valid, we should use them only if we don't have full details available from the user /// Login keys are not guaranteed to be valid, we should use them only if we don't have full details available from the user
/// </remarks> /// </remarks>
private bool ShouldUseLoginKeys => BotConfig.UseLoginKeys && (!BotConfig.IsSteamPasswordSet || string.IsNullOrEmpty(BotConfig.DecryptedSteamPassword) || !HasMobileAuthenticator); private bool ShouldUseLoginKeys => BotConfig.UseLoginKeys && (!BotConfig.IsSteamPasswordSet || !HasMobileAuthenticator);
[JsonProperty(PropertyName = $"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamID)}")] [JsonProperty(PropertyName = $"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamID)}")]
private string SSteamID => SteamID.ToString(CultureInfo.InvariantCulture); private string SSteamID => SteamID.ToString(CultureInfo.InvariantCulture);
@ -863,7 +863,7 @@ public sealed class Bot : IAsyncDisposable {
break; break;
case ASF.EUserInputType.Password: case ASF.EUserInputType.Password:
BotConfig.DecryptedSteamPassword = inputValue; BotConfig.SetDecryptedSteamPassword(inputValue, true);
BotConfig.IsSteamPasswordSet = false; BotConfig.IsSteamPasswordSet = false;
break; break;
@ -2033,16 +2033,20 @@ public sealed class Bot : IAsyncDisposable {
} }
} }
if (requiresPassword && string.IsNullOrEmpty(BotConfig.DecryptedSteamPassword)) { if (requiresPassword) {
RequiredInput = ASF.EUserInputType.Password; string? decryptedSteamPassword = await BotConfig.GetDecryptedSteamPassword().ConfigureAwait(false);
string? steamPassword = await Logging.GetUserInput(ASF.EUserInputType.Password, BotName).ConfigureAwait(false); if (string.IsNullOrEmpty(decryptedSteamPassword)) {
RequiredInput = ASF.EUserInputType.Password;
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework string? steamPassword = await Logging.GetUserInput(ASF.EUserInputType.Password, BotName).ConfigureAwait(false);
if (string.IsNullOrEmpty(steamPassword) || !SetUserInput(ASF.EUserInputType.Password, steamPassword!)) {
ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamPassword)));
return false; // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
if (string.IsNullOrEmpty(steamPassword) || !SetUserInput(ASF.EUserInputType.Password, steamPassword!)) {
ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamPassword)));
return false;
}
} }
} }
@ -2256,8 +2260,8 @@ public sealed class Bot : IAsyncDisposable {
// Decrypt login key if needed // Decrypt login key if needed
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
if (!string.IsNullOrEmpty(loginKey) && (loginKey!.Length > 19) && (BotConfig.PasswordFormat != ArchiCryptoHelper.ECryptoMethod.PlainText)) { if (!string.IsNullOrEmpty(loginKey) && (loginKey!.Length > 19) && BotConfig.PasswordFormat.HasTransformation()) {
loginKey = ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, loginKey); loginKey = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, loginKey).ConfigureAwait(false);
} }
} else { } else {
// If we're not using login keys, ensure we don't have any saved // If we're not using login keys, ensure we don't have any saved
@ -2287,7 +2291,7 @@ public sealed class Bot : IAsyncDisposable {
return; return;
} }
string? password = BotConfig.DecryptedSteamPassword; string? password = await BotConfig.GetDecryptedSteamPassword().ConfigureAwait(false);
if (!string.IsNullOrEmpty(password)) { if (!string.IsNullOrEmpty(password)) {
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework

View file

@ -273,37 +273,6 @@ public sealed class BotConfig {
set; set;
} }
internal string? DecryptedSteamPassword {
get {
if (string.IsNullOrEmpty(SteamPassword)) {
return null;
}
if (PasswordFormat == ArchiCryptoHelper.ECryptoMethod.PlainText) {
return SteamPassword;
}
string? result = ArchiCryptoHelper.Decrypt(PasswordFormat, SteamPassword!);
if (string.IsNullOrEmpty(result)) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(SteamPassword)));
return null;
}
return result;
}
set {
if (!string.IsNullOrEmpty(value) && (PasswordFormat != ArchiCryptoHelper.ECryptoMethod.PlainText)) {
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
value = ArchiCryptoHelper.Encrypt(PasswordFormat, value!);
}
SteamPassword = value;
}
}
internal bool IsSteamLoginSet { get; set; } internal bool IsSteamLoginSet { get; set; }
internal bool IsSteamParentalCodeSet { get; set; } internal bool IsSteamParentalCodeSet { get; set; }
internal bool IsSteamPasswordSet { get; set; } internal bool IsSteamPasswordSet { get; set; }
@ -530,6 +499,26 @@ public sealed class BotConfig {
return !Enum.IsDefined(typeof(ArchiHandler.EUserInterfaceMode), UserInterfaceMode) ? (false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorConfigPropertyInvalid, nameof(UserInterfaceMode), UserInterfaceMode)) : (true, null); return !Enum.IsDefined(typeof(ArchiHandler.EUserInterfaceMode), UserInterfaceMode) ? (false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorConfigPropertyInvalid, nameof(UserInterfaceMode), UserInterfaceMode)) : (true, null);
} }
internal async Task<string?> GetDecryptedSteamPassword() {
if (string.IsNullOrEmpty(SteamPassword)) {
return null;
}
if (PasswordFormat == ArchiCryptoHelper.ECryptoMethod.PlainText) {
return SteamPassword;
}
string? result = await ArchiCryptoHelper.Decrypt(PasswordFormat, SteamPassword!).ConfigureAwait(false);
if (string.IsNullOrEmpty(result)) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(SteamPassword)));
return null;
}
return result;
}
internal static async Task<(BotConfig? BotConfig, string? LatestJson)> Load(string filePath) { internal static async Task<(BotConfig? BotConfig, string? LatestJson)> Load(string filePath) {
if (string.IsNullOrEmpty(filePath)) { if (string.IsNullOrEmpty(filePath)) {
throw new ArgumentNullException(nameof(filePath)); throw new ArgumentNullException(nameof(filePath));
@ -575,7 +564,9 @@ public sealed class BotConfig {
return (null, null); return (null, null);
} }
if (!string.IsNullOrEmpty(botConfig.DecryptedSteamPassword)) { string? decryptedSteamPassword = await botConfig.GetDecryptedSteamPassword().ConfigureAwait(false);
if (!string.IsNullOrEmpty(decryptedSteamPassword)) {
HashSet<string> disallowedValues = new(StringComparer.InvariantCultureIgnoreCase) { "account" }; HashSet<string> disallowedValues = new(StringComparer.InvariantCultureIgnoreCase) { "account" };
if (!string.IsNullOrEmpty(botConfig.SteamLogin)) { if (!string.IsNullOrEmpty(botConfig.SteamLogin)) {
@ -584,7 +575,8 @@ public sealed class BotConfig {
Utilities.InBackground( Utilities.InBackground(
() => { () => {
(bool isWeak, string? reason) = Utilities.TestPasswordStrength(botConfig.DecryptedSteamPassword!, disallowedValues); // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
(bool isWeak, string? reason) = Utilities.TestPasswordStrength(decryptedSteamPassword!, disallowedValues);
if (isWeak) { if (isWeak) {
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningWeakSteamPassword, !string.IsNullOrEmpty(botConfig.SteamLogin) ? botConfig.SteamLogin! : filePath, reason)); ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningWeakSteamPassword, !string.IsNullOrEmpty(botConfig.SteamLogin) ? botConfig.SteamLogin! : filePath, reason));
@ -615,6 +607,20 @@ public sealed class BotConfig {
return (botConfig, json != latestJson ? latestJson : null); return (botConfig, json != latestJson ? latestJson : null);
} }
internal void SetDecryptedSteamPassword(string? decryptedSteamPassword, bool fromUser = false) {
if (!string.IsNullOrEmpty(decryptedSteamPassword) && PasswordFormat.HasTransformation()) {
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
decryptedSteamPassword = ArchiCryptoHelper.Encrypt(PasswordFormat, decryptedSteamPassword!);
}
SteamPassword = decryptedSteamPassword;
if (fromUser) {
// Reset steam password set flag, it actually isn't set in the config
IsSteamPasswordSet = false;
}
}
public enum EAccess : byte { public enum EAccess : byte {
None, None,
FamilySharing, FamilySharing,