using System; using System.Runtime.InteropServices; using System.Text; namespace MCPForUnity.Editor.Security { /// /// Windows Credential Manager-backed key store (DPAPI-protected by the OS) via advapi32 /// Cred* APIs. Target = "MCPForUnity.AssetGen:<provider>", blob = UTF-8 key bytes. /// internal sealed class WindowsCredentialKeyStore : ISecureKeyStore { private const int CRED_TYPE_GENERIC = 1; private const int CRED_PERSIST_LOCAL_MACHINE = 2; private static string Target(string providerId) => SecureKeyStoreConstants.ServiceName + ":" + providerId; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct CREDENTIAL { public int Flags; public int Type; public string TargetName; public string Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist; public int AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName; } [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CredReadW")] private static extern bool CredRead(string target, int type, int reservedFlag, out IntPtr credentialPtr); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CredWriteW")] private static extern bool CredWrite([In] ref CREDENTIAL credential, int flags); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CredDeleteW")] private static extern bool CredDelete(string target, int type, int flags); [DllImport("advapi32.dll", EntryPoint = "CredFree")] private static extern void CredFree(IntPtr buffer); public bool Has(string providerId) => TryGet(providerId, out _); public bool TryGet(string providerId, out string apiKey) { apiKey = null; if (string.IsNullOrEmpty(providerId)) return false; if (!CredRead(Target(providerId), CRED_TYPE_GENERIC, 0, out IntPtr ptr)) return false; try { var cred = Marshal.PtrToStructure(ptr); if (cred.CredentialBlobSize <= 0 || cred.CredentialBlob == IntPtr.Zero) return false; byte[] bytes = new byte[cred.CredentialBlobSize]; Marshal.Copy(cred.CredentialBlob, bytes, 0, cred.CredentialBlobSize); apiKey = Encoding.UTF8.GetString(bytes); return !string.IsNullOrEmpty(apiKey); } finally { CredFree(ptr); } } public void Set(string providerId, string apiKey) { if (string.IsNullOrEmpty(providerId)) return; if (string.IsNullOrEmpty(apiKey)) { Delete(providerId); return; } byte[] blob = Encoding.UTF8.GetBytes(apiKey); IntPtr blobPtr = Marshal.AllocHGlobal(blob.Length); try { Marshal.Copy(blob, 0, blobPtr, blob.Length); var cred = new CREDENTIAL { Type = CRED_TYPE_GENERIC, TargetName = Target(providerId), CredentialBlobSize = blob.Length, CredentialBlob = blobPtr, Persist = CRED_PERSIST_LOCAL_MACHINE, UserName = providerId, }; if (!CredWrite(ref cred, 0)) { throw new InvalidOperationException( "CredWrite failed (Win32 " + Marshal.GetLastWin32Error() + ")"); } } finally { Marshal.FreeHGlobal(blobPtr); } } public void Delete(string providerId) { if (string.IsNullOrEmpty(providerId)) return; CredDelete(Target(providerId), CRED_TYPE_GENERIC, 0); } } }