<commit_before>//     _                _      _  ____   _                           _____
//    / \    _ __  ___ | |__  (_)/ ___| | |_  ___   __ _  _ __ ___  |  ___|__ _  _ __  _ __ ___
//   / _ \  | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_  / _` || '__|| '_ ` _ \
//  / ___ \ | |  | (__ | | | || | ___) || |_|  __/| (_| || | | | | ||  _|| (_| || |   | | | | | |
// /_/   \_\|_|   \___||_| |_||_||____/  \__|\___| \__,_||_| |_| |_||_|   \__,_||_|   |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Contact: <EMAIL>
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#if NETFRAMEWORK
using OperatingSystem = JustArchiNET.Madness.OperatingSystemMadness.OperatingSystem;
#endif
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Localization;
using CryptSharp.Utility;
using SteamKit2;

namespace ArchiSteamFarm.Helpers {
	public static class ArchiCryptoHelper {
		private const byte DefaultHashLength = 32;
		private const byte MinimumRecommendedCryptKeyBytes = 32;
		private const ushort SteamParentalPbkdf2Iterations = 10000;
		private const byte SteamParentalSCryptBlocksCount = 8;
		private const ushort SteamParentalSCryptIterations = 8192;

		internal static bool HasDefaultCryptKey { get; private set; } = true;

		private static readonly ImmutableHashSet<string> ForbiddenCryptKeyPhrases = ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, "crypt", "key", "cryptkey");

		private static IEnumerable<byte> SteamParentalCharacters => Enumerable.Range('0', 10).Select(static character => (byte) character);

		private static IEnumerable<byte[]> SteamParentalCodes {
			get {
				HashSet<byte> steamParentalCharacters = SteamParentalCharacters.ToHashSet();

				return from a in steamParentalCharacters from b in steamParentalCharacters from c in steamParentalCharacters from d in steamParentalCharacters select new[] { a, b, c, d };
			}
		}

		private static byte[] EncryptionKey = Encoding.UTF8.GetBytes(nameof(ArchiSteamFarm));

		internal static string? Decrypt(ECryptoMethod cryptoMethod, string encryptedString) {
			if (!Enum.IsDefined(typeof(ECryptoMethod), cryptoMethod)) {
				throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
			}

			if (string.IsNullOrEmpty(encryptedString)) {
				throw new ArgumentNullException(nameof(encryptedString));
			}

			return cryptoMethod switch {
				ECryptoMethod.PlainText => encryptedString,
				ECryptoMethod.AES => DecryptAES(encryptedString),
				ECryptoMethod.ProtectedDataForCurrentUser => DecryptProtectedDataForCurrentUser(encryptedString),
				_ => throw new ArgumentOutOfRangeException(nameof(cryptoMethod))
			};
		}

		internal static string? Encrypt(ECryptoMethod cryptoMethod, string decryptedString) {
			if (!Enum.IsDefined(typeof(ECryptoMethod), cryptoMethod)) {
				throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
			}

			if (string.IsNullOrEmpty(decryptedString)) {
				throw new ArgumentNullException(nameof(decryptedString));
			}

			return cryptoMethod switch {
				ECryptoMethod.PlainText => decryptedString,
				ECryptoMethod.AES => EncryptAES(decryptedString),
				ECryptoMethod.ProtectedDataForCurrentUser => EncryptProtectedDataForCurrentUser(decryptedString),
				_ => throw new ArgumentOutOfRangeException(nameof(cryptoMethod))
			};
		}

		internal static string Hash(EHashingMethod hashingMethod, string stringToHash) {
			if (!Enum.IsDefined(typeof(EHashingMethod), hashingMethod)) {
				throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
			}

			if (string.IsNullOrEmpty(stringToHash)) {
				throw new ArgumentNullException(nameof(stringToHash));
			}

			if (hashingMethod == EHashingMethod.PlainText) {
				return stringToHash;
			}

			byte[] passwordBytes = Encoding.UTF8.GetBytes(stringToHash);
			byte[] hashBytes = Hash(passwordBytes, EncryptionKey, DefaultHashLength, hashingMethod);

			return Convert.ToBase64String(hashBytes);
		}

