using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace DodoSSH.Client.App.Platform;
///
/// The framework constants passes to CoreFoundation and Security.
///
///
///
/// Every field here is a pointer read out of a loaded framework rather than a value this code could
/// write down. The dictionaries these go into are matched by pointer identity, so a CFString built with
/// the same characters is a different key and the lookup misses — see
/// for the indirection that trips people.
///
///
/// Resolved once and cached, and the caching is what makes the failure survivable. Two frameworks
/// and nineteen symbols is a lot of things to be wrong about, and the useful property is that being
/// wrong about any one of them shows up here — as being false — rather than
/// three calls later as a parameter error. A store that reports itself unavailable sends the user back
/// to their passphrase; a store that half works corrupts the moment somebody registers a device.
///
///
/// rather than a static constructor, because a type initialiser that throws
/// poisons the type for the life of the process and turns a missing symbol into a
/// TypeInitializationException at every later call site. The load is done inside a try instead,
/// and its failure is a value.
///
///
[SupportedOSPlatform("macos")]
internal sealed class MacSymbols
{
private static readonly Lazy Cached = new(Load, LazyThreadSafetyMode.ExecutionAndPublication);
private MacSymbols()
{
}
/// Whether every symbol resolved.
///
/// Checked by every caller before any of the pointers are used. It is one check rather than
/// nineteen, which is the only reason the call sites in are
/// readable.
///
/// Computed rather than stored, so that the instance returned when a framework will not load at all
/// — every field left at zero — answers false without that having to be set anywhere. One rule,
/// applied to the only state there is.
///
internal bool Complete => AllResolved();
// CoreFoundation.
internal IntPtr True { get; private init; }
internal IntPtr TypeDictionaryKeyCallBacks { get; private init; }
internal IntPtr TypeDictionaryValueCallBacks { get; private init; }
// Security: item classes and query keys.
internal IntPtr Class { get; private init; }
internal IntPtr ClassKey { get; private init; }
internal IntPtr ReturnRef { get; private init; }
internal IntPtr UseDataProtectionKeychain { get; private init; }
internal IntPtr UseOperationPrompt { get; private init; }
// Security: key attributes.
internal IntPtr AttrKeyType { get; private init; }
internal IntPtr AttrKeySizeInBits { get; private init; }
internal IntPtr AttrTokenId { get; private init; }
internal IntPtr AttrIsPermanent { get; private init; }
internal IntPtr AttrApplicationTag { get; private init; }
internal IntPtr AttrAccessControl { get; private init; }
internal IntPtr PrivateKeyAttrs { get; private init; }
// Security: attribute values.
internal IntPtr KeyTypeEcSecPrimeRandom { get; private init; }
internal IntPtr TokenIdSecureEnclave { get; private init; }
internal IntPtr AccessibleWhenUnlockedThisDeviceOnly { get; private init; }
///
/// kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM.
///
///
/// The one algorithm the Secure Enclave's P-256 keys support for encryption, and the reason this
/// store wraps rather than signs. The long name spells out the whole construction: an ephemeral
/// key agreed against the enclave's public half with cofactor ECDH, run through the X9.63 KDF with
/// SHA-256, used as an AES-GCM key. The ephemeral public key travels in the output, which is why the
/// ciphertext is larger than the 32 bytes going in and why nothing else has to be stored beside it.
///
internal IntPtr EciesAlgorithm { get; private init; }
/// The resolved symbols, loaded once.
internal static MacSymbols Resolve() => Cached.Value;
private static MacSymbols Load()
{
try
{
if (!NativeLibrary.TryLoad(MacSecurity.CoreFoundation, out var cf)
|| !NativeLibrary.TryLoad(MacSecurity.SecurityFramework, out var sec))
{
// Every pointer left at zero, which AllResolved reads as incomplete.
return new MacSymbols();
}
// The two callback tables are structs rather than object pointers, so what is wanted is the
// address of the export itself and not what it holds. Every other symbol here is a CFTypeRef
// global and needs the dereference; these two do not, and mixing them up produces a
// dictionary that does not retain its contents.
var keyCallBacks = NativeLibrary.TryGetExport(cf, "kCFTypeDictionaryKeyCallBacks", out var k)
? k
: IntPtr.Zero;
var valueCallBacks = NativeLibrary.TryGetExport(cf, "kCFTypeDictionaryValueCallBacks", out var v)
? v
: IntPtr.Zero;
return new MacSymbols
{
True = MacSecurity.Constant(cf, "kCFBooleanTrue"),
TypeDictionaryKeyCallBacks = keyCallBacks,
TypeDictionaryValueCallBacks = valueCallBacks,
Class = MacSecurity.Constant(sec, "kSecClass"),
ClassKey = MacSecurity.Constant(sec, "kSecClassKey"),
ReturnRef = MacSecurity.Constant(sec, "kSecReturnRef"),
UseDataProtectionKeychain = MacSecurity.Constant(sec, "kSecUseDataProtectionKeychain"),
UseOperationPrompt = MacSecurity.Constant(sec, "kSecUseOperationPrompt"),
AttrKeyType = MacSecurity.Constant(sec, "kSecAttrKeyType"),
AttrKeySizeInBits = MacSecurity.Constant(sec, "kSecAttrKeySizeInBits"),
AttrTokenId = MacSecurity.Constant(sec, "kSecAttrTokenID"),
AttrIsPermanent = MacSecurity.Constant(sec, "kSecAttrIsPermanent"),
AttrApplicationTag = MacSecurity.Constant(sec, "kSecAttrApplicationTag"),
AttrAccessControl = MacSecurity.Constant(sec, "kSecAttrAccessControl"),
PrivateKeyAttrs = MacSecurity.Constant(sec, "kSecPrivateKeyAttrs"),
KeyTypeEcSecPrimeRandom = MacSecurity.Constant(sec, "kSecAttrKeyTypeECSECPrimeRandom"),
TokenIdSecureEnclave = MacSecurity.Constant(sec, "kSecAttrTokenIDSecureEnclave"),
AccessibleWhenUnlockedThisDeviceOnly =
MacSecurity.Constant(sec, "kSecAttrAccessibleWhenUnlockedThisDeviceOnly"),
EciesAlgorithm = MacSecurity.Constant(
sec,
"kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM"),
};
}
catch (Exception exception) when (exception is DllNotFoundException or BadImageFormatException)
{
return new MacSymbols();
}
}
private bool AllResolved() =>
True != IntPtr.Zero
&& TypeDictionaryKeyCallBacks != IntPtr.Zero
&& TypeDictionaryValueCallBacks != IntPtr.Zero
&& Class != IntPtr.Zero
&& ClassKey != IntPtr.Zero
&& ReturnRef != IntPtr.Zero
&& UseDataProtectionKeychain != IntPtr.Zero
&& UseOperationPrompt != IntPtr.Zero
&& AttrKeyType != IntPtr.Zero
&& AttrKeySizeInBits != IntPtr.Zero
&& AttrTokenId != IntPtr.Zero
&& AttrIsPermanent != IntPtr.Zero
&& AttrApplicationTag != IntPtr.Zero
&& AttrAccessControl != IntPtr.Zero
&& PrivateKeyAttrs != IntPtr.Zero
&& KeyTypeEcSecPrimeRandom != IntPtr.Zero
&& TokenIdSecureEnclave != IntPtr.Zero
&& AccessibleWhenUnlockedThisDeviceOnly != IntPtr.Zero
&& EciesAlgorithm != IntPtr.Zero;
}