using System.Security.Cryptography; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.MasterKey; namespace ZB.MOM.WW.Secrets.Tests.MasterKey; [Collection("Process environment")] public class MasterKeyProviderTests { // ---- Environment ---------------------------------------------------------------------- [Fact] public void Environment_ReturnsDecodedKey() { byte[] key = RandomNumberGenerator.GetBytes(32); string envVar = UniqueEnvVarName(); Environment.SetEnvironmentVariable(envVar, Convert.ToBase64String(key)); try { var provider = new EnvironmentMasterKeyProvider(new MasterKeyOptions { EnvVarName = envVar }); Assert.True(key.AsSpan().SequenceEqual(provider.GetMasterKey().Span)); } finally { Environment.SetEnvironmentVariable(envVar, null); } } [Fact] public void Environment_KekId_IsStableAcrossCalls() { byte[] key = RandomNumberGenerator.GetBytes(32); string envVar = UniqueEnvVarName(); Environment.SetEnvironmentVariable(envVar, Convert.ToBase64String(key)); try { var provider = new EnvironmentMasterKeyProvider(new MasterKeyOptions { EnvVarName = envVar }); string first = provider.KekId; string second = provider.KekId; Assert.Equal(first, second); Assert.StartsWith("sha256:", first, StringComparison.Ordinal); } finally { Environment.SetEnvironmentVariable(envVar, null); } } [Fact] public void Environment_ShortKey_Throws() { byte[] shortKey = RandomNumberGenerator.GetBytes(16); string envVar = UniqueEnvVarName(); Environment.SetEnvironmentVariable(envVar, Convert.ToBase64String(shortKey)); try { var provider = new EnvironmentMasterKeyProvider(new MasterKeyOptions { EnvVarName = envVar }); Assert.Throws(() => provider.GetMasterKey()); } finally { Environment.SetEnvironmentVariable(envVar, null); } } [Fact] public void Environment_MissingVariable_Throws() { string envVar = UniqueEnvVarName(); Environment.SetEnvironmentVariable(envVar, null); // ensure absent var provider = new EnvironmentMasterKeyProvider(new MasterKeyOptions { EnvVarName = envVar }); Assert.Throws(() => provider.GetMasterKey()); } // ---- File ----------------------------------------------------------------------------- [Fact] public void File_Raw32Bytes_ReturnsKey() { byte[] key = RandomNumberGenerator.GetBytes(32); string path = Path.GetTempFileName(); File.WriteAllBytes(path, key); try { var provider = new FileMasterKeyProvider(new MasterKeyOptions { FilePath = path }); Assert.True(key.AsSpan().SequenceEqual(provider.GetMasterKey().Span)); } finally { File.Delete(path); } } [Fact] public void File_Base64Text_ReturnsKey() { byte[] key = RandomNumberGenerator.GetBytes(32); string path = Path.GetTempFileName(); File.WriteAllText(path, Convert.ToBase64String(key) + "\n"); try { var provider = new FileMasterKeyProvider(new MasterKeyOptions { FilePath = path }); Assert.True(key.AsSpan().SequenceEqual(provider.GetMasterKey().Span)); } finally { File.Delete(path); } } [Fact] public void File_TenBytes_Throws() { // 10 raw bytes: not 32, and not valid base64 text either → fail closed. string path = Path.GetTempFileName(); File.WriteAllBytes(path, RandomNumberGenerator.GetBytes(10)); try { var provider = new FileMasterKeyProvider(new MasterKeyOptions { FilePath = path }); Assert.Throws(() => provider.GetMasterKey()); } finally { File.Delete(path); } } [Fact] public void File_MissingFile_Throws() { var provider = new FileMasterKeyProvider( new MasterKeyOptions { FilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")) }); Assert.Throws(() => provider.GetMasterKey()); } // ---- KekId derivation ----------------------------------------------------------------- [Fact] public void ExplicitKekId_IsUsedVerbatim() { byte[] key = RandomNumberGenerator.GetBytes(32); string path = Path.GetTempFileName(); File.WriteAllBytes(path, key); try { var provider = new FileMasterKeyProvider( new MasterKeyOptions { FilePath = path, KekId = "prod-kek-2026" }); Assert.Equal("prod-kek-2026", provider.KekId); } finally { File.Delete(path); } } [Fact] public void DerivedKekId_StartsWithSha256() { byte[] key = RandomNumberGenerator.GetBytes(32); string path = Path.GetTempFileName(); File.WriteAllBytes(path, key); try { var provider = new FileMasterKeyProvider(new MasterKeyOptions { FilePath = path }); Assert.StartsWith("sha256:", provider.KekId, StringComparison.Ordinal); } finally { File.Delete(path); } } // ---- Factory -------------------------------------------------------------------------- [Fact] public void Factory_Environment_ReturnsEnvironmentProvider() { IMasterKeyProvider provider = MasterKeyProviderFactory.Create( new MasterKeyOptions { Source = MasterKeySource.Environment }); Assert.IsType(provider); } [Fact] public void Factory_File_ReturnsFileProvider() { IMasterKeyProvider provider = MasterKeyProviderFactory.Create( new MasterKeyOptions { Source = MasterKeySource.File, FilePath = "unused" }); Assert.IsType(provider); } // ---- DPAPI (Windows-only) ------------------------------------------------------------- [SkippableFact] public void Dpapi_RoundTripsOnWindows() { Skip.IfNot(OperatingSystem.IsWindows(), "DPAPI is only supported on Windows."); byte[] key = RandomNumberGenerator.GetBytes(32); #pragma warning disable CA1416 // Whole body is guarded by Skip.IfNot(IsWindows) above. byte[] blob = ProtectedData.Protect(key, optionalEntropy: null, DataProtectionScope.CurrentUser); string path = Path.GetTempFileName(); File.WriteAllBytes(path, blob); try { var provider = new DpapiMasterKeyProvider(new MasterKeyOptions { FilePath = path }); Assert.True(key.AsSpan().SequenceEqual(provider.GetMasterKey().Span)); } finally { File.Delete(path); } #pragma warning restore CA1416 } private static string UniqueEnvVarName() => "ZB_SECRETS_TEST_" + Guid.NewGuid().ToString("N"); }