From dbe75a8b7afb103d2dce9e6ac2336a686056c6ed Mon Sep 17 00:00:00 2001 From: Alexander Kislitsyn <4lex.kislitsyn@gmail.com> Date: Fri, 5 Apr 2024 20:06:17 +0300 Subject: [PATCH] Allow to change encryption algorithm (#4591) --- .../Encryption/AesSecretEncryptor.cs | 66 +++++++++++++ .../Encryption/ISecretEncryptor.cs | 12 +++ .../SecretsOptionsBuilderExtensions.cs | 5 +- .../Elsa.Secrets/Manager/SecretsManager.cs | 98 +++++-------------- .../Elsa.Secrets/Providers/SecretsProvider.cs | 2 +- 5 files changed, 105 insertions(+), 78 deletions(-) create mode 100644 src/modules/secrets/Elsa.Secrets/Encryption/AesSecretEncryptor.cs create mode 100644 src/modules/secrets/Elsa.Secrets/Encryption/ISecretEncryptor.cs diff --git a/src/modules/secrets/Elsa.Secrets/Encryption/AesSecretEncryptor.cs b/src/modules/secrets/Elsa.Secrets/Encryption/AesSecretEncryptor.cs new file mode 100644 index 0000000000..aac3c9cb74 --- /dev/null +++ b/src/modules/secrets/Elsa.Secrets/Encryption/AesSecretEncryptor.cs @@ -0,0 +1,66 @@ +namespace Elsa.Secrets.Encryption +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Elsa.Secrets.Models; + using Elsa.Secrets.Options; + using Microsoft.Extensions.Options; + + public class AesSecretEncryptor : ISecretEncryptor + { + private readonly SecretsConfigOptions _secretsConfig; + + public AesSecretEncryptor(IOptions options) + { + _secretsConfig = options.Value; + } + + public Task EncryptProperties(Secret secret, CancellationToken cancellationToken = default) + { + if (_secretsConfig.EncryptionEnabled == true) + { + foreach (var property in secret.Properties) + { + if (property.IsEncrypted || (_secretsConfig.EncryptedProperties?.Contains(property.Name, StringComparer.OrdinalIgnoreCase) ?? false)) + { + continue; + } + + foreach (var (key, value) in property.Expressions) + { + property.Expressions[key] = AesEncryption.Encrypt(_secretsConfig.EncryptionKey, value); + } + + property.IsEncrypted = true; + } + } + + return Task.CompletedTask; + } + + public Task DecryptPropertiesAsync(Secret secret, CancellationToken cancellationToken = default) + { + if (_secretsConfig.EncryptionEnabled == true) + { + foreach (var property in secret.Properties) + { + if (!property.IsEncrypted) + { + continue; + } + + foreach (var (key, value) in property.Expressions) + { + property.Expressions[key] = AesEncryption.Decrypt(_secretsConfig.EncryptionKey, value); + } + + property.IsEncrypted = false; + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/modules/secrets/Elsa.Secrets/Encryption/ISecretEncryptor.cs b/src/modules/secrets/Elsa.Secrets/Encryption/ISecretEncryptor.cs new file mode 100644 index 0000000000..8b6cc15ece --- /dev/null +++ b/src/modules/secrets/Elsa.Secrets/Encryption/ISecretEncryptor.cs @@ -0,0 +1,12 @@ +namespace Elsa.Secrets.Encryption +{ + using System.Threading; + using System.Threading.Tasks; + using Elsa.Secrets.Models; + + public interface ISecretEncryptor + { + Task EncryptProperties(Secret secret, CancellationToken cancellationToken = default); + Task DecryptPropertiesAsync(Secret secret, CancellationToken cancellationToken = default); + } +} diff --git a/src/modules/secrets/Elsa.Secrets/Extensions/SecretsOptionsBuilderExtensions.cs b/src/modules/secrets/Elsa.Secrets/Extensions/SecretsOptionsBuilderExtensions.cs index a53e456c6d..a19b8a2a08 100644 --- a/src/modules/secrets/Elsa.Secrets/Extensions/SecretsOptionsBuilderExtensions.cs +++ b/src/modules/secrets/Elsa.Secrets/Extensions/SecretsOptionsBuilderExtensions.cs @@ -12,6 +12,8 @@ namespace Elsa.Secrets.Extensions { + using Elsa.Secrets.Encryption; + public static class SecretsOptionsBuilderExtensions { public static ElsaOptionsBuilder AddSecrets(this ElsaOptionsBuilder elsaOptions, IConfiguration configuration) @@ -23,7 +25,8 @@ public static ElsaOptionsBuilder AddSecrets(this ElsaOptionsBuilder elsaOptions, .AddScoped() .AddScoped() .Decorate() - .AddNotificationHandlersFrom(); + .AddNotificationHandlersFrom() + .AddSingleton(); elsaOptions.Services .TryAddProvider(ServiceLifetime.Scoped); diff --git a/src/modules/secrets/Elsa.Secrets/Manager/SecretsManager.cs b/src/modules/secrets/Elsa.Secrets/Manager/SecretsManager.cs index 1926a56f5d..c4a9d590be 100644 --- a/src/modules/secrets/Elsa.Secrets/Manager/SecretsManager.cs +++ b/src/modules/secrets/Elsa.Secrets/Manager/SecretsManager.cs @@ -1,4 +1,3 @@ -using System; using Elsa.Persistence.Specifications; using Elsa.Secrets.Models; using Elsa.Secrets.Persistence; @@ -9,57 +8,51 @@ using Elsa.Secrets.Persistence.Specifications; using System.Linq; using Elsa.Secrets.Encryption; -using Elsa.Secrets.Options; -using Microsoft.Extensions.Options; namespace Elsa.Secrets.Manager { public class SecretsManager : ISecretsManager { + private static readonly string HidedValue = new string('*', 8); private readonly ISecretsStore _secretsStore; - private readonly bool _encryptionEnabled; - private readonly string _encryptionKey; - private readonly string[] _encryptedProperties; + private readonly ISecretEncryptor _encryptor; - public SecretsManager(ISecretsStore secretsStore, IOptions options) + public SecretsManager(ISecretsStore secretsStore, ISecretEncryptor encryptor) { _secretsStore = secretsStore; - - _encryptionEnabled = options.Value.Enabled ?? false; - _encryptionKey = options.Value.EncryptionKey; - _encryptedProperties = options.Value.EncryptedProperties; + _encryptor = encryptor; } - public async Task GetSecretById(string id, CancellationToken cancellationToken = default) { + public virtual async Task GetSecretById(string id, CancellationToken cancellationToken = default) { var specification = new SecretsIdSpecification(id); var secret = await _secretsStore.FindAsync(specification, cancellationToken: cancellationToken); - DecryptProperties(secret); + await _encryptor.DecryptPropertiesAsync(secret, cancellationToken); return secret; } - public async Task GetSecretByName(string name, CancellationToken cancellationToken = default) { + public virtual async Task GetSecretByName(string name, CancellationToken cancellationToken = default) { var specification = new SecretsNameSpecification(name); var secrets = await _secretsStore.FindManyAsync(specification, OrderBySpecification.OrderBy(s => s.Type), cancellationToken: cancellationToken); var secret = secrets.FirstOrDefault(); - DecryptProperties(secret); + await _encryptor.DecryptPropertiesAsync(secret, cancellationToken); return secret; } - public async Task> GetSecrets(CancellationToken cancellationToken = default) + public virtual async Task> GetSecrets(CancellationToken cancellationToken = default) { var specification = Specification.Identity; var secrets = await _secretsStore.FindManyAsync(specification, cancellationToken: cancellationToken); foreach (var secret in secrets) { - DecryptProperties(secret); + await _encryptor.DecryptPropertiesAsync(secret, cancellationToken); } return secrets; } - public async Task> GetSecretViewModels(CancellationToken cancellationToken = default) + public virtual async Task> GetSecretViewModels(CancellationToken cancellationToken = default) { var specification = Specification.Identity; var secrets = await _secretsStore.FindManyAsync(specification, cancellationToken: cancellationToken); @@ -71,7 +64,7 @@ public async Task> GetSecretViewModels(CancellationToken can return secrets; } - public async Task> GetSecrets(string type, bool decrypt = true, CancellationToken cancellationToken = default) + public virtual async Task> GetSecrets(string type, bool decrypt = true, CancellationToken cancellationToken = default) { var specification = new SecretTypeSpecification(type); var secrets = await _secretsStore.FindManyAsync(specification, cancellationToken: cancellationToken); @@ -80,15 +73,14 @@ public async Task> GetSecrets(string type, bool decrypt = tr { foreach (var secret in secrets) { - DecryptProperties(secret); + await _encryptor.DecryptPropertiesAsync(secret, cancellationToken); } } - return secrets; } - public async Task AddOrUpdateSecret(Secret secret, bool restoreHiddenProperties, CancellationToken cancellationToken = default) + public virtual async Task AddOrUpdateSecret(Secret secret, bool restoreHiddenProperties, CancellationToken cancellationToken = default) { var clone = secret.Clone() as Secret; @@ -96,16 +88,17 @@ public async Task AddOrUpdateSecret(Secret secret, bool restoreHiddenPro { await RestoreHiddenProperties(clone, cancellationToken); } - EncryptProperties(clone); + + await _encryptor.EncryptProperties(clone, cancellationToken); if (clone.Id == null) - await _secretsStore.AddAsync(clone); + await _secretsStore.AddAsync(clone, cancellationToken); else - await _secretsStore.UpdateAsync(clone); + await _secretsStore.UpdateAsync(clone, cancellationToken); return clone; } - private async Task RestoreHiddenProperties(Secret secret, CancellationToken cancellationToken) + protected virtual async Task RestoreHiddenProperties(Secret secret, CancellationToken cancellationToken) { var specification = new SecretsIdSpecification(secret.Id); var existingSecret = await _secretsStore.FindAsync(specification, cancellationToken: cancellationToken); @@ -120,63 +113,16 @@ private async Task RestoreHiddenProperties(Secret secret, CancellationToken canc } } } - - private void HideEncryptedProperties(Secret secret) + + protected virtual void HideEncryptedProperties(Secret secret) { foreach (var secretProperty in secret.Properties) { if (!secretProperty.IsEncrypted) continue; foreach (var key in secretProperty.Expressions.Keys) { - secretProperty.Expressions[key] = new string('*', 8); - } - } - } - - private void EncryptProperties(Secret secret) - { - if (!_encryptionEnabled) - { - return; - } - foreach (var property in secret.Properties) - { - var encrypt = _encryptedProperties.Contains(property.Name, StringComparer.OrdinalIgnoreCase); - if (!encrypt || property.IsEncrypted) - { - continue; - } - - foreach (var key in property.Expressions.Keys) - { - var value = property.Expressions[key]; - property.Expressions[key] = AesEncryption.Encrypt(_encryptionKey, value); + secretProperty.Expressions[key] = HidedValue; } - - property.IsEncrypted = true; - } - } - - private void DecryptProperties(Secret secret) - { - if (!_encryptionEnabled) - { - return; - } - foreach (var property in secret.Properties) - { - if (!property.IsEncrypted) - { - continue; - } - - foreach (var key in property.Expressions.Keys) - { - var value = property.Expressions[key]; - property.Expressions[key] = AesEncryption.Decrypt(_encryptionKey, value); - } - - property.IsEncrypted = false; } } } diff --git a/src/modules/secrets/Elsa.Secrets/Providers/SecretsProvider.cs b/src/modules/secrets/Elsa.Secrets/Providers/SecretsProvider.cs index 63a99329a0..eda62181da 100644 --- a/src/modules/secrets/Elsa.Secrets/Providers/SecretsProvider.cs +++ b/src/modules/secrets/Elsa.Secrets/Providers/SecretsProvider.cs @@ -81,7 +81,6 @@ public async Task> GetSecretsAsync(string type) public async Task IsSecretValueSensitiveData(string type, string name) { var secrets = await _secretsManager.GetSecrets(type, false); - var formatter = _valueFormatters.FirstOrDefault(x => x.Type == type); var secret = secrets.Where(x => x.Name?.Equals(name, StringComparison.InvariantCultureIgnoreCase) == true && x.Type?.Equals(type, StringComparison.InvariantCultureIgnoreCase) == true) ?.FirstOrDefault(); if (secret == null) @@ -89,6 +88,7 @@ public async Task IsSecretValueSensitiveData(string type, string name) return false; } + var formatter = _valueFormatters.FirstOrDefault(x => x.Type == type); return formatter?.IsSecretValueSensitiveData(secret) ?? false; }