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; }