Automatic translations update
[ArchiSteamFarm.git] / ArchiSteamFarm / Helpers / ArchiCryptoHelper.cs
blob79753b9c50eaa3f417922013522d748b9dd04fab
1 // ----------------------------------------------------------------------------------------------
2 // _ _ _ ____ _ _____
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
8 // |
9 // Copyright 2015-2024 Ɓukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
11 // |
12 // Licensed under the Apache License, Version 2.0 (the "License");
13 // you may not use this file except in compliance with the License.
14 // You may obtain a copy of the License at
15 // |
16 // http://www.apache.org/licenses/LICENSE-2.0
17 // |
18 // Unless required by applicable law or agreed to in writing, software
19 // distributed under the License is distributed on an "AS IS" BASIS,
20 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 // See the License for the specific language governing permissions and
22 // limitations under the License.
24 using System;
25 using System.Collections.Generic;
26 using System.ComponentModel;
27 using System.Globalization;
28 using System.IO;
29 using System.Linq;
30 using System.Security.Cryptography;
31 using System.Text;
32 using System.Threading.Tasks;
33 using ArchiSteamFarm.Core;
34 using ArchiSteamFarm.Localization;
35 using CryptSharp.Utility;
36 using SteamKit2;
38 namespace ArchiSteamFarm.Helpers;
40 public static class ArchiCryptoHelper {
41 private const byte DefaultHashLength = 32;
42 private const byte MinimumRecommendedCryptKeyBytes = 32;
43 private const ushort SteamParentalPbkdf2Iterations = 10000;
44 private const byte SteamParentalSCryptBlocksCount = 8;
45 private const ushort SteamParentalSCryptIterations = 8192;
47 internal static bool HasDefaultCryptKey { get; private set; } = true;
49 private static IEnumerable<byte> SteamParentalCharacters => Enumerable.Range('0', 10).Select(static character => (byte) character);
51 private static IEnumerable<byte[]> SteamParentalCodes {
52 get {
53 HashSet<byte> steamParentalCharacters = SteamParentalCharacters.ToHashSet();
55 return from a in steamParentalCharacters from b in steamParentalCharacters from c in steamParentalCharacters from d in steamParentalCharacters select new[] { a, b, c, d };
59 private static byte[] EncryptionKey = Encoding.UTF8.GetBytes(nameof(ArchiSteamFarm));
61 internal static async Task<string?> Decrypt(ECryptoMethod cryptoMethod, string text) {
62 if (!Enum.IsDefined(cryptoMethod)) {
63 throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
66 ArgumentException.ThrowIfNullOrEmpty(text);
68 return cryptoMethod switch {
69 ECryptoMethod.AES => DecryptAES(text),
70 ECryptoMethod.EnvironmentVariable => Environment.GetEnvironmentVariable(text)?.Trim(),
71 ECryptoMethod.File => await ReadFromFile(text).ConfigureAwait(false),
72 ECryptoMethod.PlainText => text,
73 ECryptoMethod.ProtectedDataForCurrentUser => DecryptProtectedDataForCurrentUser(text),
74 _ => throw new InvalidOperationException(nameof(cryptoMethod))
78 internal static string? Encrypt(ECryptoMethod cryptoMethod, string text) {
79 if (!Enum.IsDefined(cryptoMethod)) {
80 throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
83 ArgumentException.ThrowIfNullOrEmpty(text);
85 return cryptoMethod switch {
86 ECryptoMethod.AES => EncryptAES(text),
87 ECryptoMethod.EnvironmentVariable => text,
88 ECryptoMethod.File => text,
89 ECryptoMethod.PlainText => text,
90 ECryptoMethod.ProtectedDataForCurrentUser => EncryptProtectedDataForCurrentUser(text),
91 _ => throw new InvalidOperationException(nameof(cryptoMethod))
95 internal static string Hash(EHashingMethod hashingMethod, string text) {
96 if (!Enum.IsDefined(hashingMethod)) {
97 throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
100 ArgumentException.ThrowIfNullOrEmpty(text);
102 if (hashingMethod == EHashingMethod.PlainText) {
103 return text;
106 byte[] textBytes = Encoding.UTF8.GetBytes(text);
107 byte[] hashBytes = Hash(textBytes, EncryptionKey, DefaultHashLength, hashingMethod);
109 return Convert.ToBase64String(hashBytes);
112 internal static byte[] Hash(byte[] password, byte[] salt, byte hashLength, EHashingMethod hashingMethod) {
113 if ((password == null) || (password.Length == 0)) {
114 throw new ArgumentNullException(nameof(password));
117 if ((salt == null) || (salt.Length == 0)) {
118 throw new ArgumentNullException(nameof(salt));
121 ArgumentOutOfRangeException.ThrowIfZero(hashLength);
123 if (!Enum.IsDefined(hashingMethod)) {
124 throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
127 return hashingMethod switch {
128 EHashingMethod.PlainText => password,
129 EHashingMethod.SCrypt => SCrypt.ComputeDerivedKey(password, salt, SteamParentalSCryptIterations, SteamParentalSCryptBlocksCount, 1, null, hashLength),
130 EHashingMethod.Pbkdf2 => Rfc2898DeriveBytes.Pbkdf2(password, salt, SteamParentalPbkdf2Iterations, HashAlgorithmName.SHA256, hashLength),
131 _ => throw new InvalidOperationException(nameof(hashingMethod))
135 internal static bool HasTransformation(this ECryptoMethod cryptoMethod) =>
136 cryptoMethod switch {
137 ECryptoMethod.AES => true,
138 ECryptoMethod.ProtectedDataForCurrentUser => true,
139 _ => false
142 internal static string? RecoverSteamParentalCode(byte[] passwordHash, byte[] salt, EHashingMethod hashingMethod) {
143 if ((passwordHash == null) || (passwordHash.Length == 0)) {
144 throw new ArgumentNullException(nameof(passwordHash));
147 if (passwordHash.Length > byte.MaxValue) {
148 throw new ArgumentOutOfRangeException(nameof(passwordHash));
151 if ((salt == null) || (salt.Length == 0)) {
152 throw new ArgumentNullException(nameof(salt));
155 if (!Enum.IsDefined(hashingMethod)) {
156 throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
159 byte[]? password = SteamParentalCodes.AsParallel().FirstOrDefault(passwordToTry => Hash(passwordToTry, salt, (byte) passwordHash.Length, hashingMethod).SequenceEqual(passwordHash));
161 return password != null ? Encoding.UTF8.GetString(password) : null;
164 internal static void SetEncryptionKey(string key) {
165 ArgumentException.ThrowIfNullOrEmpty(key);
167 if (!HasDefaultCryptKey) {
168 ASF.ArchiLogger.LogGenericError(Strings.ErrorAborted);
170 return;
173 byte[] encryptionKey = Encoding.UTF8.GetBytes(key);
175 if (encryptionKey.Length < MinimumRecommendedCryptKeyBytes) {
176 ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningTooShortCryptKey, MinimumRecommendedCryptKeyBytes));
179 HasDefaultCryptKey = encryptionKey.SequenceEqual(EncryptionKey);
180 EncryptionKey = encryptionKey;
183 internal static bool VerifyHash(EHashingMethod hashingMethod, string text, string hash) {
184 if (!Enum.IsDefined(hashingMethod)) {
185 throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
188 ArgumentException.ThrowIfNullOrEmpty(text);
189 ArgumentException.ThrowIfNullOrEmpty(hash);
191 // Text is always provided as plain text
192 byte[] textBytes = Encoding.UTF8.GetBytes(text);
193 textBytes = Hash(textBytes, EncryptionKey, DefaultHashLength, hashingMethod);
195 // Hash is either plain text password (when EHashingMethod.PlainText), or base64-encoded hash
196 byte[] hashBytes = hashingMethod == EHashingMethod.PlainText ? Encoding.UTF8.GetBytes(hash) : Convert.FromBase64String(hash);
198 return CryptographicOperations.FixedTimeEquals(textBytes, hashBytes);
201 private static string? DecryptAES(string text) {
202 ArgumentException.ThrowIfNullOrEmpty(text);
204 try {
205 byte[] key = SHA256.HashData(EncryptionKey);
207 byte[] decryptedData = Convert.FromBase64String(text);
208 decryptedData = CryptoHelper.SymmetricDecrypt(decryptedData, key);
210 return Encoding.UTF8.GetString(decryptedData);
211 } catch (Exception e) {
212 ASF.ArchiLogger.LogGenericException(e);
214 return null;
218 private static string? DecryptProtectedDataForCurrentUser(string text) {
219 ArgumentException.ThrowIfNullOrEmpty(text);
221 if (!OperatingSystem.IsWindows()) {
222 return null;
225 try {
226 byte[] decryptedData = ProtectedData.Unprotect(
227 Convert.FromBase64String(text),
228 EncryptionKey,
229 DataProtectionScope.CurrentUser
232 return Encoding.UTF8.GetString(decryptedData);
233 } catch (Exception e) {
234 ASF.ArchiLogger.LogGenericException(e);
236 return null;
240 private static string? EncryptAES(string text) {
241 ArgumentException.ThrowIfNullOrEmpty(text);
243 try {
244 byte[] key = SHA256.HashData(EncryptionKey);
246 byte[] encryptedData = Encoding.UTF8.GetBytes(text);
247 encryptedData = CryptoHelper.SymmetricEncrypt(encryptedData, key);
249 return Convert.ToBase64String(encryptedData);
250 } catch (Exception e) {
251 ASF.ArchiLogger.LogGenericException(e);
253 return null;
257 private static string? EncryptProtectedDataForCurrentUser(string text) {
258 ArgumentException.ThrowIfNullOrEmpty(text);
260 if (!OperatingSystem.IsWindows()) {
261 return null;
264 try {
265 byte[] encryptedData = ProtectedData.Protect(
266 Encoding.UTF8.GetBytes(text),
267 EncryptionKey,
268 DataProtectionScope.CurrentUser
271 return Convert.ToBase64String(encryptedData);
272 } catch (Exception e) {
273 ASF.ArchiLogger.LogGenericException(e);
275 return null;
279 private static async Task<string?> ReadFromFile(string filePath) {
280 ArgumentException.ThrowIfNullOrEmpty(filePath);
282 if (!File.Exists(filePath)) {
283 return null;
286 string text;
288 try {
289 text = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
290 } catch (Exception e) {
291 ASF.ArchiLogger.LogGenericException(e);
293 return null;
296 return text.Trim();
299 public enum ECryptoMethod : byte {
300 PlainText,
301 AES,
302 ProtectedDataForCurrentUser,
303 EnvironmentVariable,
304 File
307 public enum EHashingMethod : byte {
308 PlainText,
309 SCrypt,
310 Pbkdf2