230 lines
7.1 KiB
C#
230 lines
7.1 KiB
C#
using System.Security.Cryptography;
|
|
using ZB.MOM.WW.Secrets.Abstractions;
|
|
using ZB.MOM.WW.Secrets.MasterKey;
|
|
|
|
namespace ZB.MOM.WW.Secrets.Tests.MasterKey;
|
|
|
|
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<MasterKeyUnavailableException>(() => 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<MasterKeyUnavailableException>(() => 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<MasterKeyUnavailableException>(() => 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<MasterKeyUnavailableException>(() => 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<EnvironmentMasterKeyProvider>(provider);
|
|
}
|
|
|
|
[Fact]
|
|
public void Factory_File_ReturnsFileProvider()
|
|
{
|
|
IMasterKeyProvider provider = MasterKeyProviderFactory.Create(
|
|
new MasterKeyOptions { Source = MasterKeySource.File, FilePath = "unused" });
|
|
|
|
Assert.IsType<FileMasterKeyProvider>(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");
|
|
}
|