using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Timers; using static MinecraftClient.Settings; using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig; namespace MinecraftClient.Protocol.Keys { /// /// Handle keys caching and storage. /// public static class KeysCache { private const string KeysCacheFilePlaintext = "ProfileKeyCache.ini"; private static FileMonitor? cachemonitor; private static readonly Dictionary keys = new(); private static readonly Timer updatetimer = new(100); private static readonly List> pendingadds = new(); private static readonly BinaryFormatter formatter = new(); /// /// Retrieve whether KeysCache contains a keys for the given login. /// /// User login used with Minecraft.net /// TRUE if keys are available public static bool Contains(string login) { return keys.ContainsKey(login); } /// /// Store keys and save it to disk if required. /// /// User login used with Minecraft.net /// User keys public static void Store(string login, PlayerKeyPair playerKeyPair) { if (Contains(login)) { keys[login] = playerKeyPair; } else { keys.Add(login, playerKeyPair); } if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk && updatetimer.Enabled == true) { pendingadds.Add(new KeyValuePair(login, playerKeyPair)); } else if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk) { SaveToDisk(); } } /// /// Retrieve keys for the given login. /// /// User login used with Minecraft.net /// PlayerKeyPair for given login public static PlayerKeyPair Get(string login) { return keys[login]; } /// /// Initialize cache monitoring to keep cache updated with external changes. /// /// TRUE if keys are seeded from file public static bool InitializeDiskCache() { cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, KeysCacheFilePlaintext, new FileSystemEventHandler(OnChanged)); updatetimer.Elapsed += HandlePending; return LoadFromDisk(); } /// /// Reloads cache on external cache file change. /// /// Sender /// Event data private static void OnChanged(object sender, FileSystemEventArgs e) { updatetimer.Stop(); updatetimer.Start(); } /// /// Called after timer elapsed. Reads disk cache and adds new/modified keys back. /// /// Sender /// Event data private static void HandlePending(object? sender, ElapsedEventArgs e) { updatetimer.Stop(); LoadFromDisk(); foreach (KeyValuePair pending in pendingadds.ToArray()) { Store(pending.Key, pending.Value); pendingadds.Remove(pending); } } /// /// Reads cache file and loads KeysInfos into KeysCache. /// /// True if data is successfully loaded private static bool LoadFromDisk() { //User-editable keys cache file in text format if (File.Exists(KeysCacheFilePlaintext)) { if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_keys, KeysCacheFilePlaintext)); try { foreach (string line in FileMonitor.ReadAllLinesWithRetries(KeysCacheFilePlaintext)) { if (!line.TrimStart().StartsWith("#")) { int separatorIdx = line.IndexOf('='); if (separatorIdx >= 1 && line.Length > separatorIdx + 1) { string login = line[..separatorIdx]; string value = line[(separatorIdx + 1)..]; try { PlayerKeyPair playerKeyPair = PlayerKeyPair.FromString(value); keys[login] = playerKeyPair; if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded_keys, playerKeyPair.ExpiresAt.ToString())); } catch (InvalidDataException e) { if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message)); } catch (FormatException e) { if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message)); } catch (ArgumentNullException e) { if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message)); } } else if (Settings.Config.Logging.DebugMessages) { ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line_keys, line)); } } } } catch (IOException e) { ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain_keys, e.Message)); } } return keys.Count > 0; } /// /// Saves player's keypair from KeysCache into cache file. /// private static void SaveToDisk() { if (Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving_keys, acceptnewlines: true); List KeysCacheLines = new() { "# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!", "# ProfileKey=PublicKey(base64),PublicKeySignature(base64),PublicKeySignatureV2(base64),PrivateKey(base64),ExpiresAt,RefreshAfter" }; foreach (KeyValuePair entry in keys) KeysCacheLines.Add(entry.Key + '=' + entry.Value.ToString()); try { FileMonitor.WriteAllLinesWithRetries(KeysCacheFilePlaintext, KeysCacheLines); } catch (IOException e) { ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail_keys, e.Message)); } } } }