Add SGU credential provider and authentication broker
This commit is contained in:
@@ -0,0 +1,610 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SGU.CredentialProvider.SmokeProbe;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static readonly Guid ProviderClassId = new("D789CFD8-5AD4-489F-9B83-7EB5D9D09335");
|
||||
|
||||
private static readonly string[] ExpectedLabels =
|
||||
[
|
||||
"Acceso institucional SGU",
|
||||
"Usa tu clave institucional (DO, AL o AD + 6 dígitos) y contraseña.",
|
||||
"Clave institucional",
|
||||
"Contraseña",
|
||||
"Iniciar sesión"
|
||||
];
|
||||
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
string mode = args.Length == 0 ? "enumeration" : args.Single();
|
||||
if (mode is not ("enumeration" or "direct-broker" or "online-rejection" or "offline-fallback"))
|
||||
{
|
||||
Console.Error.WriteLine("Usage: SGU.CredentialProvider.SmokeProbe.exe [enumeration|direct-broker|online-rejection|offline-fallback]");
|
||||
return 64;
|
||||
}
|
||||
|
||||
if (mode == "direct-broker")
|
||||
{
|
||||
return RunDirectBrokerProbe();
|
||||
}
|
||||
|
||||
object? instance = null;
|
||||
IntPtr credential = IntPtr.Zero;
|
||||
NativeEmptyUserArray? users = null;
|
||||
try
|
||||
{
|
||||
Type providerType = Type.GetTypeFromCLSID(ProviderClassId, throwOnError: true)
|
||||
?? throw new InvalidOperationException("The SGU Credential Provider CLSID is not registered.");
|
||||
instance = Activator.CreateInstance(providerType)
|
||||
?? throw new InvalidOperationException("COM activation returned no provider instance.");
|
||||
ICredentialProvider provider = (ICredentialProvider)instance;
|
||||
ICredentialProviderSetUserArray setUserArray = (ICredentialProviderSetUserArray)instance;
|
||||
users = new NativeEmptyUserArray();
|
||||
|
||||
ThrowIfFailed(provider.SetUsageScenario(UsageScenario.Logon, 0), "SetUsageScenario");
|
||||
ThrowIfFailed(setUserArray.SetUserArray(users.Pointer), "SetUserArray");
|
||||
ThrowIfFailed(provider.GetFieldDescriptorCount(out uint fieldCount), "GetFieldDescriptorCount");
|
||||
|
||||
List<string> labels = [];
|
||||
for (uint index = 0; index < fieldCount; index++)
|
||||
{
|
||||
ThrowIfFailed(provider.GetFieldDescriptorAt(index, out IntPtr descriptorPointer), "GetFieldDescriptorAt");
|
||||
if (descriptorPointer == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException($"Field descriptor {index} was null.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
FieldDescriptor descriptor = Marshal.PtrToStructure<FieldDescriptor>(descriptorPointer);
|
||||
labels.Add(descriptor.Label ?? string.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.DestroyStructure<FieldDescriptor>(descriptorPointer);
|
||||
Marshal.FreeCoTaskMem(descriptorPointer);
|
||||
}
|
||||
}
|
||||
|
||||
ThrowIfFailed(provider.GetCredentialCount(out uint credentialCount, out uint defaultIndex, out int autoLogon), "GetCredentialCount");
|
||||
if (credentialCount > 0)
|
||||
{
|
||||
ThrowIfFailed(provider.GetCredentialAt(0, out credential), "GetCredentialAt");
|
||||
}
|
||||
|
||||
bool passed = fieldCount == ExpectedLabels.Length &&
|
||||
credentialCount == 1 &&
|
||||
credential != IntPtr.Zero &&
|
||||
labels.SequenceEqual(ExpectedLabels, StringComparer.Ordinal);
|
||||
|
||||
if (mode != "enumeration" && passed)
|
||||
{
|
||||
return RunSerializationProbe(mode, credential, labels);
|
||||
}
|
||||
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
passed,
|
||||
mode,
|
||||
providerClassId = ProviderClassId,
|
||||
usageScenario = "Logon",
|
||||
fieldCount,
|
||||
labels,
|
||||
credentialCount,
|
||||
defaultIndex,
|
||||
autoLogon = autoLogon != 0
|
||||
}));
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex);
|
||||
return 2;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (credential != IntPtr.Zero)
|
||||
{
|
||||
Marshal.Release(credential);
|
||||
}
|
||||
if (instance is not null && Marshal.IsComObject(instance))
|
||||
{
|
||||
Marshal.FinalReleaseComObject(instance);
|
||||
}
|
||||
users?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowIfFailed(int hresult, string operation)
|
||||
{
|
||||
if (hresult < 0)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(hresult);
|
||||
throw new COMException($"{operation} failed.", hresult);
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunSerializationProbe(string mode, IntPtr credential, IReadOnlyList<string> labels)
|
||||
{
|
||||
string[] labelArray = labels.ToArray();
|
||||
uint userNameFieldId = checked((uint)Array.IndexOf(labelArray, "Clave institucional"));
|
||||
uint passwordFieldId = checked((uint)Array.IndexOf(labelArray, "Contraseña"));
|
||||
string userName = "DO000000";
|
||||
string password = mode == "offline-fallback"
|
||||
? $"Probe-{Guid.NewGuid():N}-áΩ"
|
||||
: $"Probe-{Guid.NewGuid():N}";
|
||||
|
||||
IntPtr vtable = Marshal.ReadIntPtr(credential);
|
||||
SetStringValueDelegate setStringValue = Marshal.GetDelegateForFunctionPointer<SetStringValueDelegate>(
|
||||
Marshal.ReadIntPtr(vtable, 14 * IntPtr.Size));
|
||||
GetSerializationDelegate getSerialization = Marshal.GetDelegateForFunctionPointer<GetSerializationDelegate>(
|
||||
Marshal.ReadIntPtr(vtable, 18 * IntPtr.Size));
|
||||
|
||||
SetCredentialString(setStringValue, credential, userNameFieldId, userName, "SetStringValue(username)");
|
||||
SetCredentialString(setStringValue, credential, passwordFieldId, password, "SetStringValue(password)");
|
||||
|
||||
CredentialSerialization serialization = default;
|
||||
IntPtr statusTextPointer = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
ThrowIfFailed(
|
||||
getSerialization(credential, out int response, out serialization, out statusTextPointer, out int statusIcon),
|
||||
"GetSerialization");
|
||||
string statusText = statusTextPointer == IntPtr.Zero
|
||||
? string.Empty
|
||||
: Marshal.PtrToStringUni(statusTextPointer) ?? string.Empty;
|
||||
|
||||
if (mode == "online-rejection")
|
||||
{
|
||||
bool passed = response == (int)SerializationResponse.NoCredentialNotFinished &&
|
||||
serialization.SerializationData == IntPtr.Zero &&
|
||||
statusIcon == (int)StatusIcon.Error &&
|
||||
statusText == "Credenciales institucionales inválidas.";
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
passed,
|
||||
mode,
|
||||
response = (SerializationResponse)response,
|
||||
statusIcon = (StatusIcon)statusIcon,
|
||||
statusText,
|
||||
credentialReturned = serialization.SerializationData != IntPtr.Zero
|
||||
}));
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
|
||||
KerberosInteractiveUnlockLogon logon = serialization.SerializationData == IntPtr.Zero
|
||||
? default
|
||||
: Marshal.PtrToStructure<KerberosInteractiveUnlockLogon>(serialization.SerializationData);
|
||||
string packedDomain = ReadPackedString(serialization.SerializationData, logon.LogonDomainName);
|
||||
string packedUserName = ReadPackedString(serialization.SerializationData, logon.Username);
|
||||
string packedPassword = ReadPackedString(serialization.SerializationData, logon.Password);
|
||||
bool passwordPreserved = string.Equals(packedPassword, password, StringComparison.Ordinal);
|
||||
packedPassword = string.Empty;
|
||||
|
||||
bool fallbackPassed = response == (int)SerializationResponse.ReturnCredentialFinished &&
|
||||
serialization.SerializationData != IntPtr.Zero &&
|
||||
serialization.SerializationSize > 0 &&
|
||||
statusIcon == (int)StatusIcon.Warning &&
|
||||
statusText == "Servicio institucional no disponible; Windows validará la última contraseña de dominio registrada." &&
|
||||
string.Equals(packedDomain, "LCI", StringComparison.Ordinal) &&
|
||||
string.Equals(packedUserName, userName, StringComparison.Ordinal) &&
|
||||
passwordPreserved;
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
passed = fallbackPassed,
|
||||
mode,
|
||||
response = (SerializationResponse)response,
|
||||
statusIcon = (StatusIcon)statusIcon,
|
||||
statusText,
|
||||
packedDomain,
|
||||
packedUserName,
|
||||
passwordPreserved,
|
||||
serializationSize = serialization.SerializationSize
|
||||
}));
|
||||
return fallbackPassed ? 0 : 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
password = string.Empty;
|
||||
if (statusTextPointer != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeCoTaskMem(statusTextPointer);
|
||||
}
|
||||
if (serialization.SerializationData != IntPtr.Zero)
|
||||
{
|
||||
byte[] zeroes = new byte[serialization.SerializationSize];
|
||||
Marshal.Copy(zeroes, 0, serialization.SerializationData, zeroes.Length);
|
||||
Marshal.FreeCoTaskMem(serialization.SerializationData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunDirectBrokerProbe()
|
||||
{
|
||||
string settingsPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"SGU",
|
||||
"CredentialProvider",
|
||||
"settings.json");
|
||||
ProbeSettings settings = JsonSerializer.Deserialize<ProbeSettings>(
|
||||
File.ReadAllText(settingsPath),
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||
?? throw new InvalidOperationException("Provider settings could not be read.");
|
||||
|
||||
using X509Store store = new(StoreName.My, StoreLocation.LocalMachine);
|
||||
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
|
||||
using X509Certificate2 certificate = store.Certificates
|
||||
.Find(X509FindType.FindByThumbprint, settings.ClientCertificateThumbprint, validOnly: true)
|
||||
.OfType<X509Certificate2>()
|
||||
.First(item => item.HasPrivateKey);
|
||||
|
||||
string expectedThumbprint = NormalizeThumbprint(settings.ServerCertificateThumbprint);
|
||||
using HttpClientHandler handler = new()
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
CheckCertificateRevocationList = true,
|
||||
ClientCertificateOptions = ClientCertificateOption.Manual,
|
||||
MaxConnectionsPerServer = 2,
|
||||
MaxResponseHeadersLength = 32,
|
||||
UseCookies = false,
|
||||
UseDefaultCredentials = false,
|
||||
UseProxy = false,
|
||||
ServerCertificateCustomValidationCallback = (_, serverCertificate, _, policyErrors) =>
|
||||
policyErrors == SslPolicyErrors.None &&
|
||||
serverCertificate is not null &&
|
||||
string.Equals(
|
||||
NormalizeThumbprint(serverCertificate.GetCertHashString()),
|
||||
expectedThumbprint,
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
handler.ClientCertificates.Add(certificate);
|
||||
using HttpClient client = new(handler)
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds),
|
||||
DefaultRequestVersion = HttpVersion.Version11,
|
||||
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact
|
||||
};
|
||||
|
||||
string password = $"Probe-{Guid.NewGuid():N}";
|
||||
try
|
||||
{
|
||||
string json = JsonSerializer.Serialize(new { clave = "DO000000", password });
|
||||
using StringContent content = new(json, Encoding.UTF8, "application/json");
|
||||
using HttpResponseMessage response = client.PostAsync(settings.BrokerEndpoint, content).GetAwaiter().GetResult();
|
||||
string responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
string? responseCode = null;
|
||||
try
|
||||
{
|
||||
responseCode = JsonDocument.Parse(responseBody).RootElement.GetProperty("code").GetString();
|
||||
}
|
||||
catch (Exception exception) when (exception is JsonException or InvalidOperationException or KeyNotFoundException)
|
||||
{
|
||||
}
|
||||
|
||||
bool passed = response.StatusCode is HttpStatusCode.BadRequest or HttpStatusCode.Unauthorized;
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
passed,
|
||||
mode = "direct-broker",
|
||||
statusCode = (int)response.StatusCode,
|
||||
responseCode
|
||||
}));
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
passed = false,
|
||||
mode = "direct-broker",
|
||||
exception = exception.GetType().FullName,
|
||||
innerException = exception.InnerException?.GetType().FullName,
|
||||
hresult = exception.HResult
|
||||
}));
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
password = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeThumbprint(string value) =>
|
||||
value.Replace(" ", string.Empty, StringComparison.Ordinal).ToUpperInvariant();
|
||||
|
||||
private static void SetCredentialString(
|
||||
SetStringValueDelegate setStringValue,
|
||||
IntPtr credential,
|
||||
uint fieldId,
|
||||
string value,
|
||||
string operation)
|
||||
{
|
||||
IntPtr valuePointer = Marshal.StringToCoTaskMemUni(value);
|
||||
try
|
||||
{
|
||||
ThrowIfFailed(setStringValue(credential, fieldId, valuePointer), operation);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ZeroFreeCoTaskMemUnicode(valuePointer);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadPackedString(IntPtr buffer, PackedUnicodeString value)
|
||||
{
|
||||
if (buffer == IntPtr.Zero || value.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return Marshal.PtrToStringUni(
|
||||
IntPtr.Add(buffer, checked((int)value.Buffer.ToInt64())),
|
||||
value.Length / sizeof(char)) ?? string.Empty;
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate int SetStringValueDelegate(IntPtr instance, uint fieldId, IntPtr value);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate int GetSerializationDelegate(
|
||||
IntPtr instance,
|
||||
out int response,
|
||||
out CredentialSerialization serialization,
|
||||
out IntPtr statusText,
|
||||
out int statusIcon);
|
||||
}
|
||||
|
||||
internal sealed class ProbeSettings
|
||||
{
|
||||
public Uri BrokerEndpoint { get; init; } = null!;
|
||||
public int TimeoutSeconds { get; init; }
|
||||
public string ClientCertificateThumbprint { get; init; } = string.Empty;
|
||||
public string ServerCertificateThumbprint { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
internal enum SerializationResponse
|
||||
{
|
||||
NoCredentialNotFinished = 0,
|
||||
NoCredentialFinished = 1,
|
||||
ReturnCredentialFinished = 2,
|
||||
ReturnNoCredentialFinished = 3
|
||||
}
|
||||
|
||||
internal enum StatusIcon
|
||||
{
|
||||
None = 0,
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
Success = 3
|
||||
}
|
||||
|
||||
internal enum UsageScenario
|
||||
{
|
||||
Invalid = 0,
|
||||
Logon = 1
|
||||
}
|
||||
|
||||
internal enum FieldType
|
||||
{
|
||||
Invalid = 0,
|
||||
LargeText,
|
||||
SmallText,
|
||||
CommandLink,
|
||||
EditText,
|
||||
PasswordText,
|
||||
TileImage,
|
||||
CheckBox,
|
||||
ComboBox,
|
||||
SubmitButton
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Unicode)]
|
||||
internal struct FieldDescriptor
|
||||
{
|
||||
public uint FieldId;
|
||||
public FieldType FieldType;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string? Label;
|
||||
|
||||
public Guid FieldTypeGuid;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
internal struct CredentialSerialization
|
||||
{
|
||||
public uint AuthenticationPackage;
|
||||
public Guid ProviderClassGuid;
|
||||
public uint SerializationSize;
|
||||
public IntPtr SerializationData;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct PackedUnicodeString
|
||||
{
|
||||
public ushort Length;
|
||||
public ushort MaxLength;
|
||||
public IntPtr Buffer;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct KerberosInteractiveUnlockLogon
|
||||
{
|
||||
public int SubmitType;
|
||||
public PackedUnicodeString LogonDomainName;
|
||||
public PackedUnicodeString Username;
|
||||
public PackedUnicodeString Password;
|
||||
public long LoginId;
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("D27C3481-5A1C-45B2-8AAA-C20EBBE8229E")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface ICredentialProvider
|
||||
{
|
||||
[PreserveSig]
|
||||
int SetUsageScenario(UsageScenario usageScenario, uint flags);
|
||||
|
||||
[PreserveSig]
|
||||
int SetSerialization(IntPtr serialization);
|
||||
|
||||
[PreserveSig]
|
||||
int Advise(IntPtr events, IntPtr adviseContext);
|
||||
|
||||
[PreserveSig]
|
||||
int UnAdvise();
|
||||
|
||||
[PreserveSig]
|
||||
int GetFieldDescriptorCount(out uint count);
|
||||
|
||||
[PreserveSig]
|
||||
int GetFieldDescriptorAt(uint index, out IntPtr descriptor);
|
||||
|
||||
[PreserveSig]
|
||||
int GetCredentialCount(out uint count, out uint defaultIndex, out int autoLogonWithDefault);
|
||||
|
||||
[PreserveSig]
|
||||
int GetCredentialAt(
|
||||
uint index,
|
||||
out IntPtr credential);
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("095C1484-1C0C-4388-9C6D-500E61BF84BD")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface ICredentialProviderSetUserArray
|
||||
{
|
||||
[PreserveSig]
|
||||
int SetUserArray(IntPtr users);
|
||||
}
|
||||
|
||||
internal sealed class NativeEmptyUserArray : IDisposable
|
||||
{
|
||||
private static readonly Guid UserArrayInterfaceId = new("90C119AE-0F18-4520-A1F1-114366A40FE8");
|
||||
private static readonly Guid UnknownInterfaceId = new("00000000-0000-0000-C000-000000000046");
|
||||
|
||||
private readonly QueryInterfaceDelegate queryInterface;
|
||||
private readonly AddRefDelegate addRef;
|
||||
private readonly ReleaseDelegate release;
|
||||
private readonly SetProviderFilterDelegate setProviderFilter;
|
||||
private readonly GetAccountOptionsDelegate getAccountOptions;
|
||||
private readonly GetCountDelegate getCount;
|
||||
private readonly GetAtDelegate getAt;
|
||||
private IntPtr instance;
|
||||
private IntPtr vtable;
|
||||
private int referenceCount = 1;
|
||||
|
||||
public NativeEmptyUserArray()
|
||||
{
|
||||
queryInterface = QueryInterface;
|
||||
addRef = AddRef;
|
||||
release = Release;
|
||||
setProviderFilter = SetProviderFilter;
|
||||
getAccountOptions = GetAccountOptions;
|
||||
getCount = GetCount;
|
||||
getAt = GetAt;
|
||||
|
||||
vtable = Marshal.AllocHGlobal(IntPtr.Size * 7);
|
||||
Marshal.WriteIntPtr(vtable, IntPtr.Size * 0, Marshal.GetFunctionPointerForDelegate(queryInterface));
|
||||
Marshal.WriteIntPtr(vtable, IntPtr.Size * 1, Marshal.GetFunctionPointerForDelegate(addRef));
|
||||
Marshal.WriteIntPtr(vtable, IntPtr.Size * 2, Marshal.GetFunctionPointerForDelegate(release));
|
||||
Marshal.WriteIntPtr(vtable, IntPtr.Size * 3, Marshal.GetFunctionPointerForDelegate(setProviderFilter));
|
||||
Marshal.WriteIntPtr(vtable, IntPtr.Size * 4, Marshal.GetFunctionPointerForDelegate(getAccountOptions));
|
||||
Marshal.WriteIntPtr(vtable, IntPtr.Size * 5, Marshal.GetFunctionPointerForDelegate(getCount));
|
||||
Marshal.WriteIntPtr(vtable, IntPtr.Size * 6, Marshal.GetFunctionPointerForDelegate(getAt));
|
||||
|
||||
instance = Marshal.AllocHGlobal(IntPtr.Size);
|
||||
Marshal.WriteIntPtr(instance, vtable);
|
||||
}
|
||||
|
||||
public IntPtr Pointer => instance != IntPtr.Zero
|
||||
? instance
|
||||
: throw new ObjectDisposedException(nameof(NativeEmptyUserArray));
|
||||
|
||||
private int QueryInterface(IntPtr self, ref Guid interfaceId, out IntPtr result)
|
||||
{
|
||||
if (interfaceId == UnknownInterfaceId || interfaceId == UserArrayInterfaceId)
|
||||
{
|
||||
result = self;
|
||||
AddRef(self);
|
||||
return 0;
|
||||
}
|
||||
|
||||
result = IntPtr.Zero;
|
||||
return unchecked((int)0x80004002);
|
||||
}
|
||||
|
||||
private uint AddRef(IntPtr self) => unchecked((uint)Interlocked.Increment(ref referenceCount));
|
||||
|
||||
private uint Release(IntPtr self) => unchecked((uint)Math.Max(0, Interlocked.Decrement(ref referenceCount)));
|
||||
|
||||
private static int SetProviderFilter(IntPtr self, ref Guid providerToFilterTo) => 0;
|
||||
|
||||
private static int GetAccountOptions(IntPtr self, out uint accountOptions)
|
||||
{
|
||||
accountOptions = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int GetCount(IntPtr self, out uint userCount)
|
||||
{
|
||||
userCount = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int GetAt(IntPtr self, uint userIndex, out IntPtr user)
|
||||
{
|
||||
user = IntPtr.Zero;
|
||||
return unchecked((int)0x80070057);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (instance != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(instance);
|
||||
instance = IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (vtable != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(vtable);
|
||||
vtable = IntPtr.Zero;
|
||||
}
|
||||
|
||||
GC.KeepAlive(queryInterface);
|
||||
GC.KeepAlive(addRef);
|
||||
GC.KeepAlive(release);
|
||||
GC.KeepAlive(setProviderFilter);
|
||||
GC.KeepAlive(getAccountOptions);
|
||||
GC.KeepAlive(getCount);
|
||||
GC.KeepAlive(getAt);
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate int QueryInterfaceDelegate(IntPtr self, ref Guid interfaceId, out IntPtr result);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate uint AddRefDelegate(IntPtr self);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate uint ReleaseDelegate(IntPtr self);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate int SetProviderFilterDelegate(IntPtr self, ref Guid providerToFilterTo);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate int GetAccountOptionsDelegate(IntPtr self, out uint accountOptions);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate int GetCountDelegate(IntPtr self, out uint userCount);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate int GetAtDelegate(IntPtr self, uint userIndex, out IntPtr user);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user