using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Azure.Storage.Blobs; using Bit.Core.Settings; using Bit.SharedWeb.Utilities; using DotNet.Testcontainers.Builders; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NSubstitute; namespace Bit.SharedWeb.Test.DataProtectionServicesTests; public class DataProtectionServicesTests { // Created using: // using var rsa = RSA.Create(2048); // var now = DateTimeOffset.UtcNow; // var certificate = new CertificateRequest("CN=Dataprotected test certificate", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) // .CreateSelfSigned(now, now.AddDays(365)); // var data = Convert.ToBase64String(certificate.Export(X509ContentType.Pfx, "Alongside-Unworthy-Query3-Cozy")); private static readonly byte[] FakeInitialCert = Convert.FromBase64String(@" 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 PZBRQ4YxBFDFaGycVn8CAgfQ"); /// /// Grabbed from the keys.xml blob during the same test run as the protected data in /// private const string KeysData = """2026-04-29T15:06:08.62851Z2026-04-29T15:06:08.549389Z2026-07-28T15:06:08.549389ZMIIC0zCCAbugAwIBAgIJAO8fwy6R8ufxMA0GCSqGSIb3DQEBCwUAMCkxJzAlBgNVBAMTHkRhdGFwcm90ZWN0ZWQgdGVzdCBjZXJ0aWZpY2F0ZTAeFw0yNjA0MjkxNDM1NDVaFw0yNzA0MjkxNDM1NDVaMCkxJzAlBgNVBAMTHkRhdGFwcm90ZWN0ZWQgdGVzdCBjZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHISb+ZGeieAG5Vq0oj9IEfYupBZX082uox1RG/J32YjIDIapbjmzNmbNDlefmRSJG8jM4rGpYU/HbB/J0bBNA0miN2okomTs3TOQeHyVbg7d8iomHy8y3El4nSOgkXUKo8Q3tScUNURf6x5DhoKqj1RAHOZ6dp5pYtZmmD4CTpcyODDE2asxgSrX4DuPNoFw0nONWqHydQJKqabUFSHq96FRInwrG0BdFIIxu/XQ61n7Gzd2xh1x2aSI7u3J3tqZOfsw3S2mk1wJ37MENAvYWa8zk/cqOVJ2EPrfXzcde2W9H4JrF56Api6m2yI4WtQWAATRS5KsNwVxuC6QyKfo0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAaEcqhxqk2Fki6j9kOmiDRzikuTijUIkd0J2ZaPxg1HSM8mLuVLzy6XMuqSddb9NcfAl+4bKHO8WQqiiAR8pan2ZiQhRzKFS+qTWGbsDoXhYz+etWqrxAf1y+6LEwb4+vSIt7XiXRMBaU2f0690HZy1tUhlTKOrg0t6dM9QubcrWbrG24Kkj6EcQAo+2Mk9kOUO20oI1eN+GgjbxTLJZBb0uQwhJzZx6Td0CVnNpsBHqVr+f4t6LyCGUftPEmSfl6QRgKlIQ1HNZVh2t1oglgBCgv4DuA1dGuZhDBCUfrx5tVPv9wtr5+B87e4bTL/pYBJBuPuBmcJ13GuI6QtaNkIQ==EAlrlHYiE202m13tlCHvuGpsW0dWnL3EvbTfogF7a3NG28q5s3xgbj7k8bNuuHHTQ9VWlZopXv+DGjWKaPuDGHzs4zYR7YBbdgq3kqoPzayqH7h38IEPYbeJUhbpAZPewWlO5YEa1u7m7ETc6TOFD5iRVYfYZ2P8nMy9DU8fXWDLb3zmYxgRdJYF01YhbBEpPnp572+PthZVfXl8u0Nmf/T72h/QPhaeGIGmdhzb3P6CsISU7+sXRvSRVK5ubsf278elewFVuujJgfC6uGQ7+HfTL9kdDaFXErXNzov9tQqVshYo6JBucOjp4I+q6+uhPBN1QJlssp7+/+ftzDb3Pw==MKn3PmIeUPv27Pp8q8+hF/J3LWV3CG06qcP0gBXzpG5G9uVclLBG47+Wv9nZpX3E/qxoKvJFsDxUkqBYYlycj/QuPiM00po4YhkVBdjZrO5B9Zdjmm2uoTQxXvp7HCsV0VlFpqNVnjWl6VxFz0W7qGHfnhCKkW1FWy1ymf2yWvQHkVMlvRcZ1+s35RFnB/Szr89DCL6hWFmBso54QXz80zweNcoCIeN0YdAUEFhnTO0b9L3wu6k++/+eq1OloiMo/+1YQyfq2W1ZeJ3tRaeg8WEdNQnwfMlbRFNmGemExs54wOeXh0WYXqGvI1MTQd7gvO18+1D9AzQmt2tmN7V8j5f1O1OjPEoG0Djl/QE7USBKPLyzEC6fnbFOfaBscy1zp5YTRcjOHKoTm5E6SQLGjg=="""; /// /// This value was created by grabbing a value from in the debugger. /// private const string SavedProtectedData = "CfDJ8C0HF2QuvTdHiZX9KA_gAlGVSrFxAFk2m-WNRwn3MInU65VHLvlnNTKxpGKXDLSY3mgis0FninNa5hpRGIi5KS215GmhPnm-TvuikZr1N3-ib42KqADgmo1i5PTgZohmRA"; [Fact] public async Task SimpleRoundTrip() { // A very simple show case of how data protection works during a single "run" of our application. await RunTestAsync( testSetup: async (context) => { await context.Certificates.UploadBlobAsync("dataprotection.pfx", new BinaryData(FakeInitialCert)); await context.DataProtection.UploadBlobAsync("keys.xml", new BinaryData(KeysData)); context.Config.AddInMemoryCollection(new Dictionary { { "GlobalSettings:DataProtection:CertificatePassword", "Alongside-Unworthy-Query3-Cozy" }, }); }, test: (context) => { var protectedData = context.Protector.Protect("MyTestData"); Assert.Equal("MyTestData", context.Protector.Unprotect(protectedData)); } ); } [Fact] public async Task UnprotectsSavedData() { // This shows a somewhat realistic example of how our production cert setup works. We have a cert // for encrypting keys and we have a blob that stores all those keys. As long as those keys and that // cert are unchanged you should be able to unprotect data even if it was stored and retrieved during // a new instance of the application. await RunTestAsync( testSetup: async (context) => { await context.Certificates.UploadBlobAsync("dataprotection.pfx", new BinaryData(FakeInitialCert)); await context.DataProtection.UploadBlobAsync("keys.xml", new BinaryData(KeysData)); context.Config.AddInMemoryCollection(new Dictionary { { "GlobalSettings:DataProtection:CertificatePassword", "Alongside-Unworthy-Query3-Cozy" }, }); }, test: (context) => { Assert.Equal("MyTestData", context.Protector.Unprotect(SavedProtectedData)); } ); } [Fact] public async Task UnprotectsSavedData_ButWithDifferentKeysProtectionCert_Fails() { // This shows a scenario where you just decide to rip out the existing certificate we use to encrypt keys // and give it a new one. It shows that you will be unable to unprotect existing data but you will be able to // successfully protect and unprotect new data. This is unacceptable in our case as we have protected data in // the database (and other in flight data) that would brick our application. await RunTestAsync( testSetup: async (context) => { // Upload a totally different certificate for encrypting keys with using var rsa = RSA.Create(2048); var now = DateTimeOffset.UtcNow; var certificate = new CertificateRequest("CN=New Dataprotected test certificate", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) .CreateSelfSigned(now, now.AddDays(365)); await context.Certificates.UploadBlobAsync( "dataprotection.pfx", new BinaryData(certificate.Export(X509ContentType.Pfx, "County-Secluded9-Reshuffle-Womanhood")) ); context.Config.AddInMemoryCollection(new Dictionary { { "GlobalSettings:DataProtection:CertificatePassword", "County-Secluded9-Reshuffle-Womanhood" }, }); // Upload keys that were encrypted with the initial cert await context.DataProtection.UploadBlobAsync("keys.xml", new BinaryData(KeysData)); }, test: (context) => { var newProtectedData = context.Protector.Protect("NewData"); Assert.Equal("NewData", context.Protector.Unprotect(newProtectedData)); var cryptographicException = Assert.Throws(() => context.Protector.Unprotect(SavedProtectedData)); Assert.Equal("Unable to retrieve the decryption key.", cryptographicException.Message); } ); } [Fact] public async Task UnprotectSavedData_NewEncryptionCertificate_OldUnprotectCertificateAvailable_Works() { // This test shows how you are able to work around the issue of wanting to use a new certificate for encrypting // new keys but you don't want to brick the application and still allow the old certificate just for unprotection. await RunTestAsync( testSetup: async (context) => { // Upload a totally different certificate for encrypting keys with using var rsa = RSA.Create(2048); var now = DateTimeOffset.UtcNow; var certificate = new CertificateRequest("CN=New Dataprotected test certificate", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) .CreateSelfSigned(now, now.AddDays(365)); await context.Certificates.UploadBlobAsync( "dataprotection.pfx", new BinaryData(certificate.Export(X509ContentType.Pfx, "County-Secluded9-Reshuffle-Womanhood")) ); await context.Certificates.UploadBlobAsync("newcert.pfx", new BinaryData(FakeInitialCert)); context.Config.AddInMemoryCollection(new Dictionary { { "GlobalSettings:DataProtection:CertificatePassword", "County-Secluded9-Reshuffle-Womanhood" }, { "GlobalSettings:DataProtection:UnprotectCertificates:0:FileName", "newcert.pfx" }, { "GlobalSettings:DataProtection:UnprotectCertificates:0:Password", "Alongside-Unworthy-Query3-Cozy" }, }); // Upload keys that were encrypted with the initial cert await context.DataProtection.UploadBlobAsync("keys.xml", new BinaryData(KeysData)); }, test: (context) => { var unprotected = context.Protector.Unprotect(SavedProtectedData); Assert.Equal("MyTestData", unprotected); } ); } [Fact] public async Task UpgradePath() { // The goal of this test is an upgrade scenario where we want to start using a new certificate // encrypting keys at rest but want the upgrade to cause 0 issues with data protection // we also want to be able to revert the configuration changes in case there are issues. // Setup "existing" azure infrastructure. await using var azurite = new ContainerBuilder("mcr.microsoft.com/azure-storage/azurite") .WithPortBinding(10000, true) .Build(); await azurite.StartAsync(); var azuriteConnectionString = $"DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://{azurite.Hostname}:{azurite.GetMappedPublicPort(10000)}/devstoreaccount1;"; var blobServiceClient = new BlobServiceClient(azuriteConnectionString); var certificates = await blobServiceClient.CreateBlobContainerAsync("certificates"); var dataProtection = await blobServiceClient.CreateBlobContainerAsync("aspnet-dataprotection"); await certificates.Value.UploadBlobAsync("dataprotection.pfx", new BinaryData(FakeInitialCert)); await dataProtection.Value.UploadBlobAsync("keys.xml", new BinaryData(KeysData)); // End existing infrastructure // Step 1: We deploy a new version of our app but with NO config changes using var noNewConfigApp = CreateApp(new Dictionary { { "GlobalSettings:Storage:ConnectionString", azuriteConnectionString }, { "GlobalSettings:DataProtection:CertificatePassword", "Alongside-Unworthy-Query3-Cozy" }, }); var noNewConfigProtector = GetProtector(noNewConfigApp); // App should still be able to unprotect data previously protected Assert.Equal("MyTestData", noNewConfigProtector.Unprotect(SavedProtectedData)); // It should also be able to protect new data that will be consumed later var noNewConfigProtectedData = AssertRoundTrippable(noNewConfigProtector, "NoNewConfig"); // Step 2: We generate a new certificate and upload it to a DIFFERENT blob in azure // Importantly this can be done at any point. using var rsa = RSA.Create(2048); var now = DateTimeOffset.UtcNow; var certificate = new CertificateRequest("CN=New Dataprotected test certificate", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) .CreateSelfSigned(now, now.AddDays(365)); const string NewCertPassword = "Undergrad-Police0-Maturely-Countless"; await certificates.Value.UploadBlobAsync( "mynewcert.pfx", new BinaryData(certificate.Export(X509ContentType.Pfx, NewCertPassword)) ); // Step 3: Start apps that have that new cert as able to Unprotect ONLY, this step // should have 0 behavioral difference between the previous version but it's important // to get all pods with this config so that in the next step all pods don't have to // be updated at the same moment. This makes it so that pods that are slower to get the // config change are able to interoperate with pods that get it quicker. var preparedAppConfig = new Dictionary { { "GlobalSettings:Storage:ConnectionString", azuriteConnectionString }, { "GlobalSettings:DataProtection:CertificatePassword", "Alongside-Unworthy-Query3-Cozy" }, // The new cert gets prepared to be able to unprotect data but it has never been used // to protect data so its existence here is technically not needed. { "GlobalSettings:DataProtection:UnprotectCertificates:0:FileName", "mynewcert.pfx" }, { "GlobalSettings:DataProtection:UnprotectCertificates:0:Password", NewCertPassword }, }; using var preparedApp = CreateApp(preparedAppConfig); var preparedAppProtector = GetProtector(preparedApp); var preparedProtectedData = AssertRoundTrippable(preparedAppProtector, "Prepared"); // This app should be able to unprotect data from before any of these changes // and from the app that contains only the code deploy and no config changes. Assert.Equal("MyTestData", preparedAppProtector.Unprotect(SavedProtectedData)); Assert.Equal("NoNewConfig", preparedAppProtector.Unprotect(noNewConfigProtectedData)); // Step 4: This is where real config changes start to happen, we actually start protecting // data with the new cert but should still be able to unprotect all previous data using var updatedConfigApp = CreateApp(new Dictionary { // Same connection string as always { "GlobalSettings:Storage:ConnectionString", azuriteConnectionString }, // This config key gets set to the new certificate password { "GlobalSettings:DataProtection:CertificatePassword", NewCertPassword }, // This is a totally new config key and it gets set to the blob where the new // cert resides { "GlobalSettings:DataProtection:BlobName", "mynewcert.pfx" }, // The pre-existing certificate gets "demoted" to being a unprotect certificate // this is what makes it so that the data can continue to be decrypted { "GlobalSettings:DataProtection:UnprotectCertificates:0:FileName", "dataprotection.pfx" }, { "GlobalSettings:DataProtection:UnprotectCertificates:0:Password", "Alongside-Unworthy-Query3-Cozy" }, }); var updatedConfigProtector = GetProtector(updatedConfigApp); var updatedConfigData = AssertRoundTrippable(updatedConfigProtector, "UpdatedConfig"); // This should still be able to unprotect all previously protected data Assert.Equal("MyTestData", updatedConfigProtector.Unprotect(SavedProtectedData)); Assert.Equal("NoNewConfig", updatedConfigProtector.Unprotect(noNewConfigProtectedData)); Assert.Equal("Prepared", updatedConfigProtector.Unprotect(preparedProtectedData)); // Problems! If there are problems in step 4 then we should revert the config changes // back to what they were in step 3, it will hopefully still be able to unprotect // that was actually protected with the new cert but we are still in undefined territory // if there are issues as this test intends to show how it will work. using var revertedApp = CreateApp(preparedAppConfig); var revertedAppProtector = GetProtector(revertedApp); AssertRoundTrippable(revertedAppProtector, "Reverted"); Assert.Equal("MyTestData", revertedAppProtector.Unprotect(SavedProtectedData)); Assert.Equal("NoNewConfig", revertedAppProtector.Unprotect(noNewConfigProtectedData)); Assert.Equal("Prepared", revertedAppProtector.Unprotect(preparedProtectedData)); Assert.Equal("UpdatedConfig", revertedAppProtector.Unprotect(updatedConfigData)); } private record TestSetupContext(BlobContainerClient Certificates, BlobContainerClient DataProtection, IConfigurationBuilder Config); private record TestRunContext(IServiceProvider Services, IDataProtector Protector); private IDataProtector GetProtector(IServiceProvider services) { return services.GetRequiredService().CreateProtector("Test"); } private string AssertRoundTrippable(IDataProtector protector, string testString) { var protectedData = protector.Protect(testString); Assert.Equal(testString, protector.Unprotect(protectedData)); return protectedData; } private static async Task RunTestAsync(Func testSetup, Action test) { // Start azurite await using var azurite = new ContainerBuilder("mcr.microsoft.com/azure-storage/azurite") .WithPortBinding(10000, true) .Build(); await azurite.StartAsync(); var azuriteConnectionString = $"DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://{azurite.Hostname}:{azurite.GetMappedPublicPort(10000)}/devstoreaccount1;"; var blobServiceClient = new BlobServiceClient(azuriteConnectionString); var configurationBuilder = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "GlobalSettings:Storage:ConnectionString", azuriteConnectionString }, }); var context = new TestSetupContext( (await blobServiceClient.CreateBlobContainerAsync("certificates")).Value, (await blobServiceClient.CreateBlobContainerAsync("aspnet-dataprotection")).Value, configurationBuilder ); await testSetup(context); using var serviceProvider = CreateApp(context.Config); var runContext = new TestRunContext( serviceProvider, serviceProvider.GetRequiredService().CreateProtector("Test") ); test(runContext); } private static ServiceProvider CreateApp(Dictionary initialData) { var configurationBuilder = new ConfigurationBuilder() .AddInMemoryCollection(initialData); return CreateApp(configurationBuilder); } private static ServiceProvider CreateApp(IConfigurationBuilder configurationBuilder) { var services = new ServiceCollection(); var configuration = configurationBuilder.Build(); var globalSettings = new GlobalSettings(); configuration.GetSection("GlobalSettings").Bind(globalSettings); var environment = Substitute.For(); environment.EnvironmentName.Returns("Production"); services.AddCustomDataProtectionServices( environment, globalSettings ); return services.BuildServiceProvider(); } }