diff --git a/DiscordChatExporter.Gui/Services/TokenEncryptionConverter.cs b/DiscordChatExporter.Gui/Services/TokenEncryptionConverter.cs index 8b2af9f5..b85640b7 100644 --- a/DiscordChatExporter.Gui/Services/TokenEncryptionConverter.cs +++ b/DiscordChatExporter.Gui/Services/TokenEncryptionConverter.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -11,7 +12,53 @@ internal class TokenEncryptionConverter : JsonConverter private const string Prefix = "enc:"; private const int MaxPaddingLength = 16; - private static readonly byte[] Key = "DCE-Token-Key-v1"u8.ToArray(); + // Key is derived from a machine-specific identifier so that a stolen Settings.dat + // cannot be decrypted on a different machine. + private static readonly Lazy Key = new(DeriveKey); + + private static byte[] DeriveKey() + { + var machineId = GetMachineId(); + return Rfc2898DeriveBytes.Pbkdf2( + Encoding.UTF8.GetBytes(machineId), + "DCE-Token-Salt"u8.ToArray(), + iterations: 10_000, + HashAlgorithmName.SHA256, + outputLength: 16 + ); + } + + private static string GetMachineId() + { + // Windows: stable GUID written during OS installation + if (OperatingSystem.IsWindows()) + { + try + { + using var regKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey( + @"SOFTWARE\Microsoft\Cryptography" + ); + if (regKey?.GetValue("MachineGuid") is string guid && guid.Length > 0) + return guid; + } + catch { } + } + + // Linux: /etc/machine-id (set once by systemd at first boot) + foreach (var path in new[] { "/etc/machine-id", "/var/lib/dbus/machine-id" }) + { + try + { + var id = File.ReadAllText(path).Trim(); + if (id.Length > 0) + return id; + } + catch { } + } + + // Last-resort fallback (e.g. macOS without /etc/machine-id) + return Environment.MachineName; + } public override string? Read( ref Utf8JsonReader reader, @@ -29,7 +76,7 @@ internal class TokenEncryptionConverter : JsonConverter { var data = Convert.FromBase64String(value[Prefix.Length..]); using var aes = Aes.Create(); - aes.Key = Key; + aes.Key = Key.Value; // Layout: IV (16 bytes) | padLen (1 byte) | ciphertext var decrypted = aes.DecryptCbc(data[17..], data[..16]); @@ -50,7 +97,7 @@ internal class TokenEncryptionConverter : JsonConverter } using var aes = Aes.Create(); - aes.Key = Key; + aes.Key = Key.Value; aes.GenerateIV(); // Random IV ensures non-deterministic output // Random padding bytes vary the output length