Skip to content

Commit 0f8865b

Browse files
committed
Merge remote-tracking branch 'GH_CSharp/GenFreeWin'
2 parents 374cd1f + e326682 commit 0f8865b

93 files changed

Lines changed: 2602 additions & 190 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace GenSecure.Contracts;
2+
3+
/// <summary>
4+
/// Provides the stable identifier of the current principal for secure-store access control.
5+
/// </summary>
6+
public interface ICurrentPrincipalProvider
7+
{
8+
/// <summary>
9+
/// Gets the identifier of the current principal.
10+
/// </summary>
11+
/// <returns>The current principal identifier.</returns>
12+
string GetCurrentPrincipalId();
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace GenSecure.Contracts;
2+
3+
/// <summary>
4+
/// Protects and unprotects the local master key material on the current machine.
5+
/// </summary>
6+
public interface ILocalKeyProtector
7+
{
8+
/// <summary>
9+
/// Protects the specified plaintext bytes for local storage.
10+
/// </summary>
11+
/// <param name="arrPlaintext">The plaintext bytes to protect.</param>
12+
/// <returns>The protected payload.</returns>
13+
byte[] Protect(byte[] arrPlaintext);
14+
15+
/// <summary>
16+
/// Unprotects the specified locally protected bytes.
17+
/// </summary>
18+
/// <param name="arrProtectedData">The protected payload to unprotect.</param>
19+
/// <returns>The original plaintext bytes.</returns>
20+
byte[] Unprotect(byte[] arrProtectedData);
21+
}

GenFreeWin/GenSecure.Contracts/IPersonSecureStore.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,17 @@ public interface IPersonSecureStore
4242
void Delete(string sPersonId, DeleteMode eMode);
4343

4444
/// <summary>
45-
/// Grants access to a Windows SID for an existing person record.
45+
/// Grants access to a principal identifier for an existing person record.
4646
/// </summary>
4747
/// <param name="sPersonId">The stable person identifier.</param>
48-
/// <param name="sWindowsSid">The Windows SID to add.</param>
49-
void GrantAccess(string sPersonId, string sWindowsSid);
48+
/// <param name="sPrincipalId">The principal identifier to add.</param>
49+
void GrantAccess(string sPersonId, string sPrincipalId);
5050

5151
/// <summary>
52-
/// Gets the SHA-256 hashes of Windows SIDs that are permitted to decrypt a person record.
53-
/// The raw SIDs are never persisted; only their hashes are stored.
52+
/// Gets the hashes of principal identifiers that are permitted to decrypt a person record.
53+
/// The raw principal identifiers are never persisted; only their hashes are stored.
5454
/// </summary>
5555
/// <param name="sPersonId">The stable person identifier.</param>
56-
/// <returns>SHA-256 hashes (lowercase hex) of the configured Windows SIDs.</returns>
57-
IReadOnlyCollection<string> GetAllowedWindowsSidHashes(string sPersonId);
56+
/// <returns>Lowercase hex hashes of the configured principal identifiers.</returns>
57+
IReadOnlyCollection<string> GetAllowedPrincipalHashes(string sPersonId);
5858
}

GenFreeWin/GenSecure.Contracts/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ tested with mocks and swapped without recompilation.
1212
## Key Features
1313

1414
- `IPersonSecureStore` — save, load, delete, exists, and access-control operations
15+
- `ICurrentPrincipalProvider` — supplies the current platform principal identifier
16+
- `ILocalKeyProtector` — abstracts local master-key protection for the active platform
1517
- `IMasterKeyBackupService` — PBKDF2 recovery key creation and restore
1618
- `DeleteMode``SoftDelete` (removes files) vs. `SecureDelete` (crypto-deletion, DSGVO Art. 17)
1719
- `StoreMode``Encrypted` (AES-256-GCM, living persons) vs. `Plaintext` (deceased / historical)
1820

1921
## Targets
2022

21-
`net9.0-windows`
23+
`net481`, `net6.0`, `net7.0`, `net8.0`
2224