		internal static byte[] Hash(byte[] password, byte[] salt, byte hashLength, EHashingMethod hashingMethod) {
			if ((password == null) || (password.Length == 0)) {
				throw new ArgumentNullException(nameof(password));
			}

			if ((salt == null) || (salt.Length == 0)) {
				throw new ArgumentNullException(nameof(salt));
			}

			if (hashLength == 0) {
				throw new ArgumentOutOfRangeException(nameof(hashLength));
			}

			if (!Enum.IsDefined(typeof(EHashingMethod), hashingMethod)) {
				throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
			}

			switch (hashingMethod) {
				case EHashingMethod.PlainText:
					return password;
				case EHashingMethod.SCrypt:
					return SCrypt.ComputeDerivedKey(password, salt, SteamParentalSCryptIterations, SteamParentalSCryptBlocksCount, 1, null, hashLength);
				case EHashingMethod.Pbkdf2:
					using (HMACSHA256 hmacAlgorithm = new(password)) {
						return Pbkdf2.ComputeDerivedKey(hmacAlgorithm, salt, SteamParentalPbkdf2Iterations, hashLength);
					}
				default:
					throw new ArgumentOutOfRangeException(nameof(hashingMethod));
			}
		}

		internal static string? RecoverSteamParentalCode(byte[] passwordHash, byte[] salt, EHashingMethod hashingMethod) {
			if ((passwordHash == null) || (passwordHash.Length == 0)) {
				throw new ArgumentNullException(nameof(passwordHash));
			}

			if ((salt == null) || (salt.Length == 0)) {
				throw new ArgumentNullException(nameof(salt));
			}

			if (!Enum.IsDefined(typeof(EHashingMethod), hashingMethod)) {
				throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
			}

			byte[]? password = SteamParentalCodes.AsParallel().FirstOrDefault(passwordToTry => Hash(passwordToTry, salt, (byte) passwordHash.Length, hashingMethod).SequenceEqual(passwordHash));

			return password != null ? Encoding.UTF8.GetString(password) : null;
		}

