diff --git a/src/FiscalOS.Infra/FiscalOS.Infra.csproj b/src/FiscalOS.Infra/FiscalOS.Infra.csproj
index 877c74d..3d93af5 100644
--- a/src/FiscalOS.Infra/FiscalOS.Infra.csproj
+++ b/src/FiscalOS.Infra/FiscalOS.Infra.csproj
@@ -4,6 +4,7 @@
+
diff --git a/src/FiscalOS.Infra/Security/Encryptor.cs b/src/FiscalOS.Infra/Security/Encryptor.cs
new file mode 100644
index 0000000..0d8d45b
--- /dev/null
+++ b/src/FiscalOS.Infra/Security/Encryptor.cs
@@ -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();
+ return new Encryptor(keyRing);
+ }
+
+ public static Encryptor From(IKeyRing keyRing)
+ {
+ return new Encryptor(keyRing);
+ }
+
+ private KeyRingEntry PrimaryKey => _keyRing.GetPrimaryKey();
+
+ private static async Task 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 DecryptAsync(string cipherText, CancellationToken ct)
+ {
+ return await DecryptCoreAsync(PrimaryKey.Key, cipherText, ct).ConfigureAwait(false);
+ }
+
+ public async Task 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 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 EncryptAsync(string plainText, CancellationToken ct)
+ {
+ return await EncryptCoreAsync(PrimaryKey.Key, plainText, ct).ConfigureAwait(false);
+ }
+
+ public async Task 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 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);
+ }
+
+}
\ No newline at end of file
diff --git a/src/FiscalOS.Infra/Security/FileKeyRing.cs b/src/FiscalOS.Infra/Security/FileKeyRing.cs
new file mode 100644
index 0000000..bc6b2e2
--- /dev/null
+++ b/src/FiscalOS.Infra/Security/FileKeyRing.cs
@@ -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 _keys = [];
+ private string KeyRingPath => _fileSystem.Path.GetFullPath(_options.KeysDirectoryPath, AppContext.BaseDirectory);
+
+ private FileKeyRing(IOptionsMonitor 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>();
+ var fileSystem = serviceProvider.GetRequiredService();
+
+ return new(options, fileSystem);
+ }
+
+ public static FileKeyRing From(IOptionsMonitor 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 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;
+ }
+}
\ No newline at end of file
diff --git a/src/FiscalOS.Infra/Security/FileKeyRingOptions.cs b/src/FiscalOS.Infra/Security/FileKeyRingOptions.cs
new file mode 100644
index 0000000..0daad64
--- /dev/null
+++ b/src/FiscalOS.Infra/Security/FileKeyRingOptions.cs
@@ -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
+{
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/src/FiscalOS.Infra/Usings.cs b/src/FiscalOS.Infra/Usings.cs
index b428c29..a681a67 100644
--- a/src/FiscalOS.Infra/Usings.cs
+++ b/src/FiscalOS.Infra/Usings.cs
@@ -1,4 +1,5 @@
global using System.IdentityModel.Tokens.Jwt;
+global using System.IO.Abstractions;
global using System.Security.Claims;
global using System.Security.Cryptography;
global using System.Text;
@@ -7,10 +8,13 @@ global using System.Text.Json;
global using FiscalOS.Core.Authentication;
global using FiscalOS.Core.Data;
global using FiscalOS.Core.Identity;
+global using FiscalOS.Core.Security;
global using FiscalOS.Infra.Authentication;
global using FiscalOS.Infra.Authorization;
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.Policy;
global using Microsoft.AspNetCore.Http;