1 // ----------------------------------------------------------------------------------------------
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
9 // Copyright 2015-2024 Ćukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
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
16 // http://www.apache.org/licenses/LICENSE-2.0
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.
25 using System
.Collections
.Generic
;
26 using System
.ComponentModel
;
27 using System
.Globalization
;
30 using System
.Security
.Cryptography
;
32 using System
.Threading
.Tasks
;
33 using ArchiSteamFarm
.Core
;
34 using ArchiSteamFarm
.Localization
;
35 using CryptSharp
.Utility
;
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
{
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
) {
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,
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
);
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
);
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
);
218 private static string? DecryptProtectedDataForCurrentUser(string text
) {
219 ArgumentException
.ThrowIfNullOrEmpty(text
);
221 if (!OperatingSystem
.IsWindows()) {
226 byte[] decryptedData
= ProtectedData
.Unprotect(
227 Convert
.FromBase64String(text
),
229 DataProtectionScope
.CurrentUser
232 return Encoding
.UTF8
.GetString(decryptedData
);
233 } catch (Exception e
) {
234 ASF
.ArchiLogger
.LogGenericException(e
);
240 private static string? EncryptAES(string text
) {
241 ArgumentException
.ThrowIfNullOrEmpty(text
);
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
);
257 private static string? EncryptProtectedDataForCurrentUser(string text
) {
258 ArgumentException
.ThrowIfNullOrEmpty(text
);
260 if (!OperatingSystem
.IsWindows()) {
265 byte[] encryptedData
= ProtectedData
.Protect(
266 Encoding
.UTF8
.GetBytes(text
),
268 DataProtectionScope
.CurrentUser
271 return Convert
.ToBase64String(encryptedData
);
272 } catch (Exception e
) {
273 ASF
.ArchiLogger
.LogGenericException(e
);
279 private static async Task
<string?> ReadFromFile(string filePath
) {
280 ArgumentException
.ThrowIfNullOrEmpty(filePath
);
282 if (!File
.Exists(filePath
)) {
289 text
= await File
.ReadAllTextAsync(filePath
).ConfigureAwait(false);
290 } catch (Exception e
) {
291 ASF
.ArchiLogger
.LogGenericException(e
);
299 public enum ECryptoMethod
: byte {
302 ProtectedDataForCurrentUser
,
307 public enum EHashingMethod
: byte {