		internal static void SetEncryptionKey(string key) {
			if (string.IsNullOrEmpty(key)) {
				throw new ArgumentNullException(nameof(key));
			}

			Utilities.InBackground(
				() => {
					(bool isWeak, string? reason) = Utilities.TestPasswordStrength(key, ForbiddenCryptKeyPhrases);

					if (isWeak) {
						ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningWeakCryptKey, reason));
					}
				}
			);

			EncryptionKey = Encoding.UTF8.GetBytes(key);
			HasDefaultCryptKey = false;

			if (EncryptionKey.Length < MinimumRecommendedCryptKeyBytes) {
				ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningTooShortCryptKey, MinimumRecommendedCryptKeyBytes));
			}
		}

		private static string? DecryptAES(string encryptedString) {
			if (string.IsNullOrEmpty(encryptedString)) {
				throw new ArgumentNullException(nameof(encryptedString));
			}

			try {
				byte[] key;

				using (SHA256 sha256 = SHA256.Create()) {
					key = sha256.ComputeHash(EncryptionKey);
				}

				byte[] decryptedData = Convert.FromBase64String(encryptedString);
				decryptedData = CryptoHelper.SymmetricDecrypt(decryptedData, key);

				return Encoding.UTF8.GetString(decryptedData);
			} catch (Exception e) {
				ASF.ArchiLogger.LogGenericException(e);

				return null;
			}<commit_msg>Actually be smarter<commit_after>//     _                _      _  ____   _                           _____
//    / \    _ __  ___ | |__  (_)/ ___| | |_  ___   __ _  _ __ ___  |  ___|__ _  _ __  _ __ ___
//   / _ \  | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_  / _` || '__|| '_ ` _ \
//  / ___ \ | |  | (__ | | | || | ___) || |_|  __/| (_| || | | | | ||  _|| (_| || |   | | | | | |
// /_/   \_\|_|   \___||_| |_||_||____/  \__|\___| \__,_||_| |_| |_||_|   \__,_||_|   |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Contact: <EMAIL>
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#if NETFRAMEWORK
using OperatingSystem = JustArchiNET.Madness.OperatingSystemMadness.OperatingSystem;
#endif
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Localization;
using CryptSharp.Utility;
using SteamKit2;

namespace ArchiSteamFarm.Helpers {
	public static class ArchiCryptoHelper {
		private const byte DefaultHashLength = 32;
		private const byte MinimumRecommendedCryptKeyBytes = 32;
		private const ushort SteamParentalPbkdf2Iterations = 10000;
		private const byte SteamParentalSCryptBlocksCount = 8;
		private const ushort SteamParentalSCryptIterations = 8192;

		internal static bool HasDefaultCryptKey { get; private set; } = true;

		private static readonly ImmutableHashSet<string> ForbiddenCryptKeyPhrases = ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, "crypt", "key", "cryptkey");

		private static IEnumerable<byte> SteamParentalCharacters => Enumerable.Range('0', 10).Select(static character => (byte) character);

		private static IEnumerable<byte[]> SteamParentalCodes {
			get {
				HashSet<byte> steamParentalCharacters = SteamParentalCharacters.ToHashSet();

				return from a in steamParentalCharacters from b in steamParentalCharacters from c in steamParentalCharacters from d in steamParentalCharacters select new[] { a, b, c, d };
			}
		}

		private static byte[] EncryptionKey = Encoding.UTF8.GetBytes(nameof(ArchiSteamFarm));

		internal static string? Decrypt(ECryptoMethod cryptoMethod, string encryptedString) {
			if (!Enum.IsDefined(typeof(ECryptoMethod), cryptoMethod)) {
				throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
			}

			if (string.IsNullOrEmpty(encryptedString)) {
				throw new ArgumentNullException(nameof(encryptedString));
			}

			return cryptoMethod switch {
				ECryptoMethod.PlainText => encryptedString,
				ECryptoMethod.AES => DecryptAES(encryptedString),
				ECryptoMethod.ProtectedDataForCurrentUser => DecryptProtectedDataForCurrentUser(encryptedString),
				_ => throw new ArgumentOutOfRangeException(nameof(cryptoMethod))
			};
		}

		internal static string? Encrypt(ECryptoMethod cryptoMethod, string decryptedString) {
			if (!Enum.IsDefined(typeof(ECryptoMethod), cryptoMethod)) {
				throw new InvalidEnumArgumentException(nameof(cryptoMethod), (int) cryptoMethod, typeof(ECryptoMethod));
			}

			if (string.IsNullOrEmpty(decryptedString)) {
				throw new ArgumentNullException(nameof(decryptedString));
			}

			return cryptoMethod switch {
				ECryptoMethod.PlainText => decryptedString,
				ECryptoMethod.AES => EncryptAES(decryptedString),
				ECryptoMethod.ProtectedDataForCurrentUser => EncryptProtectedDataForCurrentUser(decryptedString),
				_ => throw new ArgumentOutOfRangeException(nameof(cryptoMethod))
			};
		}

		internal static string Hash(EHashingMethod hashingMethod, string stringToHash) {
			if (!Enum.IsDefined(typeof(EHashingMethod), hashingMethod)) {
				throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
			}

			if (string.IsNullOrEmpty(stringToHash)) {
				throw new ArgumentNullException(nameof(stringToHash));
			}

			if (hashingMethod == EHashingMethod.PlainText) {
				return stringToHash;
			}

			byte[] passwordBytes = Encoding.UTF8.GetBytes(stringToHash);
			byte[] hashBytes = Hash(passwordBytes, EncryptionKey, DefaultHashLength, hashingMethod);

			return Convert.ToBase64String(hashBytes);
		}

		internal static byte[] Hash(byte[] password, byte[] salt, byte hashLength, EHashingMethod hashingMethod) {
			if ((password == null) || (password.Length == 0)) {
				throw new ArgumentNullException(nameof(password));
			}

			if ((salt == null) || (salt.Length == 0)) {
				throw new ArgumentNullException(nameof(salt));
			}

			if (hashLength == 0) {
				throw new ArgumentOutOfRangeException(nameof(hashLength));
			}

			if (!Enum.IsDefined(typeof(EHashingMethod), hashingMethod)) {
				throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
			}

			switch (hashingMethod) {
				case EHashingMethod.PlainText:
					return password;
				case EHashingMethod.SCrypt:
					return SCrypt.ComputeDerivedKey(password, salt, SteamParentalSCryptIterations, SteamParentalSCryptBlocksCount, 1, null, hashLength);
				case EHashingMethod.Pbkdf2:
					using (HMACSHA256 hmacAlgorithm = new(password)) {
						return Pbkdf2.ComputeDerivedKey(hmacAlgorithm, salt, SteamParentalPbkdf2Iterations, hashLength);
					}
				default:
					throw new ArgumentOutOfRangeException(nameof(hashingMethod));
			}
		}

		internal static string? RecoverSteamParentalCode(byte[] passwordHash, byte[] salt, EHashingMethod hashingMethod) {
			if ((passwordHash == null) || (passwordHash.Length == 0)) {
				throw new ArgumentNullException(nameof(passwordHash));
			}

			if ((salt == null) || (salt.Length == 0)) {
				throw new ArgumentNullException(nameof(salt));
			}

			if (!Enum.IsDefined(typeof(EHashingMethod), hashingMethod)) {
				throw new InvalidEnumArgumentException(nameof(hashingMethod), (int) hashingMethod, typeof(EHashingMethod));
			}

			byte[]? password = SteamParentalCodes.AsParallel().FirstOrDefault(passwordToTry => Hash(passwordToTry, salt, (byte) passwordHash.Length, hashingMethod).SequenceEqual(passwordHash));

			return password != null ? Encoding.UTF8.GetString(password) : null;
		}

		internal static void SetEncryptionKey(string key) {
			if (string.IsNullOrEmpty(key)) {
				throw new ArgumentNullException(nameof(key));
			}

			if (!HasDefaultCryptKey) {
				ASF.ArchiLogger.LogGenericError(Strings.ErrorAborted);

				return;
			}

			Utilities.InBackground(
				() => {
					(bool isWeak, string? reason) = Utilities.TestPasswordStrength(key, ForbiddenCryptKeyPhrases);

					if (isWeak) {
						ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningWeakCryptKey, reason));
					}
				}
			);

			byte[] encryptionKey = Encoding.UTF8.GetBytes(key);

			if (encryptionKey.Length < MinimumRecommendedCryptKeyBytes) {
				ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningTooShortCryptKey, MinimumRecommendedCryptKeyBytes));
			}

			HasDefaultCryptKey = encryptionKey.SequenceEqual(EncryptionKey);
			EncryptionKey = encryptionKey;
		}

		private static string? DecryptAES(string encryptedString) {
			if (string.IsNullOrEmpty(encryptedString)) {
				throw new ArgumentNullException(nameof(encryptedString));
			}

			try {
				byte[] key;

				using (SHA256 sha256 = SHA256.Create()) {
					key = sha256.ComputeHash(EncryptionKey);
				}

				byte[] decryptedData = Convert.FromBase64String(encryptedString);
				decryptedData = CryptoHelper.SymmetricDecrypt(decryptedData, key);

				return Encoding.UTF8.GetString(decryptedData);
			} catch (Exception e) {
				ASF.ArchiLogger.LogGenericException(e);

				return null;
			}
{'fraction_non_alphanumeric': 0.1113423517169615, 'fraction_numerical': 0.0078349758217543, 'mean_word_length': 4.451451451451452, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6635645', 'n_tokens_mistral': 5703, 'n_tokens_neox': 4869, 'n_words': 1003}