2325
## Public API
2426

@@ -34,8 +36,8 @@ void Delete(string sPersonId, DeleteMode eMode);
3436
// DeleteMode.SecureDelete — removes only the DEK (crypto-deletion); plaintext fallback: data file
3537
3638
// Access control
37-
void GrantAccess(string sPersonId, string sWindowsSid);
38-
IReadOnlyCollection<string> GetAllowedWindowsSidHashes(string sPersonId);
39+
void GrantAccess(string sPersonId, string sPrincipalId);
40+
IReadOnlyCollection<string> GetAllowedPrincipalHashes(string sPersonId);
3941

4042
// Recovery
4143
void CreateRecoveryKeyBackup(string sPassphrase, bool xOverwrite = false);

GenFreeWin/GenSecure.Core.Tests/GenSecure.Core.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<Import Project="..\GenFreeWin.props" />
33
<PropertyGroup>
4-
<TargetFramework>net9.0-windows</TargetFramework>
4+
<TargetFramework>net9.0</TargetFramework>
55
<ImplicitUsings>disable</ImplicitUsings>
66
<IsPackable>false</IsPackable>
77
<UseWPF>false</UseWPF>

GenFreeWin/GenSecure.Core.Tests/GenealogySecureStoreTests.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,18 @@ public GenealogyStoreScope()
197197
{
198198
RootDirectory = sRootDirectory,
199199
};
200-
BackupService = new MasterKeyBackupService(Options);
201-
Store = new GenealogySecureStore(BackupService, Options);
200+
LocalKeyProtector = new PassThroughLocalKeyProtector();
201+
PrincipalProvider = new FixedPrincipalProvider();
202+
BackupService = new MasterKeyBackupService(Options, LocalKeyProtector);
203+
Store = new GenealogySecureStore(BackupService, Options, PrincipalProvider);
202204
}
203205

204206
public GenSecureStoreOptions Options { get; }
205207

208+
public PassThroughLocalKeyProtector LocalKeyProtector { get; }
209+
210+
public FixedPrincipalProvider PrincipalProvider { get; }
211+
206212
public MasterKeyBackupService BackupService { get; }
207213

208214
public GenealogySecureStore Store { get; }
@@ -220,6 +226,26 @@ public void Dispose()
220226
Directory.Delete(Options.GetValidatedRootDirectory(), recursive: true);
221227
}
222228
}
229+
230+
public sealed class PassThroughLocalKeyProtector : ILocalKeyProtector
231+
{
232+
public byte[] Protect(byte[] arrPlaintext)
233+
{
234+
ArgumentNullException.ThrowIfNull(arrPlaintext);
235+
return arrPlaintext.ToArray();
236+
}
237+
238+
public byte[] Unprotect(byte[] arrProtectedData)
239+
{
240+
ArgumentNullException.ThrowIfNull(arrProtectedData);
241+
return arrProtectedData.ToArray();
242+
}
243+
}
244+
245+
public sealed class FixedPrincipalProvider : ICurrentPrincipalProvider
246+
{
247+
public string GetCurrentPrincipalId() => "user:test-user";
248+
}
223249
}
224250

225251
private sealed class BaseGenClassesFactory : IGenealogyModelFactory

GenFreeWin/GenSecure.Core.Tests/PersonSecureStoreTests.cs

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,13 @@ public void SecureDelete_Plaintext_ShouldRemoveDataFile()
119119
}
120120

