diff --git a/EnvelopeGenerator.Application/Configurations/CodeGeneratorConfig.cs b/EnvelopeGenerator.Application/Configurations/CodeGeneratorConfig.cs deleted file mode 100644 index b44c989a..00000000 --- a/EnvelopeGenerator.Application/Configurations/CodeGeneratorConfig.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace EnvelopeGenerator.Application.Configurations -{ - public class CodeGeneratorConfig - { - public string CharPool { get; init; } = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789012345678901234567890123456789"; - } -} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Configurations/CodeGeneratorParams.cs b/EnvelopeGenerator.Application/Configurations/CodeGeneratorParams.cs new file mode 100644 index 00000000..f4c8e1b1 --- /dev/null +++ b/EnvelopeGenerator.Application/Configurations/CodeGeneratorParams.cs @@ -0,0 +1,18 @@ +namespace EnvelopeGenerator.Application.Configurations +{ + public class CodeGeneratorParams + { + public string CharPool { get; init; } = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890123456789012345678901234567890123456789"; + + public int DefaultTotpSecretKeyLength { get; init; } = 32; + + public string TotpIssuer { get; init; } = "signFlow"; + + /// + /// 0 is user email, 1 is secret key and 2 is issuer. + /// + public string TotpUrlFormat { get; init; } = "otpauth://totp/{0}?secret={1}&issuer={2}"; + + public int TotpQRPixelsPerModule { get; init; } = 20; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Contracts/ICodeGenerator.cs b/EnvelopeGenerator.Application/Contracts/ICodeGenerator.cs index 4a282c66..38a27b85 100644 --- a/EnvelopeGenerator.Application/Contracts/ICodeGenerator.cs +++ b/EnvelopeGenerator.Application/Contracts/ICodeGenerator.cs @@ -3,5 +3,11 @@ public interface ICodeGenerator { string GenerateCode(int length); + + public string GenerateTotpSecretKey(int? length = null); + + public byte[] GenerateTotpQrCode(string userEmail, string secretKey, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null); + + public byte[] GenerateTotpQrCode(string userEmail, int? length = null, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Extensions/DIExtensions.cs b/EnvelopeGenerator.Application/Extensions/DIExtensions.cs index 3c1c62d5..ae8fedf9 100644 --- a/EnvelopeGenerator.Application/Extensions/DIExtensions.cs +++ b/EnvelopeGenerator.Application/Extensions/DIExtensions.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using DigitalData.Core.Client; using EnvelopeGenerator.Application.Configurations.GtxMessaging; +using QRCoder; namespace EnvelopeGenerator.Application.Extensions { @@ -55,13 +56,14 @@ namespace EnvelopeGenerator.Application.Extensions services.Configure(dispatcherConfigSection); services.Configure(mailConfigSection); - services.Configure(codeGeneratorConfigSection); + services.Configure(codeGeneratorConfigSection); services.Configure(envelopeReceiverCacheParamsSection); services.AddHttpClientService(smsConfigSection); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); return services; } @@ -70,7 +72,7 @@ namespace EnvelopeGenerator.Application.Extensions dispatcherConfigSection: config.GetSection("DispatcherConfig"), mailConfigSection: config.GetSection("MailConfig"), smsConfigSection: config.GetSection("SmsConfig"), - codeGeneratorConfigSection: config.GetSection("CodeGeneratorConfig"), + codeGeneratorConfigSection: config.GetSection("CodeGeneratorParams"), envelopeReceiverCacheParamsSection: config.GetSection("EnvelopeReceiverCacheParams")); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Extensions/MappingExtensions.cs b/EnvelopeGenerator.Application/Extensions/MappingExtensions.cs index 0d7f19c8..bd996b58 100644 --- a/EnvelopeGenerator.Application/Extensions/MappingExtensions.cs +++ b/EnvelopeGenerator.Application/Extensions/MappingExtensions.cs @@ -7,5 +7,8 @@ namespace EnvelopeGenerator.Application.Extensions public static bool Ok(this GtxMessagingResponse gtxMessagingResponse) => gtxMessagingResponse.TryGetValue("message-status", out var status) && status?.ToString()?.ToLower() == "ok"; + + public static string ToBase64String(this byte[] bytes) + => Convert.ToBase64String(bytes); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Services/CodeGenerator.cs b/EnvelopeGenerator.Application/Services/CodeGenerator.cs index 7b3f4b3c..89d22522 100644 --- a/EnvelopeGenerator.Application/Services/CodeGenerator.cs +++ b/EnvelopeGenerator.Application/Services/CodeGenerator.cs @@ -1,21 +1,26 @@ using EnvelopeGenerator.Application.Configurations; using EnvelopeGenerator.Application.Contracts; using Microsoft.Extensions.Options; +using OtpNet; +using QRCoder; using System.Text; namespace EnvelopeGenerator.Application.Services { public class CodeGenerator : ICodeGenerator { - public static Lazy LazyStatic => new(() => new CodeGenerator(Options.Create(new()))); + public static Lazy LazyStatic => new(() => new CodeGenerator(Options.Create(new()), new QRCodeGenerator())); public static CodeGenerator Static => LazyStatic.Value; - private readonly string _charPool; + private readonly CodeGeneratorParams _params; - public CodeGenerator(IOptions options) + private readonly QRCodeGenerator _qrCodeGenerator; + + public CodeGenerator(IOptions options, QRCodeGenerator qrCodeGenerator) { - _charPool = options.Value.CharPool; + _params = options.Value; + _qrCodeGenerator = qrCodeGenerator; } public string GenerateCode(int length) @@ -29,9 +34,33 @@ namespace EnvelopeGenerator.Application.Services var passwordBuilder = new StringBuilder(length); for (int i = 0; i < length; i++) - passwordBuilder.Append(_charPool[random.Next(_charPool.Length)]); + passwordBuilder.Append(_params.CharPool[random.Next(_params.CharPool.Length)]); return passwordBuilder.ToString(); } + + public string GenerateTotpSecretKey(int? length = null) + => Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(length ?? _params.DefaultTotpSecretKeyLength)); + + public byte[] GenerateTotpQrCode(string userEmail, string secretKey, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null) + { + var url = string.Format(totpUrlFormat ?? _params.TotpUrlFormat, + Uri.EscapeDataString(userEmail), + Uri.EscapeDataString(secretKey), + Uri.EscapeDataString(issuer ?? _params.TotpIssuer)); + using var qrCodeData = _qrCodeGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q); + using var qrCode = new BitmapByteQRCode(qrCodeData); + return qrCode.GetGraphic(pixelsPerModule ?? _params.TotpQRPixelsPerModule); + } + + public byte[] GenerateTotpQrCode(string userEmail, int? length = null, string? issuer = null, string? totpUrlFormat = null, int? pixelsPerModule = null) + { + return GenerateTotpQrCode( + userEmail: userEmail, + secretKey: GenerateTotpSecretKey(length: length), + issuer: issuer, + totpUrlFormat: totpUrlFormat, + pixelsPerModule: pixelsPerModule); + } } } \ No newline at end of file