using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace DodoSSH.Client.App.Platform;
///
/// The pieces of CoreFoundation and Security.framework needs.
///
///
///
/// Separated from the store itself because it is a different kind of code with a different kind of
/// review: nothing here makes a decision, and everything here is a translation of a C declaration that
/// is either right or wrong. Mixing the two would mean the security argument in
/// had to be read past two hundred lines of marshalling to find.
///
///
/// Every Create or Copy returns an object this process owns. That is CoreFoundation's Create
/// Rule, and it is the thing here that goes wrong silently: the enclave key handle is small, so a leak
/// shows up as nothing at all until a long-running process has done a few thousand unlocks.
/// exists so ownership is tracked by construction rather than by
/// remembering, and every function below that returns a handle says whether it is owned.
///
///
/// The integer widths are the part worth checking against the headers rather than skimming.
/// CFIndex, CFOptionFlags and CFNumberType are all pointer-width on a 64-bit Mac,
/// not 32-bit, and getting one wrong does not fail cleanly — it shifts every argument after it, so the
/// call receives plausible rubbish and returns a parameter error that names nothing.
///
///
[SupportedOSPlatform("macos")]
internal static partial class MacSecurity
{
internal const string SecurityFramework =
"/System/Library/Frameworks/Security.framework/Security";
internal const string CoreFoundation =
"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
///
/// The access control flags SecAccessControlCreateWithFlags takes.
///
///
/// ulong because the parameter is a CFOptionFlags, which is an unsigned long.
/// Only the two flags that are used are listed; the full set is large, and copying it in would
/// invite somebody to reach for one without reading what it does to the prompt — Biometry
/// alone, for instance, leaves a Mac with no Touch ID unable to unlock at all rather than falling
/// back to the login password.
///
[Flags]
internal enum AccessControlFlags : ulong
{
///
/// Touch ID if the machine has it, the login password if not.
///
///
/// The forgiving one, deliberately. BiometryCurrentSet would additionally invalidate the
/// key whenever a fingerprint is added or removed, which sounds stricter and here buys nothing:
/// this key wraps a device key whose loss already means "ask for the passphrase", so the only
/// effect would be users being sent back to their passphrase by an unrelated Settings change
/// they would never connect to it.
///
UserPresence = 1ul << 0,
/// Required for any key that lives in the Secure Enclave.
PrivateKeyUsage = 1ul << 30,
}
/// The CFNumberType code for a 32-bit int, from CFNumber.h.
internal const long CFNumberIntType = 9;
/// errSecSuccess.
internal const int Success = 0;
/// errSecItemNotFound, which is an answer rather than a failure.
internal const int ItemNotFound = -25300;
// ---- CoreFoundation ------------------------------------------------------------------------------
/// Releases an owned handle.
[LibraryImport(CoreFoundation)]
internal static partial void CFRelease(IntPtr handle);
/// Copies bytes into a new CFData. Owned.
[LibraryImport(CoreFoundation)]
internal static partial IntPtr CFDataCreate(IntPtr allocator, IntPtr bytes, nint length);
[LibraryImport(CoreFoundation)]
internal static partial IntPtr CFDataGetBytePtr(IntPtr data);
[LibraryImport(CoreFoundation)]
internal static partial nint CFDataGetLength(IntPtr data);
/// Boxes a value as a CFNumber. Owned.
[LibraryImport(CoreFoundation)]
internal static partial IntPtr CFNumberCreate(IntPtr allocator, nint theType, IntPtr valuePtr);
/// Builds an immutable dictionary. Owned.
///
///
/// The key and value arrays are passed as raw pointers to memory the caller pins, rather than as
/// managed arrays. Source-generated interop wants an explicit element count for a marshalled array,
/// and supplying one here would mean stating the length twice — once for the marshaller and once as
/// — which is exactly the pair that drifts.
///
///
/// The two callback tables are what make the dictionary retain its keys and values, which is why
/// they are passed rather than left null: with null callbacks the dictionary stores raw pointers and
/// keeps nothing alive, and the resulting use-after-free is intermittent by nature.
///
///
[LibraryImport(CoreFoundation)]
internal static partial IntPtr CFDictionaryCreate(
IntPtr allocator,
IntPtr keys,
IntPtr values,
nint numValues,
IntPtr keyCallBacks,
IntPtr valueCallBacks);
// ---- Security.framework --------------------------------------------------------------------------
/// Builds the access policy a Secure Enclave key is created under. Owned.
[LibraryImport(SecurityFramework)]
internal static partial IntPtr SecAccessControlCreateWithFlags(
IntPtr allocator,
IntPtr protection,
AccessControlFlags flags,
out IntPtr error);
/// Creates a key pair from an attribute dictionary. Owned.
[LibraryImport(SecurityFramework)]
internal static partial IntPtr SecKeyCreateRandomKey(IntPtr parameters, out IntPtr error);
/// The public half of a key. Owned.
///
/// Available even for an enclave key, and that asymmetry is the whole reason this design works: the
/// public half is an ordinary key this process can hold and use, while the private half is a handle
/// to something inside the enclave that never becomes bytes. So sealing is silent and unsealing is
/// the thing the user is asked about.
///
[LibraryImport(SecurityFramework)]
internal static partial IntPtr SecKeyCopyPublicKey(IntPtr key);
/// Encrypts with a public key. Owned.
[LibraryImport(SecurityFramework)]
internal static partial IntPtr SecKeyCreateEncryptedData(
IntPtr key,
IntPtr algorithm,
IntPtr plaintext,
out IntPtr error);
/// Decrypts with a private key, prompting for whatever guards it. Owned.
[LibraryImport(SecurityFramework)]
internal static partial IntPtr SecKeyCreateDecryptedData(
IntPtr key,
IntPtr algorithm,
IntPtr ciphertext,
out IntPtr error);
/// Finds a keychain item. The out handle is owned when the result is .
[LibraryImport(SecurityFramework)]
internal static partial int SecItemCopyMatching(IntPtr query, out IntPtr result);
/// Deletes every keychain item matching the query.
[LibraryImport(SecurityFramework)]
internal static partial int SecItemDelete(IntPtr query);
// ---- The framework constants ---------------------------------------------------------------------
///
/// Reads one of a framework's global CFString constants, or zero if it is not exported.
///
///
///
/// The keys these dictionaries take are not strings this code may spell for itself. They are
/// pointer-comparable constants exported by the framework, and a CFString built here with the same
/// characters is a different object — the lookups would miss and the call would fail with a
/// parameter error naming nothing.
///
///
/// Dereferenced once, because the exported symbol is the variable rather than its value.
/// TryGetExport answers the address of the global; the CFStringRef is what that address
/// holds. Missing the indirection produces a pointer that is stable, plausible and wrong, which is
/// the worst of the three available outcomes.
///
///
/// Zero on a missing symbol rather than an exception, because the caller's answer to every failure
/// is the same one — report the store unavailable and let unlock ask for the passphrase — and a
/// constant that has been renamed by a future macOS should reach that answer rather than a crash.
///
///
internal static IntPtr Constant(IntPtr library, string symbol) =>
NativeLibrary.TryGetExport(library, symbol, out var address)
? Marshal.ReadIntPtr(address)
: IntPtr.Zero;
}
///
/// Releases every CoreFoundation handle put into it, in reverse order, exactly once.
///
///
///
/// The alternative is a try/finally per handle, and the operations here need six or seven at a time — a
/// dictionary holding a nested dictionary holding an access control object holding a CFData tag. Finallys
/// nested that deep stop being read, and a handle released twice is a crash rather than a leak.
///
///
/// returns what it was given, so a handle can be tracked in the same expression that
/// produces it and the call sites read as ordinary code.
///
///
[SupportedOSPlatform("macos")]
internal sealed class CoreFoundationScope : IDisposable
{
private readonly List owned = [];
private bool disposed;
/// Takes ownership of a handle and hands it straight back.
///
/// Zero is ignored rather than rejected. Every CoreFoundation call here answers zero on failure, so
/// accepting it lets a caller track the result in the expression that produces it and check it on
/// the next line, instead of writing the check twice.
///
internal IntPtr Keep(IntPtr handle)
{
if (handle != IntPtr.Zero)
{
owned.Add(handle);
}
return handle;
}
public void Dispose()
{
if (disposed)
{
return;
}
disposed = true;
// Reverse order, so a container is released before the things it retains. CoreFoundation does not
// require it — retain counts make the order irrelevant — but it keeps the lifetimes readable in a
// debugger, where a released container that still lists its contents is a confusing thing to meet.
for (var i = owned.Count - 1; i >= 0; i--)
{
MacSecurity.CFRelease(owned[i]);
}
owned.Clear();
}
}