121121
[TestMethod]
122-
public void GetAllowedWindowsSidHashes_ShouldReturnHmacSha256Hashes_NotRawSids()
122+
public void GetAllowedPrincipalHashes_ShouldReturnHmacSha256Hashes_NotRawPrincipalIds()
123123
{
124124
using TestStoreScope scope = new();
125125

126126
scope.Store.Save("person-5", new TestPerson("Emmy", "Noether"));
127127

128-
IReadOnlyCollection<string> lstHashes = scope.Store.GetAllowedWindowsSidHashes("person-5");
128+
IReadOnlyCollection<string> lstHashes = scope.Store.GetAllowedPrincipalHashes("person-5");
129129

130130
Assert.AreEqual(1, lstHashes.Count);
131131

@@ -134,28 +134,27 @@ public void GetAllowedWindowsSidHashes_ShouldReturnHmacSha256Hashes_NotRawSids()
134134
// An HMAC-SHA256 output is 32 bytes = exactly 64 lowercase hex characters
135135
Assert.AreEqual(64, sSidHash.Length);
136136
Assert.IsTrue(sSidHash.All(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')),
137-
"The returned value must be a lowercase hex string, not a raw Windows SID.");
137+
"The returned value must be a lowercase hex string, not a raw principal identifier.");
138138

139-
// Must not look like a Windows SID
140-
Assert.IsFalse(sSidHash.StartsWith("S-", StringComparison.OrdinalIgnoreCase));
139+
// Must not be the raw principal identifier
140+
Assert.IsFalse(string.Equals(scope.PrincipalProvider.CurrentPrincipalId, sSidHash, StringComparison.OrdinalIgnoreCase));
141141

142-
// Must not be the plain SHA-256 of the SID — proves the HMAC pepper is in use
143-
string sCurrentSid = System.Security.Principal.WindowsIdentity.GetCurrent().User?.Value ?? string.Empty;
144-
string sPlainSha256 = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(sCurrentSid))).ToLowerInvariant();
142+
// Must not be the plain SHA-256 of the principal ID — proves the HMAC pepper is in use
143+
string sPlainSha256 = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(scope.PrincipalProvider.CurrentPrincipalId))).ToLowerInvariant();
145144
Assert.AreNotEqual(sPlainSha256, sSidHash,
146-
"The stored hash must be HMAC-SHA256(SID, pepper), not plain SHA-256(SID).");
145+
"The stored hash must be HMAC-SHA256(principalId, pepper), not plain SHA-256(principalId).");
147146
}
148147

149148
[TestMethod]
150149
public void GrantAccess_ShouldStoreHashAndBeVerifiable()
151150
{
152151
using TestStoreScope scope = new();
153152

154-
const string sFakeSid = "S-1-5-21-0000000000-1111111111-2222222222-500";
153+
const string sFakePrincipalId = "user:granted-user";
155154
scope.Store.Save("person-6", new TestPerson("Lise", "Meitner"));
156-
scope.Store.GrantAccess("person-6", sFakeSid);
155+
scope.Store.GrantAccess("person-6", sFakePrincipalId);
157156

158-
IReadOnlyCollection<string> lstHashes = scope.Store.GetAllowedWindowsSidHashes("person-6");
157+
IReadOnlyCollection<string> lstHashes = scope.Store.GetAllowedPrincipalHashes("person-6");
159158

160159
// Owner hash (current user) + granted hash = 2 entries
161160
Assert.AreEqual(2, lstHashes.Count);
@@ -165,17 +164,17 @@ public void GrantAccess_ShouldStoreHashAndBeVerifiable()
165164
{
166165
Assert.AreEqual(64, sHash.Length);
167166
Assert.IsTrue(sHash.All(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')),
168-
"Every stored hash must be a lowercase hex string, not a raw SID.");
167+
"Every stored hash must be a lowercase hex string, not a raw principal identifier.");
169168
}
170169

171-
// Raw SID must not appear in the list
172-
Assert.IsFalse(lstHashes.Contains(sFakeSid, StringComparer.OrdinalIgnoreCase),
173-
"The raw Windows SID must never be stored.");
170+
// Raw principal identifier must not appear in the list
171+
Assert.IsFalse(lstHashes.Contains(sFakePrincipalId, StringComparer.OrdinalIgnoreCase),
172+
"The raw principal identifier must never be stored.");
174173

175-
// Plain SHA-256(SID) must not appear — proves the pepper (HMAC) is in use
176-
string sPlainSha256 = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(sFakeSid))).ToLowerInvariant();
174+
// Plain SHA-256(principalId) must not appear — proves the pepper (HMAC) is in use
175+
string sPlainSha256 = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(sFakePrincipalId))).ToLowerInvariant();
177176
Assert.IsFalse(lstHashes.Contains(sPlainSha256, StringComparer.OrdinalIgnoreCase),
178-
"The stored hash must be HMAC-SHA256(SID, pepper), not plain SHA-256(SID).");
177+
"The stored hash must be HMAC-SHA256(principalId, pepper), not plain SHA-256(principalId).");
179178
}
180179

