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