Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Keyring v0.2.1
Keyring.dll
Decompiled 2 weeks agousing System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Splatform; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("Adam Robichaud")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © 2026 Adam Robichaud")] [assembly: AssemblyDescription("Remembers Valheim server passwords in the operating system's credential store, logs you in automatically, and adds a Continue button to the main menu.")] [assembly: AssemblyFileVersion("0.2.1.0")] [assembly: AssemblyInformationalVersion("0.2.1+d9fa889d799d55e57262f035e0ce5e38001e0ec0")] [assembly: AssemblyProduct("Keyring")] [assembly: AssemblyTitle("Keyring")] [assembly: AssemblyVersion("0.2.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Keyring { [HarmonyPatch] internal static class ContinueMenu { [CompilerGenerated] private static class <>O { public static PopupButtonCallback <0>__Pop; } private const string ButtonName = "Keyring_Continue"; private static readonly FieldInfo F_menuButtons = AccessTools.Field(typeof(FejdStartup), "m_menuButtons"); private static readonly FieldInfo F_queuedJoinServer = AccessTools.Field(typeof(FejdStartup), "m_queuedJoinServer"); private static readonly FieldInfo F_joinServer = AccessTools.Field(typeof(FejdStartup), "m_joinServer"); private static readonly FieldInfo F_startingWorld = AccessTools.Field(typeof(FejdStartup), "m_startingWorld"); private static readonly FieldInfo F_world = AccessTools.Field(typeof(FejdStartup), "m_world"); private static readonly FieldInfo F_profiles = AccessTools.Field(typeof(FejdStartup), "m_profiles"); private static readonly FieldInfo F_profileIndex = AccessTools.Field(typeof(FejdStartup), "m_profileIndex"); private static readonly MethodInfo M_setSelectedProfile = AccessTools.Method(typeof(FejdStartup), "SetSelectedProfile", (Type[])null, (Type[])null); private static readonly MethodInfo M_findWorld = AccessTools.Method(typeof(FejdStartup), "FindWorld", (Type[])null, (Type[])null); private static readonly MethodInfo M_hideAll = AccessTools.Method(typeof(FejdStartup), "HideAll", (Type[])null, (Type[])null); private static Button _button; private static HoverTooltip _tooltip; [HarmonyPatch(typeof(FejdStartup), "TransitionToMainScene")] [HarmonyPrefix] private static void BeforeTransition(FejdStartup __instance) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown //IL_0055: Unknown result type (might be due to invalid IL or missing references) try { PlayerProfile profile = SelectedProfile(__instance); if ((bool)F_startingWorld.GetValue(__instance)) { World val = (World)F_world.GetValue(__instance); if (val != null) { LastSession.CaptureWorld(val.m_name, profile); } } else { ServerJoinData server = (ServerJoinData)F_joinServer.GetValue(__instance); if (((ServerJoinData)(ref server)).IsValid) { LastSession.CaptureServer(server, profile); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not record the session for Continue: " + ex.Message)); } } [HarmonyPatch(typeof(Player), "OnSpawned")] [HarmonyPostfix] private static void AfterSpawned() { LastSession.CommitCandidate(); } private static PlayerProfile SelectedProfile(FejdStartup fs) { List<PlayerProfile> list = (List<PlayerProfile>)F_profiles.GetValue(fs); int num = (int)F_profileIndex.GetValue(fs); if (list == null || num < 0 || num >= list.Count) { return null; } return list[num]; } [HarmonyPatch(typeof(FejdStartup), "Awake")] [HarmonyPostfix] private static void AfterAwake(FejdStartup __instance) { if (!Plugin.ShowContinue.Value || (Object)(object)__instance.m_menuList == (Object)null) { return; } try { AddButton(__instance); } catch (Exception ex) { Plugin.Log.LogError((object)("Could not add the Continue button: " + ex)); } } private static void AddButton(FejdStartup fs) { //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Expected O, but got Unknown //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Expected O, but got Unknown //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Expected O, but got Unknown //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) Button[] componentsInChildren = fs.m_menuList.GetComponentsInChildren<Button>(true); if (componentsInChildren.Length == 0) { return; } Button val = FindStartButton(componentsInChildren) ?? componentsInChildren[0]; if (((Object)val).name == "Keyring_Continue") { return; } GameObject val2 = Object.Instantiate<GameObject>(((Component)val).gameObject, ((Component)val).transform.parent); ((Object)val2).name = "Keyring_Continue"; val2.transform.SetSiblingIndex(((Component)val).transform.GetSiblingIndex()); val2.SetActive(true); if ((Object)(object)((Component)((Component)val).transform.parent).GetComponent<LayoutGroup>() == (Object)null) { RectTransform val3 = (RectTransform)val2.transform; RectTransform val4 = (RectTransform)((Component)val).transform; Rect rect = val4.rect; Vector2 val5 = default(Vector2); ((Vector2)(ref val5))..ctor(0f, ((Rect)(ref rect)).height + 4f); int num = ((Component)val).transform.GetSiblingIndex() + 1; if (num < ((Component)val).transform.parent.childCount) { Transform child = ((Component)val).transform.parent.GetChild(num); RectTransform val6 = (RectTransform)(object)((child is RectTransform) ? child : null); if (val6 != null && (Object)(object)((Component)val6).GetComponent<Button>() != (Object)null) { val5 = val4.anchoredPosition - val6.anchoredPosition; } } val3.anchoredPosition = val4.anchoredPosition + val5; } TMP_Text componentInChildren = val2.GetComponentInChildren<TMP_Text>(true); if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.text = "Continue"; } _button = val2.GetComponent<Button>(); _button.onClick = new ButtonClickedEvent(); ((UnityEvent)_button.onClick).AddListener((UnityAction)delegate { OnContinue(fs); }); _tooltip = val2.AddComponent<HoverTooltip>(); _tooltip.Build(componentInChildren); Refresh(); F_menuButtons.SetValue(fs, fs.m_menuList.GetComponentsInChildren<Button>()); } private static Button FindStartButton(Button[] buttons) { Button[] array = buttons; foreach (Button val in array) { for (int j = 0; j < ((UnityEventBase)val.onClick).GetPersistentEventCount(); j++) { if (((UnityEventBase)val.onClick).GetPersistentMethodName(j) == "OnStartGame") { return val; } } } array = buttons; foreach (Button val2 in array) { if (((Object)val2).name.ToLowerInvariant().Contains("start")) { return val2; } } return null; } internal static void Refresh() { if (!((Object)(object)_button == (Object)null)) { bool interactable = LastSession.Current != LastSession.Kind.None; ((Selectable)_button).interactable = interactable; if ((Object)(object)_tooltip != (Object)null) { _tooltip.Text = LastSession.Describe(); } } } private static void OnContinue(FejdStartup fs) { //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Expected O, but got Unknown //IL_0086: Unknown result type (might be due to invalid IL or missing references) LastSession.Kind current = LastSession.Current; if (current == LastSession.Kind.None) { return; } fs.m_mainMenu.SetActive(false); M_setSelectedProfile.Invoke(fs, new object[1] { LastSession.CharacterFile }); PlayerProfile val = SelectedProfile(fs); if (val == null || val.GetFilename() != LastSession.CharacterFile) { Fail(fs, "Character \"" + LastSession.CharacterName + "\" was not found. Pick one from Start Game."); return; } if (current == LastSession.Kind.Server) { if (!LastSession.TryGetServer(out var server)) { Fail(fs, "The remembered server could not be read. Join it once more from the server list."); return; } F_queuedJoinServer.SetValue(fs, server); fs.OnCharacterStart(); return; } F_queuedJoinServer.SetValue(fs, ServerJoinData.None); fs.OnCharacterStart(); World val2 = (World)M_findWorld.Invoke(fs, new object[1] { LastSession.WorldName }); if (val2 == null) { Fail(fs, "World \"" + LastSession.WorldName + "\" was not found. Pick one from Start Game."); return; } F_world.SetValue(fs, val2); fs.OnWorldStart(); } private static void Fail(FejdStartup fs, string message) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Expected O, but got Unknown //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown Plugin.Log.LogWarning((object)("Continue: " + message)); LastSession.Clear(); Refresh(); M_hideAll?.Invoke(fs, null); fs.m_mainMenu.SetActive(true); object obj = <>O.<0>__Pop; if (obj == null) { PopupButtonCallback val = UnifiedPopup.Pop; <>O.<0>__Pop = val; obj = (object)val; } UnifiedPopup.Push((PopupBase)new WarningPopup("Continue", message, (PopupButtonCallback)obj, false)); } } internal sealed class HoverTooltip : MonoBehaviour, IPointerEnterHandler, IEventSystemHandler, IPointerExitHandler, ISelectHandler, IDeselectHandler { private const float GapPixels = 16f; private GameObject _panel; private RectTransform _panelRt; private TMP_Text _label; private string _text = ""; private readonly Vector3[] _corners = (Vector3[])(object)new Vector3[4]; public string Text { get { return _text; } set { _text = value ?? ""; if ((Object)(object)_label != (Object)null) { _label.text = _text; } } } public void Build(TMP_Text styleSource) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Expected O, but got Unknown //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Expected O, but got Unknown //IL_01bb: Unknown result type (might be due to invalid IL or missing references) //IL_01c1: Expected O, but got Unknown //IL_0267: Unknown result type (might be due to invalid IL or missing references) _panel = new GameObject("Keyring_ContinueTooltip", new Type[5] { typeof(RectTransform), typeof(Image), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter), typeof(LayoutElement) }); _panel.GetComponent<LayoutElement>().ignoreLayout = true; Canvas componentInParent = ((Component)this).GetComponentInParent<Canvas>(); Transform val = (((Object)(object)componentInParent != (Object)null) ? ((Component)componentInParent.rootCanvas).transform : ((Component)this).transform.parent); _panel.transform.SetParent(val, false); _panel.transform.SetAsLastSibling(); _panelRt = (RectTransform)_panel.transform; _panelRt.anchorMin = new Vector2(0f, 0f); _panelRt.anchorMax = new Vector2(0f, 0f); _panelRt.pivot = new Vector2(0f, 0.5f); Image component = _panel.GetComponent<Image>(); ((Graphic)component).color = new Color(0.05f, 0.05f, 0.05f, 0.9f); ((Graphic)component).raycastTarget = false; VerticalLayoutGroup component2 = _panel.GetComponent<VerticalLayoutGroup>(); ((LayoutGroup)component2).padding = new RectOffset(12, 12, 8, 8); ((HorizontalOrVerticalLayoutGroup)component2).childControlWidth = true; ((HorizontalOrVerticalLayoutGroup)component2).childControlHeight = true; ((HorizontalOrVerticalLayoutGroup)component2).childForceExpandWidth = false; ((HorizontalOrVerticalLayoutGroup)component2).childForceExpandHeight = false; ContentSizeFitter component3 = _panel.GetComponent<ContentSizeFitter>(); component3.horizontalFit = (FitMode)2; component3.verticalFit = (FitMode)2; GameObject val2 = new GameObject("Text", new Type[2] { typeof(RectTransform), typeof(LayoutElement) }); val2.transform.SetParent(_panel.transform, false); val2.GetComponent<LayoutElement>().preferredWidth = 380f; _label = (TMP_Text)(object)val2.AddComponent<TextMeshProUGUI>(); if ((Object)(object)styleSource != (Object)null) { _label.font = styleSource.font; _label.fontSharedMaterial = styleSource.fontSharedMaterial; _label.fontSize = Mathf.Max(14f, styleSource.fontSize * 0.7f); } else { _label.fontSize = 18f; } ((Graphic)_label).color = new Color(0.95f, 0.9f, 0.75f); _label.alignment = (TextAlignmentOptions)513; _label.textWrappingMode = (TextWrappingModes)1; ((Graphic)_label).raycastTarget = false; _label.text = _text; _panel.SetActive(false); } public void OnPointerEnter(PointerEventData eventData) { Show(on: true); } public void OnPointerExit(PointerEventData eventData) { Show(on: false); } public void OnSelect(BaseEventData eventData) { Show(on: true); } public void OnDeselect(BaseEventData eventData) { Show(on: false); } private void OnDisable() { Show(on: false); } private void OnDestroy() { if ((Object)(object)_panel != (Object)null) { Object.Destroy((Object)(object)_panel); } } private void Show(bool on) { if (!((Object)(object)_panel == (Object)null)) { if (on) { Place(); } _panel.SetActive(on); } } private void Place() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) ((RectTransform)((Component)this).transform).GetWorldCorners(_corners); Vector3 val = (_corners[2] + _corners[3]) * 0.5f; float x = ((Transform)_panelRt).lossyScale.x; ((Transform)_panelRt).position = val + new Vector3(16f * x, 0f, 0f); } private void LateUpdate() { if ((Object)(object)_panel != (Object)null && _panel.activeSelf) { Place(); } } } internal static class CredentialStoreFactory { public static ICredentialStore Create() { if (Environment.OSVersion.Platform != PlatformID.Win32NT) { return new NullCredentialStore("no credential store backend for " + Environment.OSVersion.Platform.ToString() + "; passwords are not saved on this platform"); } if (IsWine()) { if (!Plugin.AllowWineStore.Value) { return new NullCredentialStore("running under Wine/Proton, whose credential store is not encrypted; set AllowWineStore=true to use it anyway"); } Plugin.Log.LogWarning((object)"Running under Wine/Proton: the emulated Windows credential store is only obfuscated, not encrypted."); } WindowsCredentialStore windowsCredentialStore = new WindowsCredentialStore(); if (!windowsCredentialStore.SelfTest(out var error)) { return new NullCredentialStore("Windows Credential Manager unavailable: " + error); } return windowsCredentialStore; } [DllImport("ntdll.dll", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr wine_get_version(); private static bool IsWine() { try { wine_get_version(); return true; } catch (EntryPointNotFoundException) { return false; } catch (DllNotFoundException) { return false; } catch (Exception ex3) { Plugin.Log.LogDebug((object)("Wine check failed: " + ex3.Message)); return false; } } } internal struct StoredEntry { public string Key; public string Comment; public DateTime LastWritten; } internal interface ICredentialStore { string Description { get; } bool IsAvailable { get; } bool TryRead(string key, out string secret); bool Write(string key, string secret, string comment); bool Delete(string key); List<StoredEntry> List(); } internal sealed class NullCredentialStore : ICredentialStore { private readonly string _reason; public string Description => "none (" + _reason + ")"; public bool IsAvailable => false; public NullCredentialStore(string reason) { _reason = reason; } public bool TryRead(string key, out string secret) { secret = null; return false; } public bool Write(string key, string secret, string comment) { return false; } public bool Delete(string key) { return false; } public List<StoredEntry> List() { return new List<StoredEntry>(); } } [HarmonyPatch(typeof(Terminal), "InitTerminal")] internal static class KeyringCommands { [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__Run; } [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleOptionsFetcher <>9__1_0; internal List<string> <Postfix>b__1_0() { return new List<string> { "list", "forget", "status" }; } } private static List<StoredEntry> _lastList = new List<StoredEntry>(); private static void Postfix() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Expected O, but got Unknown object obj = <>O.<0>__Run; if (obj == null) { ConsoleEvent val = Run; <>O.<0>__Run = val; obj = (object)val; } object obj2 = <>c.<>9__1_0; if (obj2 == null) { ConsoleOptionsFetcher val2 = () => new List<string> { "list", "forget", "status" }; <>c.<>9__1_0 = val2; obj2 = (object)val2; } new ConsoleCommand("keyring", "[list|forget <n|all>|status] Manage server passwords saved by Keyring", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)obj2, false, false, false); } private static void Run(ConsoleEventArgs args) { Terminal context = args.Context; switch ((args.Length > 1) ? args[1].ToLowerInvariant() : "list") { case "list": ListEntries(context); return; case "forget": Forget(context, (args.Length > 2) ? args[2] : ""); return; case "status": Status(context); return; } if (context != null) { context.AddString("Usage: keyring [list|forget <n|all>|status]"); } } private static void ListEntries(Terminal t) { if (!Plugin.Store.IsAvailable) { if (t != null) { t.AddString("Keyring: no credential store available (" + Plugin.Store.Description + ")."); } return; } _lastList = Plugin.Store.List(); if (_lastList.Count == 0) { if (t != null) { t.AddString("Keyring: no saved server passwords."); } return; } if (t != null) { t.AddString("Keyring: " + _lastList.Count + " saved server password(s) in " + Plugin.Store.Description + ":"); } for (int i = 0; i < _lastList.Count; i++) { StoredEntry storedEntry = _lastList[i]; string text = ((storedEntry.LastWritten == DateTime.MinValue) ? "" : (" (saved " + storedEntry.LastWritten.ToString("yyyy-MM-dd HH:mm") + ")")); string text2 = (string.IsNullOrEmpty(storedEntry.Comment) ? storedEntry.Key : (storedEntry.Comment + " [" + storedEntry.Key + "]")); if (t != null) { t.AddString(" " + (i + 1) + ". " + text2 + text); } } if (t != null) { t.AddString("Use 'keyring forget <n>' to delete one, or 'keyring forget all'."); } } private static void Forget(Terminal t, string which) { int result; if (!Plugin.Store.IsAvailable) { if (t != null) { t.AddString("Keyring: no credential store available."); } } else if (which == "all") { int num = 0; foreach (StoredEntry item in Plugin.Store.List()) { if (Plugin.Store.Delete(item.Key)) { num++; } } _lastList.Clear(); if (t != null) { t.AddString("Keyring: forgot " + num + " saved password(s)."); } } else if (int.TryParse(which, out result)) { if (_lastList.Count == 0) { _lastList = Plugin.Store.List(); } if (result < 1 || result > _lastList.Count) { if (t != null) { t.AddString("Keyring: no entry " + result + ". Run 'keyring list' first."); } return; } StoredEntry storedEntry = _lastList[result - 1]; string text = (string.IsNullOrEmpty(storedEntry.Comment) ? storedEntry.Key : storedEntry.Comment); if (t != null) { t.AddString(Plugin.Store.Delete(storedEntry.Key) ? ("Keyring: forgot the password for " + text + ".") : ("Keyring: could not delete the entry for " + text + ".")); } _lastList.Clear(); } else if (t != null) { t.AddString("Usage: keyring forget <n|all> (n from 'keyring list')"); } } private static void Status(Terminal t) { if (t != null) { t.AddString("Keyring 0.2.1"); } if (t != null) { t.AddString(" Store: " + Plugin.Store.Description); } if (t != null) { t.AddString(" RememberPasswords: " + Plugin.RememberPasswords.Value); } if (t != null) { t.AddString(" AutoLogin: " + Plugin.AutoLogin.Value); } if (t != null) { t.AddString(" ForgetRejected: " + Plugin.ForgetRejected.Value); } if (Plugin.Store.IsAvailable && t != null) { t.AddString(" Saved servers: " + Plugin.Store.List().Count); } } } internal static class LastSession { public enum Kind { None, World, Server } private sealed class Candidate { public Kind Kind; public string CharacterFile; public string CharacterName; public string WorldName; public ServerJoinData Server; public string ServerName; } private const string Section = "9 - Last session (written by the mod)"; private static ConfigEntry<Kind> _kind; private static ConfigEntry<string> _characterFile; private static ConfigEntry<string> _characterName; private static ConfigEntry<string> _worldName; private static ConfigEntry<ServerJoinDataType> _serverType; private static ConfigEntry<string> _serverAddress; private static ConfigEntry<string> _serverOwner; private static ConfigEntry<string> _serverName; private static Candidate _candidate; public static Kind Current => _kind.Value; public static string CharacterFile => _characterFile.Value; public static string CharacterName => _characterName.Value; public static string WorldName => _worldName.Value; public static string ServerName => _serverName.Value; public static void Bind(ConfigFile config) { _kind = config.Bind<Kind>("9 - Last session (written by the mod)", "LastKind", Kind.None, "Filled in automatically by the Continue button. Clear LastKind to None to forget it."); _characterFile = config.Bind<string>("9 - Last session (written by the mod)", "LastCharacterFile", "", "Filled in automatically by the Continue button. Clear LastKind to None to forget it."); _characterName = config.Bind<string>("9 - Last session (written by the mod)", "LastCharacterName", "", "Filled in automatically by the Continue button. Clear LastKind to None to forget it."); _worldName = config.Bind<string>("9 - Last session (written by the mod)", "LastWorldName", "", "Filled in automatically by the Continue button. Clear LastKind to None to forget it."); _serverType = config.Bind<ServerJoinDataType>("9 - Last session (written by the mod)", "LastServerType", (ServerJoinDataType)0, "Filled in automatically by the Continue button. Clear LastKind to None to forget it."); _serverAddress = config.Bind<string>("9 - Last session (written by the mod)", "LastServerAddress", "", "Filled in automatically by the Continue button. Clear LastKind to None to forget it."); _serverOwner = config.Bind<string>("9 - Last session (written by the mod)", "LastServerOwner", "", "Filled in automatically by the Continue button. Clear LastKind to None to forget it."); _serverName = config.Bind<string>("9 - Last session (written by the mod)", "LastServerName", "", "Filled in automatically by the Continue button. Clear LastKind to None to forget it."); } public static void Clear() { _kind.Value = Kind.None; } public static void CaptureWorld(string worldName, PlayerProfile profile) { _candidate = new Candidate { Kind = Kind.World, WorldName = worldName, CharacterFile = (((profile != null) ? profile.GetFilename() : null) ?? ""), CharacterName = (((profile != null) ? profile.GetName() : null) ?? "") }; Plugin.Log.LogDebug((object)("Continue candidate: world " + worldName)); } public unsafe static void CaptureServer(ServerJoinData server, PlayerProfile profile) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) string text; try { text = MultiBackendMatchmaking.GetServerName(server); } catch { text = ((object)(*(ServerJoinData*)(&server))/*cast due to .constrained prefix*/).ToString(); } _candidate = new Candidate { Kind = Kind.Server, Server = server, ServerName = (text ?? ""), CharacterFile = (((profile != null) ? profile.GetFilename() : null) ?? ""), CharacterName = (((profile != null) ? profile.GetName() : null) ?? "") }; Plugin.Log.LogDebug((object)("Continue candidate: server " + text)); } public static void CommitCandidate() { //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) Candidate candidate = _candidate; _candidate = null; if (candidate != null) { _characterFile.Value = candidate.CharacterFile; _characterName.Value = candidate.CharacterName; if (candidate.Kind == Kind.World) { _worldName.Value = candidate.WorldName; _serverType.Value = (ServerJoinDataType)0; _serverAddress.Value = ""; _serverOwner.Value = ""; _serverName.Value = ""; } else { _worldName.Value = ""; _serverType.Value = candidate.Server.m_type; _serverAddress.Value = AddressOf(candidate.Server); _serverOwner.Value = ((object)Unsafe.As<PlatformUserID, PlatformUserID>(ref candidate.Server.m_owner)/*cast due to .constrained prefix*/).ToString() ?? ""; _serverName.Value = candidate.ServerName; } _kind.Value = candidate.Kind; Plugin.Log.LogInfo((object)("Continue will rejoin " + Describe())); } } private static string AddressOf(ServerJoinData server) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected I4, but got Unknown //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) ServerJoinDataType type = server.m_type; switch (type - 1) { case 0: { ulong steamID = ((ServerJoinData)(ref server)).SteamUser.m_joinUserID.m_SteamID; return steamID.ToString(); } case 1: return ((ServerJoinData)(ref server)).PlayFabUser.m_remotePlayerId ?? ""; case 2: { ServerJoinDataDedicated dedicated = ((ServerJoinData)(ref server)).Dedicated; string host = ((ServerJoinDataDedicated)(ref dedicated)).m_host; ushort port = ((ServerJoinData)(ref server)).Dedicated.m_port; return host + ":" + port; } default: return ""; } } public static bool TryGetServer(out ServerJoinData server) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected I4, but got Unknown //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) server = ServerJoinData.None; if (_kind.Value != Kind.Server || string.IsNullOrEmpty(_serverAddress.Value)) { return false; } try { ServerJoinDataType value = _serverType.Value; switch (value - 1) { case 0: { if (!ulong.TryParse(_serverAddress.Value, out var result) || result == 0L) { return false; } server = new ServerJoinData(new ServerJoinDataSteamUser(result)); return true; } case 1: { ServerJoinDataPlayFabUser val = default(ServerJoinDataPlayFabUser); ((ServerJoinDataPlayFabUser)(ref val))..ctor(_serverAddress.Value); server = (string.IsNullOrEmpty(_serverOwner.Value) ? new ServerJoinData(val) : new ServerJoinData(val, new PlatformUserID(_serverOwner.Value))); return true; } case 2: server = new ServerJoinData(new ServerJoinDataDedicated(_serverAddress.Value)); return ((ServerJoinData)(ref server)).IsValid; default: return false; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not rebuild the remembered server: " + ex.Message)); return false; } } public static string Describe() { //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Invalid comparison between Unknown and I4 //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Invalid comparison between Unknown and I4 //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Invalid comparison between Unknown and I4 string text = (string.IsNullOrEmpty(_characterName.Value) ? "" : (" as " + _characterName.Value)); switch (_kind.Value) { case Kind.World: return "Start world \"" + _worldName.Value + "\" (local)" + text; case Kind.Server: { string value = _serverAddress.Value; string text2 = (((int)_serverType.Value == 3) ? "dedicated" : (((int)_serverType.Value == 1) ? "Steam host" : (((int)_serverType.Value == 2) ? "crossplay host" : "server"))); string text3 = (string.IsNullOrEmpty(_serverName.Value) ? value : _serverName.Value); return "Rejoin \"" + text3 + "\"\n" + text2 + ": " + value + text; } default: return "Nothing to continue yet. Join a world or server and it will be remembered here."; } } } [HarmonyPatch] internal static class LoginPatches { private static readonly MethodInfo OnPasswordEntered = AccessTools.Method(typeof(ZNet), "OnPasswordEntered", (Type[])null, (Type[])null); private static string _pendingKey; private static string _pendingPassword; private static string _pendingName; private static string _autoKey; private static string _autoName; private static void ClearPending() { _pendingKey = null; _pendingPassword = null; _pendingName = null; } [HarmonyPatch(typeof(ZNet), "RPC_ClientHandshake")] [HarmonyPostfix] private static void AfterClientHandshake(ZNet __instance, bool needPassword) { ClearPending(); _autoKey = null; _autoName = null; if (!needPassword || !Plugin.AutoLogin.Value || !Plugin.Store.IsAvailable || FejdStartup.ServerPassword != null) { return; } if (OnPasswordEntered == null) { Plugin.Log.LogError((object)"ZNet.OnPasswordEntered not found; the game may have changed. Auto-login disabled."); return; } string text = ServerIdentity.CurrentKey(); if (Plugin.Store.TryRead(text, out var secret)) { _autoKey = text; _autoName = ServerIdentity.CurrentDisplayName(); Plugin.Log.LogInfo((object)("Sending saved password for " + _autoName)); OnPasswordEntered.Invoke(__instance, new object[1] { secret }); } } [HarmonyPatch(typeof(ZNet), "OnPasswordEntered")] [HarmonyPrefix] private static void BeforePasswordEntered(string pwd) { if (_autoKey == null && Plugin.RememberPasswords.Value && Plugin.Store.IsAvailable && !string.IsNullOrEmpty(pwd) && (FejdStartup.ServerPassword == null || !(pwd == FejdStartup.ServerPassword))) { _pendingKey = ServerIdentity.CurrentKey(); _pendingPassword = pwd; _pendingName = ServerIdentity.CurrentDisplayName(); } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPostfix] private static void AfterPeerInfo(ZNet __instance) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Invalid comparison between Unknown and I4 if (__instance.IsServer() || (int)ZNet.GetConnectionStatus() != 2) { return; } if (_autoKey != null) { Notices.Queue("Keyring: logged in to " + _autoName + " with the saved password."); _autoKey = null; _autoName = null; } if (_pendingKey != null) { if (Plugin.Store.Write(_pendingKey, _pendingPassword, _pendingName)) { Plugin.Log.LogInfo((object)("Saved password for " + _pendingName + " to " + Plugin.Store.Description)); Notices.Queue("Keyring: password for " + _pendingName + " saved to " + Plugin.Store.Description + "."); } else { Plugin.Log.LogWarning((object)("Could not save the password for " + _pendingName)); } ClearPending(); } } [HarmonyPatch(typeof(ZNet), "RPC_Error")] [HarmonyPostfix] private static void AfterError(int error) { if (error == 6 && _autoKey != null) { if (Plugin.ForgetRejected.Value) { Plugin.Store.Delete(_autoKey); Plugin.Log.LogWarning((object)("The saved password for " + _autoName + " was rejected and has been forgotten. Connect again to enter the new one.")); } else { Plugin.Log.LogWarning((object)("The saved password for " + _autoName + " was rejected. Use 'keyring forget' in the console to remove it.")); } } ClearPending(); _autoKey = null; _autoName = null; } } [HarmonyPatch] internal static class Notices { private static readonly Queue<string> Pending = new Queue<string>(); public static void Queue(string text) { if (Plugin.ShowMessages.Value) { Pending.Enqueue(text); } } [HarmonyPatch(typeof(Player), "OnSpawned")] [HarmonyPostfix] private static void AfterSpawned() { if (Pending.Count == 0) { return; } MessageHud instance = MessageHud.instance; while (Pending.Count > 0) { string text = Pending.Dequeue(); if ((Object)(object)instance != (Object)null) { instance.ShowMessage((MessageType)1, text, 0, (Sprite)null, false, true); } } } } [BepInPlugin("com.puhpuhburr.keyring", "Keyring", "0.2.1")] public class Plugin : BaseUnityPlugin { public const string GUID = "com.puhpuhburr.keyring"; public const string NAME = "Keyring"; public const string VERSION = "0.2.1"; internal static ManualLogSource Log; internal static ICredentialStore Store; internal static ConfigEntry<bool> RememberPasswords; internal static ConfigEntry<bool> AutoLogin; internal static ConfigEntry<bool> ForgetRejected; internal static ConfigEntry<bool> ShowMessages; internal static ConfigEntry<bool> AllowWineStore; internal static ConfigEntry<bool> ShowContinue; private Harmony _harmony; private void Awake() { //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; RememberPasswords = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - General", "RememberPasswords", true, "Save the password after a server accepts it. Passwords go to the operating system's credential store, never to this config file."); AutoLogin = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - General", "AutoLogin", true, "When a server asks for a password and one is saved for it, send it instead of showing the password dialog."); ForgetRejected = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - General", "ForgetRejected", true, "If a saved password is rejected by the server (it was changed), delete it so the dialog is shown on the next attempt."); ShowMessages = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - General", "ShowMessages", true, "Show a short on-screen message after spawning when a password was saved or used."); AllowWineStore = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Advanced", "AllowWineStore", false, "Under Wine/Proton the Windows credential API is emulated and stores secrets with weak obfuscation. Keyring refuses to save there unless this is true. Has no effect on real Windows."); ShowContinue = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Continue", "ShowContinueButton", true, "Add a Continue entry at the top of the main menu that rejoins the last world or server you played on. Hover it to see what it would join."); LastSession.Bind(((BaseUnityPlugin)this).Config); Store = CredentialStoreFactory.Create(); Log.LogInfo((object)("Credential store: " + Store.Description)); _harmony = new Harmony("com.puhpuhburr.keyring"); _harmony.PatchAll(typeof(Plugin).Assembly); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal static class ServerIdentity { public static string CurrentKey() { return ZNet.GetServerString(true); } public static string CurrentDisplayName() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) try { FejdStartup instance = FejdStartup.instance; if ((Object)(object)instance != (Object)null && instance.HasServerToJoin()) { ServerJoinData serverToJoin = instance.GetServerToJoin(); string text = default(string); if (((ServerJoinData)(ref serverToJoin)).IsValid && MultiBackendMatchmaking.TryGetServerName(serverToJoin, ref text) && !string.IsNullOrEmpty(text)) { return text; } } } catch { } return ZNet.GetServerString(false); } } internal sealed class WindowsCredentialStore : ICredentialStore { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct CREDENTIAL { public uint Flags; public uint Type; public IntPtr TargetName; public IntPtr Comment; public FILETIME LastWritten; public uint CredentialBlobSize; public IntPtr CredentialBlob; public uint Persist; public uint AttributeCount; public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName; } private const string TargetPrefix = "Valheim/Keyring/"; private const string UserName = "valheim"; private const uint CRED_TYPE_GENERIC = 1u; private const uint CRED_PERSIST_LOCAL_MACHINE = 2u; private const uint CRED_MAX_CREDENTIAL_BLOB_SIZE = 2560u; private const int ERROR_NOT_FOUND = 1168; public string Description => "Windows Credential Manager (DPAPI, current user)"; public bool IsAvailable => true; public bool SelfTest(out string error) { try { if (!CredRead("Valheim/Keyring/\u0001selftest", 1u, 0u, out var credential)) { int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error != 1168) { error = new Win32Exception(lastWin32Error).Message; return false; } } else { CredFree(credential); } error = null; return true; } catch (Exception ex) { error = ex.GetType().Name + ": " + ex.Message; return false; } } public bool TryRead(string key, out string secret) { secret = null; IntPtr credential = IntPtr.Zero; try { if (!CredRead("Valheim/Keyring/" + key, 1u, 0u, out credential)) { int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error != 1168) { Plugin.Log.LogWarning((object)("CredRead failed: " + new Win32Exception(lastWin32Error).Message)); } return false; } CREDENTIAL cREDENTIAL = Marshal.PtrToStructure<CREDENTIAL>(credential); if (cREDENTIAL.CredentialBlob == IntPtr.Zero || cREDENTIAL.CredentialBlobSize == 0) { return false; } byte[] array = new byte[cREDENTIAL.CredentialBlobSize]; Marshal.Copy(cREDENTIAL.CredentialBlob, array, 0, array.Length); secret = Encoding.Unicode.GetString(array); Array.Clear(array, 0, array.Length); return secret.Length > 0; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Reading credential failed: " + ex.Message)); return false; } finally { if (credential != IntPtr.Zero) { CredFree(credential); } } } public bool Write(string key, string secret, string comment) { byte[] bytes = Encoding.Unicode.GetBytes(secret ?? ""); if (bytes.Length == 0 || (long)bytes.Length > 2560L) { Plugin.Log.LogWarning((object)"Password is empty or too long to store."); return false; } IntPtr intPtr = IntPtr.Zero; IntPtr intPtr2 = IntPtr.Zero; IntPtr intPtr3 = IntPtr.Zero; IntPtr intPtr4 = IntPtr.Zero; try { intPtr = Marshal.StringToCoTaskMemUni("Valheim/Keyring/" + key); intPtr2 = Marshal.StringToCoTaskMemUni("valheim"); intPtr3 = Marshal.StringToCoTaskMemUni(Truncate(comment ?? "", 256)); intPtr4 = Marshal.AllocCoTaskMem(bytes.Length); Marshal.Copy(bytes, 0, intPtr4, bytes.Length); CREDENTIAL credential = new CREDENTIAL { Flags = 0u, Type = 1u, TargetName = intPtr, Comment = intPtr3, CredentialBlobSize = (uint)bytes.Length, CredentialBlob = intPtr4, Persist = 2u, AttributeCount = 0u, Attributes = IntPtr.Zero, TargetAlias = IntPtr.Zero, UserName = intPtr2 }; if (!CredWrite(ref credential, 0u)) { int lastWin32Error = Marshal.GetLastWin32Error(); Plugin.Log.LogWarning((object)("CredWrite failed: " + new Win32Exception(lastWin32Error).Message)); return false; } return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Writing credential failed: " + ex.Message)); return false; } finally { if (intPtr4 != IntPtr.Zero) { Marshal.Copy(new byte[bytes.Length], 0, intPtr4, bytes.Length); Marshal.FreeCoTaskMem(intPtr4); } Array.Clear(bytes, 0, bytes.Length); if (intPtr != IntPtr.Zero) { Marshal.FreeCoTaskMem(intPtr); } if (intPtr2 != IntPtr.Zero) { Marshal.FreeCoTaskMem(intPtr2); } if (intPtr3 != IntPtr.Zero) { Marshal.FreeCoTaskMem(intPtr3); } } } public bool Delete(string key) { try { if (CredDelete("Valheim/Keyring/" + key, 1u, 0u)) { return true; } int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error != 1168) { Plugin.Log.LogWarning((object)("CredDelete failed: " + new Win32Exception(lastWin32Error).Message)); } return false; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Deleting credential failed: " + ex.Message)); return false; } } public List<StoredEntry> List() { List<StoredEntry> list = new List<StoredEntry>(); IntPtr credentials = IntPtr.Zero; try { if (!CredEnumerate("Valheim/Keyring/*", 0u, out var count, out credentials)) { int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error != 1168) { Plugin.Log.LogWarning((object)("CredEnumerate failed: " + new Win32Exception(lastWin32Error).Message)); } return list; } for (int i = 0; i < count; i++) { CREDENTIAL cREDENTIAL = Marshal.PtrToStructure<CREDENTIAL>(Marshal.ReadIntPtr(credentials, i * IntPtr.Size)); if (cREDENTIAL.Type == 1) { string text = Marshal.PtrToStringUni(cREDENTIAL.TargetName) ?? ""; if (text.StartsWith("Valheim/Keyring/", StringComparison.Ordinal)) { long num = ((long)cREDENTIAL.LastWritten.dwHighDateTime << 32) | (uint)cREDENTIAL.LastWritten.dwLowDateTime; list.Add(new StoredEntry { Key = text.Substring("Valheim/Keyring/".Length), Comment = ((cREDENTIAL.Comment == IntPtr.Zero) ? "" : Marshal.PtrToStringUni(cREDENTIAL.Comment)), LastWritten = ((num > 0) ? DateTime.FromFileTimeUtc(num).ToLocalTime() : DateTime.MinValue) }); } } } list.Sort((StoredEntry a, StoredEntry b) => string.Compare(a.Comment, b.Comment, StringComparison.OrdinalIgnoreCase)); return list; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Listing credentials failed: " + ex.Message)); return list; } finally { if (credentials != IntPtr.Zero) { CredFree(credentials); } } } private static string Truncate(string s, int max) { if (s.Length > max) { return s.Substring(0, max); } return s; } [DllImport("advapi32.dll", CharSet = CharSet.Unicode, EntryPoint = "CredReadW", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CredRead(string target, uint type, uint flags, out IntPtr credential); [DllImport("advapi32.dll", CharSet = CharSet.Unicode, EntryPoint = "CredWriteW", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CredWrite(ref CREDENTIAL credential, uint flags); [DllImport("advapi32.dll", CharSet = CharSet.Unicode, EntryPoint = "CredDeleteW", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CredDelete(string target, uint type, uint flags); [DllImport("advapi32.dll", CharSet = CharSet.Unicode, EntryPoint = "CredEnumerateW", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CredEnumerate(string filter, uint flags, out uint count, out IntPtr credentials); [DllImport("advapi32.dll")] private static extern void CredFree(IntPtr buffer); } }