using System.Runtime.InteropServices; using System.Security; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Serialization; using DeviceId; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Models; internal record struct KeyInfo(byte[] Key, byte[] Salt, int Iterations); /// /// Global instance of user secrets. /// Stored in %APPDATA%\StabilityMatrix\user-secrets.data /// public class GlobalUserSecrets { private const int KeySize = 32; private const int Iterations = 300; private const int SaltSize = 16; [JsonIgnore] public static FilePath File { get; } = GlobalConfig.HomeDir + "user-secrets.data"; public Dictionary PatreonCookies { get; set; } = new(); private static string? GetComputerSid() { var deviceId = new DeviceIdBuilder() .AddMachineName() .AddOsVersion() .OnWindows(windows => windows .AddProcessorId() .AddMotherboardSerialNumber() .AddSystemDriveSerialNumber()) .OnLinux(linux => linux .AddMotherboardSerialNumber() .AddSystemDriveSerialNumber()) .OnMac(mac => mac .AddSystemDriveSerialNumber() .AddPlatformSerialNumber()) .ToString(); return deviceId; } private static SecureString GetComputerKeyPhrase() { var keySource = GetComputerSid(); // If no sid, use username as fallback keySource ??= Environment.UserName; // XOR with fixed constant const string keyPhrase = "StabilityMatrix"; var result = new SecureString(); for (var i = 0; i < keySource.Length; i++) { result.AppendChar((char)(keySource[i] ^ keyPhrase[i % keyPhrase.Length])); } return result; } private static KeyInfo DeriveKeyWithSalt(SecureString password, int saltLength, int iterations, int keyLength) { var salt = RandomNumberGenerator.GetBytes(saltLength); var key = DeriveKey(password, salt, iterations, keyLength); return new KeyInfo(key, salt, iterations); } private static byte[] DeriveKey(SecureString password, byte[] salt, int iterations, int keyLength) { var ptr = Marshal.SecureStringToBSTR(password); try { var length = Marshal.ReadInt32(ptr, -4); var passwordByteArray = new byte[length]; var handle = GCHandle.Alloc(passwordByteArray, GCHandleType.Pinned); try { for (var i = 0; i < length; i++) { passwordByteArray[i] = Marshal.ReadByte(ptr, i); } using var rfc2898 = new Rfc2898DeriveBytes(passwordByteArray, salt, iterations); return rfc2898.GetBytes(keyLength); } finally { Array.Clear(passwordByteArray, 0, passwordByteArray.Length); handle.Free(); } } finally { Marshal.ZeroFreeBSTR(ptr); } } private static (byte[], byte[]) EncryptBytes(byte[] data) { var keyInfo = DeriveKeyWithSalt(GetComputerKeyPhrase(), SaltSize, Iterations, KeySize); using var aes = Aes.Create(); aes.Key = keyInfo.Key; aes.IV = keyInfo.Salt; aes.Padding = PaddingMode.PKCS7; aes.Mode = CipherMode.CBC; var transform = aes.CreateEncryptor(); return (transform.TransformFinalBlock(data, 0, data.Length), keyInfo.Salt); } private static byte[] DecryptBytes(IReadOnlyCollection encryptedData, byte[] salt) { var key = DeriveKey(GetComputerKeyPhrase(), salt, Iterations, KeySize); using var aes = Aes.Create(); aes.Key = key; aes.IV = salt; aes.Padding = PaddingMode.PKCS7; aes.Mode = CipherMode.CBC; var transform = aes.CreateDecryptor(); return transform.TransformFinalBlock(encryptedData.ToArray(), 0, encryptedData.Count); } public void SaveToFile() { var json = JsonSerializer.SerializeToUtf8Bytes(this); var (encrypted, salt) = EncryptBytes(json); // Prepend salt to encrypted json var fileBytes = salt.Concat(encrypted).ToArray(); File.WriteAllBytes(fileBytes); } public static GlobalUserSecrets? LoadFromFile() { var fileBytes = File.ReadAllBytes(); // Get salt from start of file var salt = fileBytes.AsSpan(0, SaltSize).ToArray(); // Get encrypted json from rest of file var encryptedJson = fileBytes.AsSpan(SaltSize).ToArray(); var json = DecryptBytes(encryptedJson, salt); return JsonSerializer.Deserialize(json); } }