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.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Localization;
using CryptSharp.Utility;
@ -57,7 +59,7 @@ public static class ArchiCryptoHelper {
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)) {
throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
}
@ -67,8 +69,10 @@ public static class ArchiCryptoHelper {
}
return cryptoMethod switch {
ECryptoMethod.PlainText => 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),
_ => throw new ArgumentOutOfRangeException(nameof(cryptoMethod))
};
@ -84,8 +88,10 @@ public static class ArchiCryptoHelper {
}
return cryptoMethod switch {
ECryptoMethod.PlainText => decryptedString,
ECryptoMethod.AES => EncryptAES(decryptedString),
ECryptoMethod.EnvironmentVariable => decryptedString,
ECryptoMethod.File => decryptedString,
ECryptoMethod.PlainText => decryptedString,
ECryptoMethod.ProtectedDataForCurrentUser => EncryptProtectedDataForCurrentUser(decryptedString),
_ => 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) {
if ((passwordHash == null) || (passwordHash.Length == 0)) {
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 {
PlainText,
AES,
ProtectedDataForCurrentUser
ProtectedDataForCurrentUser,
EnvironmentVariable,
File
}
public enum EHashingMethod : byte {

View file

@ -125,7 +125,7 @@ public sealed class BotController : ArchiController {
}
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) {

View file

@ -180,7 +180,7 @@ public sealed class Bot : IAsyncDisposable {
/// <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
/// </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)}")]
private string SSteamID => SteamID.ToString(CultureInfo.InvariantCulture);
@ -863,7 +863,7 @@ public sealed class Bot : IAsyncDisposable {
break;
case ASF.EUserInputType.Password:
BotConfig.DecryptedSteamPassword = inputValue;
BotConfig.SetDecryptedSteamPassword(inputValue, true);
BotConfig.IsSteamPasswordSet = false;
break;
@ -2033,7 +2033,10 @@ public sealed class Bot : IAsyncDisposable {
}
}
if (requiresPassword && string.IsNullOrEmpty(BotConfig.DecryptedSteamPassword)) {
if (requiresPassword) {
string? decryptedSteamPassword = await BotConfig.GetDecryptedSteamPassword().ConfigureAwait(false);
if (string.IsNullOrEmpty(decryptedSteamPassword)) {
RequiredInput = ASF.EUserInputType.Password;
string? steamPassword = await Logging.GetUserInput(ASF.EUserInputType.Password, BotName).ConfigureAwait(false);
@ -2045,6 +2048,7 @@ public sealed class Bot : IAsyncDisposable {
return false;
}
}
}
return true;
}
@ -2256,8 +2260,8 @@ public sealed class Bot : IAsyncDisposable {
// Decrypt login key if needed
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
if (!string.IsNullOrEmpty(loginKey) && (loginKey!.Length > 19) && (BotConfig.PasswordFormat != ArchiCryptoHelper.ECryptoMethod.PlainText)) {
loginKey = ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, loginKey);
if (!string.IsNullOrEmpty(loginKey) && (loginKey!.Length > 19) && BotConfig.PasswordFormat.HasTransformation()) {
loginKey = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, loginKey).ConfigureAwait(false);
}
} else {
// If we're not using login keys, ensure we don't have any saved
@ -2287,7 +2291,7 @@ public sealed class Bot : IAsyncDisposable {
return;
}
string? password = BotConfig.DecryptedSteamPassword;
string? password = await BotConfig.GetDecryptedSteamPassword().ConfigureAwait(false);
if (!string.IsNullOrEmpty(password)) {
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework

View file

@ -273,37 +273,6 @@ public sealed class BotConfig {
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 IsSteamParentalCodeSet { 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);
}
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) {
if (string.IsNullOrEmpty(filePath)) {
throw new ArgumentNullException(nameof(filePath));
@ -575,7 +564,9 @@ public sealed class BotConfig {
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" };
if (!string.IsNullOrEmpty(botConfig.SteamLogin)) {
@ -584,7 +575,8 @@ public sealed class BotConfig {
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) {
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);
}
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 {
None,
FamilySharing,