181180
[TestMethod]
@@ -205,12 +204,18 @@ public TestStoreScope()
205204
{
206205
RootDirectory = sRootDirectory,
207206
};
208-
BackupService = new MasterKeyBackupService(Options);
209-
Store = new PersonSecureStore(BackupService, Options);
207+
LocalKeyProtector = new PassThroughLocalKeyProtector();
208+
PrincipalProvider = new FixedPrincipalProvider();
209+
BackupService = new MasterKeyBackupService(Options, LocalKeyProtector);
210+
Store = new PersonSecureStore(BackupService, Options, PrincipalProvider);
210211
}
211212

212213
public GenSecureStoreOptions Options { get; }
213214

215+
public PassThroughLocalKeyProtector LocalKeyProtector { get; }
216+
217+
public FixedPrincipalProvider PrincipalProvider { get; }
218+
214219
public MasterKeyBackupService BackupService { get; }
215220

216221
public PersonSecureStore Store { get; }
@@ -245,6 +250,28 @@ public void Dispose()
245250

246251
}
247252

253+
private sealed class PassThroughLocalKeyProtector : ILocalKeyProtector
254+
{
255+
public byte[] Protect(byte[] arrPlaintext)
256+
{
257+
ArgumentNullException.ThrowIfNull(arrPlaintext);
258+
return arrPlaintext.ToArray();
259+
}
260+
261+
public byte[] Unprotect(byte[] arrProtectedData)
262+
{
263+
ArgumentNullException.ThrowIfNull(arrProtectedData);
264+
return arrProtectedData.ToArray();
265+
}
266+
}
267+
268+
private sealed class FixedPrincipalProvider : ICurrentPrincipalProvider
269+
{
270+
public string CurrentPrincipalId { get; } = "user:test-user";
271+
272+
public string GetCurrentPrincipalId() => CurrentPrincipalId;
273+
}
274+
248275
private static string[] GetRelativeSegments(string sRootPath, string sFilePath)
249276
{
250277
string sRelativePath = Path.GetRelativePath(sRootPath, sFilePath);

GenFreeWin/GenSecure.Core/CryptoUtilities.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -80,36 +80,36 @@ public static string GetShardedFilePath(string sRootDirectory, string sLogicalId
8080
}
8181

8282
/// <summary>
83-
/// Derives the SID pepper key from the master key using HKDF-SHA256.
84-
/// The pepper ensures that SID hashes cannot be brute-forced via SID enumeration,
85-
/// even when the SID domain and RID range are known to an attacker.
83+
/// Derives the principal pepper key from the master key using HKDF-SHA256.
84+
/// The pepper ensures that principal hashes cannot be brute-forced via principal enumeration,
85+
/// even when the principal identifier namespace is predictable to an attacker.
8686
/// </summary>
8787
/// <param name="arrMasterKey">The 32-byte AES master key.</param>
88-
/// <returns>A 32-byte SID pepper key.</returns>
89-
public static byte[] DeriveSidPepperKey(byte[] arrMasterKey)
88+
/// <returns>A 32-byte principal pepper key.</returns>
89+
public static byte[] DerivePrincipalPepperKey(byte[] arrMasterKey)
9090
{
9191
ArgumentNullException.ThrowIfNull(arrMasterKey);
9292

9393
return HKDF.DeriveKey(
9494
HashAlgorithmName.SHA256,
9595
arrMasterKey,
9696
outputLength: 32,
97-
info: Encoding.UTF8.GetBytes("GenSecure-SID-Pepper"));
97+
info: Encoding.UTF8.GetBytes("GenSecure-Principal-Pepper"));
9898
}
9999

