using DigitalData.Core.Abstractions.Security; using Microsoft.Extensions.Options; using System.Security.Cryptography; using System.Text; namespace DigitalData.Core.Security { public class RSAFactory : IRSAFactory where TRSAFactoryParams : RSAFactoryParams { private static readonly Lazy> LazyInstance = new(() => new(Options.Create(new()))); public static RSAFactory Static => LazyInstance.Value; private readonly RSAFactoryParams _params; private readonly IEnumerable _lowerFileTags; //TODO: make the validation using regex public static string DefaultRSAKeyNameFormatter(string separator, string issuer, string audience, string encryptedPrivateKeyFileTag, string privateKeyFileTag, string publicKeyFileTag, bool isPrivate = true, Version? passwordVersion = null) { var sb = new StringBuilder(issuer.Length + audience.Length + separator.Length * 2 + 20); sb.Append(issuer).Append(separator).Append(audience).Append(separator); if (passwordVersion is null && isPrivate) sb.Append(privateKeyFileTag); else if (isPrivate) sb.Append(encryptedPrivateKeyFileTag).Append(separator).Append(passwordVersion); else if (passwordVersion is null) sb.Append(publicKeyFileTag); else sb.Append(publicKeyFileTag).Append(separator).Append(passwordVersion); return sb.ToString(); } private readonly PbeParameters _pbeParameters; public RSAFactory(IOptions options) { _params = options.Value; var keyFileTags = new string[] { _params.EncryptedPrivateKeyFileTag, _params.PrivateKeyFileTag, _params.PublicKeyFileTag }; _lowerFileTags = keyFileTags.Select(tag => tag.ToLower()); _pbeParameters = new PbeParameters(_params.PbeEncryptionAlgorithm, _params.PbeHashAlgorithmName, _params.PbeIterationCount); } public void ValidateFormatterParams(string issuer, string audience) { void ValidateForbidden(string value, string paramName) { if (Path.GetInvalidFileNameChars().Any(value.Contains) || _lowerFileTags.Any(tag => value.ToLower().Contains(tag))) throw new ArgumentException($"RSA decryptor key name creation is forbidden. The {paramName} contains forbidden characters that are not allowed in file naming.", paramName); } static void ValidateSeparator(string value, string paramName, string separator) { if (value.Contains(separator)) throw new ArgumentException($"RSA decryptor key name creation is forbidden. The {paramName} contains separator characters ({separator}) that are not allowed in file naming.", paramName); } ValidateForbidden(issuer, nameof(issuer)); ValidateForbidden(audience, nameof(audience)); ValidateForbidden(_params.RSAKeyNameSeparator, nameof(_params.RSAKeyNameSeparator)); ValidateSeparator(issuer, nameof(issuer), _params.RSAKeyNameSeparator); ValidateSeparator(audience, nameof(audience), _params.RSAKeyNameSeparator); } public string CreateRSAPrivateKeyPem(int? keySizeInBits = null) => RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportRSAPrivateKeyPem(); public string CreateEncryptedPrivateKeyPem( int? keySizeInBits = null, string? password = null, PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null, HashAlgorithmName? hashAlgorithmName = null, int? iterationCount = null) { password ??= _params.PbePassword; var pbeParameters = (pbeEncryptionAlgorithm is null && hashAlgorithmName is null && iterationCount is null) ? new PbeParameters( pbeEncryptionAlgorithm ?? _params.PbeEncryptionAlgorithm, hashAlgorithmName ?? _params.PbeHashAlgorithmName, iterationCount ?? _params.PbeIterationCount) : _pbeParameters; var encryptedPrivateKey = RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters); var pemChars = PemEncoding.Write(_params.EncryptedPrivateKeyPemLabel, encryptedPrivateKey); return new string(pemChars); } public async Task ReadRSADecryptorAsync(string path, Version? version = null, CancellationToken cancellationToken = default) { var pem = await File.ReadAllTextAsync(path, cancellationToken); (string Value, Version Version)? versionedPassword = null; if(version is not null) { if (version != Secrets.Version) throw new InvalidOperationException($"The provided version {version} does not match the expected version {Secrets.Version}."); versionedPassword = (Secrets.PBE_PASSWORD, Secrets.Version); } return new RSADecryptor() { Pem = pem, VersionedPassword = versionedPassword }; } } }