633 lines
23 KiB
C#
633 lines
23 KiB
C#
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 Guid ProviderLogoFieldType = new("2D837775-F6CD-464E-A745-482FD0B47493");
|
|
|
|
private static readonly string[] ExpectedLabels =
|
|
[
|
|
"La Salle · Acceso SGU",
|
|
"Acceso institucional SGU",
|
|
"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 "online-success" or "offline-fallback"))
|
|
{
|
|
Console.Error.WriteLine("Usage: SGU.CredentialProvider.SmokeProbe.exe [enumeration|direct-broker|online-rejection|online-success|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 = [];
|
|
bool providerLogoPresent = false;
|
|
bool userTilePresent = false;
|
|
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);
|
|
providerLogoPresent |= descriptor.FieldType == FieldType.TileImage &&
|
|
descriptor.FieldTypeGuid == ProviderLogoFieldType;
|
|
userTilePresent |= descriptor.FieldType == FieldType.TileImage &&
|
|
descriptor.FieldTypeGuid == Guid.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 &&
|
|
providerLogoPresent &&
|
|
!userTilePresent &&
|
|
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,
|
|
providerLogoPresent,
|
|
userTilePresent,
|
|
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 = mode == "online-success"
|
|
? Environment.GetEnvironmentVariable("SGU_TEST_USERNAME")
|
|
?? throw new InvalidOperationException("SGU_TEST_USERNAME is required for online-success.")
|
|
: "DO000000";
|
|
string password = mode == "online-success"
|
|
? Environment.GetEnvironmentVariable("SGU_TEST_PASSWORD")
|
|
?? throw new InvalidOperationException("SGU_TEST_PASSWORD is required for online-success.")
|
|
: 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 rejectionPassed = response == (int)SerializationResponse.NoCredentialNotFinished &&
|
|
serialization.SerializationData == IntPtr.Zero &&
|
|
statusIcon == (int)StatusIcon.Error &&
|
|
statusText == "Credenciales institucionales inválidas.";
|
|
Console.WriteLine(JsonSerializer.Serialize(new
|
|
{
|
|
passed = rejectionPassed,
|
|
mode,
|
|
response = (SerializationResponse)response,
|
|
statusIcon = (StatusIcon)statusIcon,
|
|
statusText,
|
|
credentialReturned = serialization.SerializationData != IntPtr.Zero
|
|
}));
|
|
return rejectionPassed ? 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 serializedCredentialPassed = response == (int)SerializationResponse.ReturnCredentialFinished &&
|
|
serialization.SerializationData != IntPtr.Zero &&
|
|
serialization.SerializationSize > 0 &&
|
|
string.Equals(packedDomain, "LCI", StringComparison.Ordinal) &&
|
|
string.Equals(packedUserName, userName, StringComparison.Ordinal) &&
|
|
passwordPreserved;
|
|
bool passed = mode == "online-success"
|
|
? serializedCredentialPassed && statusIcon == (int)StatusIcon.None && string.IsNullOrEmpty(statusText)
|
|
: serializedCredentialPassed &&
|
|
statusIcon == (int)StatusIcon.Warning &&
|
|
statusText == "Servicio institucional no disponible; Windows validará la última contraseña de dominio registrada.";
|
|
Console.WriteLine(JsonSerializer.Serialize(new
|
|
{
|
|
passed,
|
|
mode,
|
|
response = (SerializationResponse)response,
|
|
statusIcon = (StatusIcon)statusIcon,
|
|
statusText,
|
|
packedDomain,
|
|
packedUserName,
|
|
passwordPreserved,
|
|
serializationSize = serialization.SerializationSize
|
|
}));
|
|
return passed ? 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);
|
|
}
|