100100
/// <summary>
101-
/// Computes an HMAC-SHA256 of a Windows SID keyed with the pepper derived from the master key.
102-
/// Raw SIDs are never written to disk; only their keyed hashes are persisted.
101+
/// Computes an HMAC-SHA256 of a principal identifier keyed with the pepper derived from the master key.
102+
/// Raw principal identifiers are never written to disk; only their keyed hashes are persisted.
103103
/// </summary>
104-
/// <param name="sSid">The Windows SID string (e.g. <c>S-1-5-21-…</c>).</param>
105-
/// <param name="arrSidPepperKey">The 32-byte pepper key obtained from <see cref="DeriveSidPepperKey"/>.</param>
104+
/// <param name="sPrincipalId">The principal identifier.</param>
105+
/// <param name="arrPrincipalPepperKey">The 32-byte pepper key obtained from <see cref="DerivePrincipalPepperKey"/>.</param>
106106
/// <returns>Lowercase hex-encoded HMAC-SHA256 digest.</returns>
107-
public static string ToSidHash(string sSid, byte[] arrSidPepperKey)
107+
public static string ToPrincipalHash(string sPrincipalId, byte[] arrPrincipalPepperKey)
108108
{
109-
ArgumentException.ThrowIfNullOrWhiteSpace(sSid);
110-
ArgumentNullException.ThrowIfNull(arrSidPepperKey);
109+
ArgumentException.ThrowIfNullOrWhiteSpace(sPrincipalId);
110+
ArgumentNullException.ThrowIfNull(arrPrincipalPepperKey);
111111

112-
byte[] arrHash = HMACSHA256.HashData(arrSidPepperKey, Encoding.UTF8.GetBytes(sSid));
112+
byte[] arrHash = HMACSHA256.HashData(arrPrincipalPepperKey, Encoding.UTF8.GetBytes(sPrincipalId));
113113
return Convert.ToHexString(arrHash).ToLowerInvariant();
114114
}
115115

GenFreeWin/GenSecure.Core/DependencyInjectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using GenSecure.Contracts;
33
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.DependencyInjection.Extensions;
45

56
namespace GenSecure.Core;
67

@@ -24,6 +25,8 @@ public static IServiceCollection AddGenSecureStore(this IServiceCollection servi
2425
configureOptions(options);
2526

2627
services.AddSingleton(options);
28+
services.TryAddSingleton<ILocalKeyProtector>(provider => PlatformServiceResolver.CreateLocalKeyProtector(provider.GetRequiredService<GenSecureStoreOptions>()));
29+
services.TryAddSingleton<ICurrentPrincipalProvider>(static _ => PlatformServiceResolver.CreateCurrentPrincipalProvider());
2730
services.AddSingleton<MasterKeyBackupService>();
2831
services.AddSingleton<IMasterKeyBackupService>(provider => provider.GetRequiredService<MasterKeyBackupService>());
2932
services.AddSingleton<IPersonSecureStore, PersonSecureStore>();

GenFreeWin/GenSecure.Core/FileModels.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Text.Json;
4+
using System.Text.Json.Serialization;
45

56
namespace GenSecure.Core;
67

@@ -31,9 +32,11 @@ internal sealed class PersonKeyRecord
3132

3233
public required string Tag { get; init; }
3334

34-
public required string OwnerWindowsSidHash { get; init; }
35+
[JsonPropertyName("OwnerWindowsSidHash")]
36+
public required string OwnerPrincipalHash { get; init; }
3537

36-
public required List<string> AllowedWindowsSidHashes { get; init; }
38+
[JsonPropertyName("AllowedWindowsSidHashes")]
39+
public required List<string> AllowedPrincipalHashes { get; init; }
3740

3841
public DateTimeOffset CreatedUtc { get; init; }
3942

0 commit comments

Comments
 (0)