feat(infra): create initial encryption implementations
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||||
|
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
namespace FiscalOS.Infra.Security;
|
||||||
|
|
||||||
|
public sealed class Encryptor : IEncryptor
|
||||||
|
{
|
||||||
|
private readonly IKeyRing _keyRing;
|
||||||
|
|
||||||
|
private Encryptor(IKeyRing keyRing)
|
||||||
|
{
|
||||||
|
_keyRing = keyRing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Encryptor From(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var keyRing = serviceProvider.GetRequiredService<IKeyRing>();
|
||||||
|
return new Encryptor(keyRing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Encryptor From(IKeyRing keyRing)
|
||||||
|
{
|
||||||
|
return new Encryptor(keyRing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyRingEntry PrimaryKey => _keyRing.GetPrimaryKey();
|
||||||
|
|
||||||
|
private static async Task<string> DecryptCoreAsync(string key, string cipherText, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var cipherTextBytes = Convert.FromBase64String(cipherText);
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = Convert.FromBase64String(key);
|
||||||
|
|
||||||
|
var iv = new byte[16];
|
||||||
|
Array.Copy(cipherTextBytes, 0, iv, 0, 16);
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
var decryptor = aes.CreateDecryptor();
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream(cipherTextBytes, 16, cipherTextBytes.Length - 16);
|
||||||
|
using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
|
||||||
|
using var streamReader = new StreamReader(cryptoStream);
|
||||||
|
|
||||||
|
return await streamReader.ReadToEndAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> DecryptAsync(string cipherText, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await DecryptCoreAsync(PrimaryKey.Key, cipherText, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> DecryptAsyncFor(User user, string plainText, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var decryptedKey = await DecryptCoreAsync(PrimaryKey.Key, user.EncryptedDataKey, ct).ConfigureAwait(false);
|
||||||
|
return await DecryptCoreAsync(decryptedKey, plainText, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> EncryptCoreAsync(string key, string plainText, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = Convert.FromBase64String(key);
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
aes.GenerateIV();
|
||||||
|
await memoryStream.WriteAsync(aes.IV, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var encryptor = aes.CreateEncryptor();
|
||||||
|
|
||||||
|
using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
|
||||||
|
using var streamWriter = new StreamWriter(cryptoStream);
|
||||||
|
|
||||||
|
await streamWriter.WriteAsync(plainText.ToCharArray(), ct).ConfigureAwait(false);
|
||||||
|
await streamWriter.FlushAsync(ct).ConfigureAwait(false);
|
||||||
|
streamWriter.Close();
|
||||||
|
|
||||||
|
return Convert.ToBase64String(memoryStream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> EncryptAsync(string plainText, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await EncryptCoreAsync(PrimaryKey.Key, plainText, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> EncryptAsyncFor(User user, string plainText, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var decryptedKey = await DecryptCoreAsync(PrimaryKey.Key, user.EncryptedDataKey, ct).ConfigureAwait(false);
|
||||||
|
return await EncryptCoreAsync(decryptedKey, plainText, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateKey()
|
||||||
|
{
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.GenerateKey();
|
||||||
|
|
||||||
|
return Convert.ToBase64String(aes.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EncryptedDataKey> GenerateEncryptedKeyAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var key = GenerateKey();
|
||||||
|
var keyUsed = PrimaryKey;
|
||||||
|
var encryptedKey = await EncryptCoreAsync(keyUsed.Key, key, ct).ConfigureAwait(false);
|
||||||
|
return EncryptedDataKey.From(keyUsed.KeyId, encryptedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
namespace FiscalOS.Infra.Security;
|
||||||
|
|
||||||
|
public sealed class FileKeyRing : IKeyRing, IDisposable
|
||||||
|
{
|
||||||
|
private const string KeyFileExtension = ".key";
|
||||||
|
private FileKeyRingOptions _options;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly IDisposable? _optionsChangeHandler;
|
||||||
|
private readonly Dictionary<string, KeyRingEntry> _keys = [];
|
||||||
|
private string KeyRingPath => _fileSystem.Path.GetFullPath(_options.KeysDirectoryPath, AppContext.BaseDirectory);
|
||||||
|
|
||||||
|
private FileKeyRing(IOptionsMonitor<FileKeyRingOptions> options, IFileSystem fileSystem)
|
||||||
|
{
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_options = options.CurrentValue;
|
||||||
|
|
||||||
|
LoadKeys();
|
||||||
|
|
||||||
|
_optionsChangeHandler = options.OnChange(options =>
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
LoadKeys();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FileKeyRing From(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var options = serviceProvider.GetRequiredService<IOptionsMonitor<FileKeyRingOptions>>();
|
||||||
|
var fileSystem = serviceProvider.GetRequiredService<IFileSystem>();
|
||||||
|
|
||||||
|
return new(options, fileSystem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FileKeyRing From(IOptionsMonitor<FileKeyRingOptions> options, IFileSystem fileSystem)
|
||||||
|
{
|
||||||
|
return new(options, fileSystem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyRingEntry GetKey(string keyId)
|
||||||
|
{
|
||||||
|
if (_keys.TryGetValue(keyId, out var key))
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new KeyNotFoundException($"Key with ID '{keyId}' not found in the key ring.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyRingEntry GetPrimaryKey()
|
||||||
|
{
|
||||||
|
return GetKey(_options.PrimaryKeyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadKeys()
|
||||||
|
{
|
||||||
|
_keys.Clear();
|
||||||
|
|
||||||
|
if (_fileSystem.Directory.Exists(KeyRingPath))
|
||||||
|
{
|
||||||
|
var keyFiles = _fileSystem.Directory.GetFiles(KeyRingPath, $"*{KeyFileExtension}");
|
||||||
|
|
||||||
|
foreach (var keyFile in keyFiles)
|
||||||
|
{
|
||||||
|
var keyId = _fileSystem.Path.GetFileNameWithoutExtension(keyFile);
|
||||||
|
var key = _fileSystem.File.ReadAllText(keyFile).Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(key) is false)
|
||||||
|
{
|
||||||
|
_keys[keyId] = KeyRingEntry.From(keyId, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_optionsChangeHandler?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KeyRingEntry> SaveKeyAsync(string key)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid().ToString();
|
||||||
|
var entry = KeyRingEntry.From(id, key);
|
||||||
|
var filename = id + KeyFileExtension;
|
||||||
|
var entryPath = _fileSystem.Path.Combine(KeyRingPath, filename);
|
||||||
|
await _fileSystem.File.WriteAllTextAsync(entryPath, key).ConfigureAwait(false);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace FiscalOS.Infra.Security;
|
||||||
|
|
||||||
|
public sealed record FileKeyRingOptions
|
||||||
|
{
|
||||||
|
public string KeysDirectoryPath { get; init; } = string.Empty;
|
||||||
|
public string PrimaryKeyId { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record class FileKeyRingOptionsSetup : IConfigureOptions<FileKeyRingOptions>
|
||||||
|
{
|
||||||
|
private const string SectionName = nameof(FileKeyRingOptions);
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public FileKeyRingOptionsSetup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(FileKeyRingOptions options)
|
||||||
|
{
|
||||||
|
_configuration.GetSection(SectionName).Bind(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
global using System.IdentityModel.Tokens.Jwt;
|
global using System.IdentityModel.Tokens.Jwt;
|
||||||
|
global using System.IO.Abstractions;
|
||||||
global using System.Security.Claims;
|
global using System.Security.Claims;
|
||||||
global using System.Security.Cryptography;
|
global using System.Security.Cryptography;
|
||||||
global using System.Text;
|
global using System.Text;
|
||||||
@@ -7,10 +8,13 @@ global using System.Text.Json;
|
|||||||
global using FiscalOS.Core.Authentication;
|
global using FiscalOS.Core.Authentication;
|
||||||
global using FiscalOS.Core.Data;
|
global using FiscalOS.Core.Data;
|
||||||
global using FiscalOS.Core.Identity;
|
global using FiscalOS.Core.Identity;
|
||||||
|
global using FiscalOS.Core.Security;
|
||||||
global using FiscalOS.Infra.Authentication;
|
global using FiscalOS.Infra.Authentication;
|
||||||
global using FiscalOS.Infra.Authorization;
|
global using FiscalOS.Infra.Authorization;
|
||||||
global using FiscalOS.Infra.Data;
|
global using FiscalOS.Infra.Data;
|
||||||
|
global using FiscalOS.Infra.Security;
|
||||||
|
|
||||||
|
global using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
global using Microsoft.AspNetCore.Authorization;
|
global using Microsoft.AspNetCore.Authorization;
|
||||||
global using Microsoft.AspNetCore.Authorization.Policy;
|
global using Microsoft.AspNetCore.Authorization.Policy;
|
||||||
global using Microsoft.AspNetCore.Http;
|
global using Microsoft.AspNetCore.Http;
|
||||||
|
|||||||
Reference in New Issue
Block a user