using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.DataProtection; namespace SharepointToolbox.Web.Infrastructure.Auth; /// /// File-backed . Each profile's certificate is /// re-exported password-less, encrypted with ASP.NET Core Data Protection, and /// written to {certsFolder}/{profileId}.bin. The uploaded PFX password is consumed /// at save time and never persisted. /// public class AppOnlyCertStore : IAppOnlyCertStore { private const string Purpose = "SharepointToolbox.AppOnlyCert.v1"; private readonly string _certsFolder; private readonly IDataProtector _protector; public AppOnlyCertStore(string certsFolder, IDataProtectionProvider dataProtection) { _certsFolder = certsFolder; _protector = dataProtection.CreateProtector(Purpose); Directory.CreateDirectory(_certsFolder); } private string PathFor(string profileId) => Path.Combine(_certsFolder, $"{profileId}.bin"); public async Task SaveAsync(string profileId, byte[] pfxBytes, string? password, CancellationToken ct = default) { // Open the uploaded PFX (Exportable so we can re-emit a password-less copy that // the loader can open later without prompting). EphemeralKeySet keeps the key // out of the Windows certificate store during this transient operation. using var cert = X509CertificateLoader.LoadPkcs12( pfxBytes, password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet); if (!cert.HasPrivateKey) throw new InvalidOperationException("The uploaded certificate has no private key. Export the PFX with its key."); var passwordless = cert.Export(X509ContentType.Pkcs12); var protectedBytes = _protector.Protect(passwordless); Directory.CreateDirectory(_certsFolder); var tmp = PathFor(profileId) + ".tmp"; await File.WriteAllBytesAsync(tmp, protectedBytes, ct); File.Move(tmp, PathFor(profileId), overwrite: true); return cert.Thumbprint; } public async Task LoadAsync(string profileId, CancellationToken ct = default) { var path = PathFor(profileId); if (!File.Exists(path)) return null; var protectedBytes = await File.ReadAllBytesAsync(path, ct); var pfx = _protector.Unprotect(protectedBytes); return X509CertificateLoader.LoadPkcs12( pfx, password: null, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet); } public bool Exists(string profileId) => File.Exists(PathFor(profileId)); public void Delete(string profileId) { var path = PathFor(profileId); if (File.Exists(path)) File.Delete(path); } }