diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..aba2a7a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,191 @@ +############################### +# PandaTech Editor Config # +############################### + + +################################ +# ReSharper Generated Settings # +################################ + +[*] +charset = utf-8-bom +end_of_line = crlf +trim_trailing_whitespace = false +insert_final_newline = false +indent_style = space +indent_size = 4 + +# Microsoft .NET properties +csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_align_linq_query = true +resharper_align_multiline_calls_chain = true +resharper_apply_auto_detected_rules = false +resharper_braces_for_for = required +resharper_braces_for_foreach = required +resharper_braces_for_ifelse = required +resharper_braces_for_while = required +resharper_cpp_insert_final_newline = true +resharper_csharp_indent_size = 3 +resharper_csharp_max_enum_members_on_line = 0 +resharper_csharp_tab_width = 3 +resharper_csharp_wrap_arguments_style = chop_if_long +resharper_csharp_wrap_parameters_style = chop_if_long +resharper_formatter_off_tag = @formatter:off +resharper_formatter_on_tag = @formatter:on +resharper_formatter_tags_enabled = true +resharper_keep_existing_declaration_parens_arrangement = false +resharper_keep_existing_expr_member_arrangement = false +resharper_keep_existing_initializer_arrangement = false +resharper_keep_existing_switch_expression_arrangement = false +resharper_max_array_initializer_elements_on_line = 0 +resharper_max_initializer_elements_on_line = 0 +resharper_place_accessorholder_attribute_on_same_line = false +resharper_place_accessor_attribute_on_same_line = false +resharper_place_field_attribute_on_same_line = false +resharper_place_simple_anonymousmethod_on_single_line = false +resharper_place_simple_embedded_statement_on_same_line = false +resharper_place_simple_initializer_on_single_line = false +resharper_place_simple_property_pattern_on_single_line = false +resharper_use_indent_from_vs = false +resharper_wrap_after_property_in_chained_method_calls = true +resharper_wrap_array_initializer_style = chop_if_long +resharper_wrap_chained_method_calls = chop_always +resharper_wrap_linq_expressions = chop_always +resharper_wrap_list_pattern = chop_if_long +resharper_wrap_object_and_collection_initializer_style = chop_always +resharper_wrap_property_pattern = chop_always + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_enforce_foreach_statement_braces_highlighting = warning +resharper_enforce_for_statement_braces_highlighting = warning +resharper_enforce_if_statement_braces_highlighting = warning +resharper_enforce_lock_statement_braces_highlighting = warning +resharper_enforce_using_statement_braces_highlighting = warning +resharper_enforce_while_statement_braces_highlighting = warning +resharper_mvc_action_not_resolved_highlighting = warning +resharper_mvc_area_not_resolved_highlighting = warning +resharper_mvc_controller_not_resolved_highlighting = warning +resharper_mvc_masterpage_not_resolved_highlighting = warning +resharper_mvc_partial_view_not_resolved_highlighting = warning +resharper_mvc_template_not_resolved_highlighting = warning +resharper_mvc_view_component_not_resolved_highlighting = warning +resharper_mvc_view_component_view_not_resolved_highlighting = warning +resharper_mvc_view_not_resolved_highlighting = warning +resharper_razor_assembly_not_resolved_highlighting = warning +resharper_redundant_base_qualifier_highlighting = warning +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,bowerrc,jest.config}] +indent_style = space +indent_size = 2 + +[{*.yaml,*.yml}] +indent_style = space +indent_size = 2 + +[*.cs] +indent_style = space +indent_size = 3 +tab_width = 3 + +[*.{appxmanifest,asax,ascx,aspx,axaml,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,skin,tpp,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 + +######################### +# Custom added settings # +######################### +[*.cs] +dotnet_diagnostic.cs8600.severity = error #Converting null literal or possible null value to non-nullable type. +dotnet_diagnostic.cs8601.severity = error #Possible null reference assignment. +dotnet_diagnostic.cs8602.severity = error #possible dereference of a null reference +dotnet_diagnostic.cs8603.severity = error #possible null reference return +dotnet_diagnostic.cs8604.severity = error #possible null reference argument for parameter +dotnet_diagnostic.cs8605.severity = error #Unboxing a possibly null value +dotnet_diagnostic.cs8618.severity = error # Non-nullable field is uninitialized. Consider declaring as nullable. +dotnet_diagnostic.cs8625.severity = error # Cannot convert null literal to non-nullable reference type. +dotnet_diagnostic.cs8762.severity = error # Nullability of reference types in type doesn't match overridden member. +dotnet_diagnostic.cs1717.severity = error #variable is assigned to itself +dotnet_diagnostic.cs1718.severity = error #comparison made to same variable +dotnet_diagnostic.cs0659.severity = error #overriding object.Equals but not overriding object.GetHashCode +dotnet_diagnostic.cs0251.severity = error #Indexing an array with a negative index (array indices always start at zero) +dotnet_diagnostic.s3363.severity = none #Never set DateTime as PrimaryKey. Ignored as we never do it but have warnings for cache entities. +dotnet_diagnostic.ca2016.severity = error #Forwarding cancellation tokens +csharp_style_namespace_declarations = file_scoped:error +resharper_entity_framework_model_validation_unlimited_string_length_highlighting = none + + +######################### +# VS added settings # +######################### + +[*.cs] +csharp_style_namespace_declarations = file_scoped:error +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + +[{*.yaml,*.yml}] +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 3 +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent + +[*.csv] +indent_style = tab +tab_width = 4 \ No newline at end of file diff --git a/Pandatech.Crypto.sln b/Pandatech.Crypto.sln index 00df5ab..41ff508 100644 --- a/Pandatech.Crypto.sln +++ b/Pandatech.Crypto.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore Readme.md = Readme.md global.json = global.json + .editorconfig = .editorconfig EndProjectSection EndProject Global diff --git a/Readme.md b/Readme.md index 6c53d4c..7d25f1c 100644 --- a/Readme.md +++ b/Readme.md @@ -28,30 +28,31 @@ Install-Package Pandatech.Crypto ## How to Use -### Configuring Dependency Injection +### Configuring in Program.cs -Add the following code to your Program.cs file to configure AES256 and Argon2Id services with minimal setup: +Use the following code to configure AES256 and Argon2Id in your `Program.cs`: ```csharp -using Pandatech.Crypto; +using Pandatech.Crypto.Helpers; +using Pandatech.Crypto.Extensions; -// For Aes256 -builder.services.AddPandatechCryptoAes256(options => -{ - options.Key = "YourAes256KeyHere"; // Make sure to use a secure key -}); +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); -// For Argon2Id default configuration -builder.services.AddPandatechCryptoArgon2Id(); +// Register AES key +app.AddAes256Key("YourBase64EncodedAes256KeyHere"); -// For Argon2Id overriding default configurations - builder.services.AddPandatechCryptoArgon2Id(options => +// Optional - Change default Argon2Id configurations. If below method is not called, default configurations will be used. +app.ConfigureArgon2Id(options => { options.SaltSize = 16; options.DegreeOfParallelism = 8; options.Iterations = 5; options.MemorySize = 128 * 1024; -}); +}); + +app.Run(); + ``` ### AES256 Class @@ -59,8 +60,14 @@ builder.services.AddPandatechCryptoArgon2Id(); **Encryption/Decryption methods with hashing** ```csharp -byte[] cipherText = aes256.Encrypt("your-plaintext"); -string plainText = aes256.Decrypt(cipherText); +using Pandatech.Crypto.Helpers; + +// Encrypt using AES256 +var encryptedBytes = Aes256.Encrypt("your-plaintext"); + +// Decrypt AES256-encrypted data +var decryptedText = Aes256.Decrypt(encryptedBytes); + ``` **Encryption/Decryption methods without hashing** @@ -74,24 +81,26 @@ string plainText = aes256.DecryptWithoutHash(cipherText); ```csharp string customKey = "your-custom-base64-encoded-key"; -byte[] cipherText = aes256.Encrypt("your-plaintext", customKey); -string plainText = aes256.Decrypt(cipherText, customKey); + +// Encrypt with a custom key +var encrypted = Aes256.Encrypt("your-plaintext", customKey); + +// Decrypt with the same key +var decrypted = Aes256.Decrypt(encrypted, customKey); ``` **Stream-based Encryption/Decryption methods** -The AES256 class also supports stream-based operations, allowing for encryption and decryption directly on streams, -which is ideal for handling large files or data streams efficiently. - ```csharp using var inputStream = new MemoryStream(Encoding.UTF8.GetBytes("your-plaintext")); using var outputStream = new MemoryStream(); -aes256.EncryptStream(inputStream, outputStream, "your-custom-base64-encoded-key"); -byte[] encryptedBytes = outputStream.ToArray(); -using var inputStream = new MemoryStream(encryptedBytes); -using var outputStream = new MemoryStream(); -aes256.DecryptStream(inputStream, outputStream, "your-custom-base64-encoded-key"); +// Encrypt stream +Aes256.Encrypt(inputStream, outputStream, "your-base64-key"); + +// Decrypt stream +using var decryptedStream = new MemoryStream(outputStream.ToArray()); +Aes256.Decrypt(decryptedStream, outputStream, "your-base64-key"); string decryptedText = Encoding.UTF8.GetString(outputStream.ToArray()); ``` @@ -116,11 +125,13 @@ string decryptedText = Encoding.UTF8.GetString(outputStream.ToArray()); **Examples on usage** ```csharp -// Example usage for hashing -var hashedPassword = _argon2Id.HashPassword("yourPassword"); +using Pandatech.Crypto.Helpers; -// Example usage for verifying a hash -var isPasswordValid = _argon2Id.VerifyHash("yourPassword", hashedPassword); +// Hash a password using Argon2Id +var hashedPassword = Argon2Id.HashPassword("yourPassword"); + +// Verify a hashed password +bool isValid = Argon2Id.VerifyHash("yourPassword", hashedPassword); ``` ### Random Class diff --git a/src/Pandatech.Crypto/Aes256.cs b/src/Pandatech.Crypto/Aes256.cs deleted file mode 100644 index 576b108..0000000 --- a/src/Pandatech.Crypto/Aes256.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System.Security.Cryptography; - -namespace Pandatech.Crypto; - -public class Aes256(Aes256Options options) -{ - private readonly Aes256Options _options = options ?? throw new ArgumentNullException(nameof(options)); - private const int KeySize = 256; - private const int IvSize = 16; - private const int HashSize = 64; - - public byte[] Encrypt(string plainText) - { - return EncryptWithHashInner(plainText); - } - - public byte[] EncryptWithoutHash(string plainText) - { - return EncryptWithoutHashInner(plainText, null); - } - - public byte[] Encrypt(string plainText, string key) - { - ValidateKey(key); - - return EncryptWithHashInner(plainText, key); - } - - public byte[] EncryptWithoutHash(string plainText, string key) - { - ValidateKey(key); - - return EncryptWithoutHashInner(plainText, key); - } - - public void Encrypt(Stream inputStream, Stream outputStream, string? key = null) - { - key ??= _options.Key; - ValidateKey(key); - using var aesAlg = Aes.Create(); - aesAlg.KeySize = KeySize; - aesAlg.Padding = PaddingMode.PKCS7; - aesAlg.Key = Convert.FromBase64String(key); - aesAlg.GenerateIV(); - - outputStream.Write(aesAlg.IV, 0, aesAlg.IV.Length); - - using var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); - using var cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write, leaveOpen: true); - inputStream.CopyTo(cryptoStream); - } - - private byte[] EncryptWithHashInner(string plainText, string? key = null) - { - key ??= _options.Key; - var encryptedBytes = EncryptWithoutHashInner(plainText, key); - var hashBytes = Sha3.Hash(plainText); - return hashBytes.Concat(encryptedBytes).ToArray(); - } - - private byte[] EncryptWithoutHashInner(string plainText, string? key) - { - key ??= _options.Key; - if (plainText == "") - return []; - using var aesAlg = Aes.Create(); - aesAlg.KeySize = KeySize; - aesAlg.Padding = PaddingMode.PKCS7; - aesAlg.Key = Convert.FromBase64String(key); - - var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); - - using var msEncrypt = new MemoryStream(); - using var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write); - using var swEncrypt = new StreamWriter(csEncrypt); - swEncrypt.Write(plainText); - swEncrypt.Flush(); - csEncrypt.FlushFinalBlock(); - - var encryptedPasswordByte = msEncrypt.ToArray(); - - var result = aesAlg.IV.Concat(encryptedPasswordByte).ToArray(); - return result; - } - - public string Decrypt(byte[] cipherText) - { - return cipherText.Length == 0 - ? "" - : DecryptSkippingHashInner(cipherText); - } - - public string DecryptWithoutHash(byte[] cipherText) - { - return cipherText.Length == 0 - ? "" - : DecryptWithoutSkippingHashInner(cipherText, null); - } - - public string Decrypt(byte[] cipherText, string key) - { - ValidateKey(key); - return cipherText.Length == 0 - ? "" - : DecryptSkippingHashInner(cipherText, key); - } - - public string DecryptWithoutHash(byte[] cipherText, string key) - { - ValidateKey(key); - return cipherText.Length == 0 - ? "" - : DecryptWithoutSkippingHashInner(cipherText, key); - } - - public void Decrypt(Stream inputStream, Stream outputStream, string? key = null) - { - key ??= _options.Key; - ValidateKey(key); - - var iv = new byte[IvSize]; - if (inputStream.Read(iv, 0, IvSize) != IvSize) - throw new ArgumentException("Input stream does not contain a complete IV."); - - using var aesAlg = Aes.Create(); - aesAlg.KeySize = KeySize; - aesAlg.Padding = PaddingMode.PKCS7; - aesAlg.Key = Convert.FromBase64String(key); - aesAlg.IV = iv; - - using var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); - using var cryptoStream = new CryptoStream(inputStream, decryptor, CryptoStreamMode.Read, leaveOpen: true); - cryptoStream.CopyTo(outputStream); - } - - private string DecryptWithoutSkippingHashInner(byte[] cipherText, string? key) - { - key ??= _options.Key; - if (cipherText.Length == 0) - return ""; - var iv = cipherText.Take(IvSize).ToArray(); - var encrypted = cipherText.Skip(IvSize).ToArray(); - - using var aesAlg = Aes.Create(); - aesAlg.KeySize = KeySize; - aesAlg.Padding = PaddingMode.PKCS7; - aesAlg.Key = Convert.FromBase64String(key); - aesAlg.IV = iv; - - var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); - - using var msDecrypt = new MemoryStream(encrypted); - using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read); - using var srDecrypt = new StreamReader(csDecrypt); - return srDecrypt.ReadToEnd(); - } - - private string DecryptSkippingHashInner(IEnumerable cipherTextWithHash, string? key = null) - { - key ??= _options.Key; - var cipherText = cipherTextWithHash.Skip(HashSize).ToArray(); - return DecryptWithoutSkippingHashInner(cipherText, key); - } - - private static void ValidateKey(string key) - { - if (string.IsNullOrEmpty(key) || !IsBase64String(key) || Convert.FromBase64String(key).Length != 32) - throw new ArgumentException("Invalid key."); - } - - - private static bool IsBase64String(string s) - { - var buffer = new Span(new byte[s.Length]); - return Convert.TryFromBase64String(s, buffer, out _); - } -} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Argon2Id.cs b/src/Pandatech.Crypto/Argon2Id.cs deleted file mode 100644 index 6cd0fc2..0000000 --- a/src/Pandatech.Crypto/Argon2Id.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Text; -using Konscious.Security.Cryptography; - -namespace Pandatech.Crypto; - -public class Argon2Id -{ - private readonly Argon2IdOptions _options; - - public Argon2Id(Argon2IdOptions options) - { - _options = options; - } - - public Argon2Id() - { - _options = new Argon2IdOptions(); - } - - private const int SaltSize = 16; - - - public byte[] HashPassword(string password) - { - if (string.IsNullOrEmpty(password)) - { - throw new ArgumentException("Password cannot be null or empty.", nameof(password)); - } - - var salt = Random.GenerateBytes(_options.SaltSize); - return HashPassword(password, salt); - } - - private byte[] HashPassword(string password, byte[] salt) - { - using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)) - { - Salt = salt, - DegreeOfParallelism = _options.DegreeOfParallelism, - Iterations = _options.Iterations, - MemorySize = _options.MemorySize - }; - - var result = salt.Concat(argon2.GetBytes(32)).ToArray(); - - return result; - } - - public bool VerifyHash(string password, byte[] passwordHash) - { - if (passwordHash == null || passwordHash.Length <= _options.SaltSize) - { - throw new ArgumentException($"Hash must be at least {SaltSize} bytes.", nameof(passwordHash)); - } - - var salt = passwordHash.Take(_options.SaltSize).ToArray(); - - var newHash = HashPassword(password, salt); - return ConstantTimeComparison(passwordHash, newHash); - } - - private static bool ConstantTimeComparison(IReadOnlyList a, IReadOnlyList b) - { - var diff = (ushort)a.Count ^ (ushort)b.Count; - for (var i = 0; i < a.Count && i < b.Count; i++) - { - diff |= (ushort)(a[i] ^ b[i]); - } - - return diff == 0; - } -} \ No newline at end of file diff --git a/src/Pandatech.Crypto/AssemblyInfo.cs b/src/Pandatech.Crypto/AssemblyInfo.cs new file mode 100644 index 0000000..8c3ec57 --- /dev/null +++ b/src/Pandatech.Crypto/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Pandatech.Crypto.Tests")] \ No newline at end of file diff --git a/src/Pandatech.Crypto/Extensions/HostBuilderExtensions.cs b/src/Pandatech.Crypto/Extensions/HostBuilderExtensions.cs new file mode 100644 index 0000000..76b97d3 --- /dev/null +++ b/src/Pandatech.Crypto/Extensions/HostBuilderExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Builder; +using Pandatech.Crypto.Helpers; + +namespace Pandatech.Crypto.Extensions; + +public static class HostBuilderExtensions +{ + public static WebApplication AddAes256Key(this WebApplication app, string aesKey) + { + Aes256.RegisterKey(aesKey); + return app; + } + + public static WebApplication ConfigureArgon2Id(this WebApplication app, Action configure) + { + var options = new Argon2IdOptions(); + configure(options); + Argon2Id.Configure(options); + return app; + } +} \ No newline at end of file diff --git a/src/Pandatech.Crypto/GZip.cs b/src/Pandatech.Crypto/GZip.cs deleted file mode 100644 index e1d8289..0000000 --- a/src/Pandatech.Crypto/GZip.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.IO.Compression; -using System.Text; -using System.Text.Json; - -namespace Pandatech.Crypto; - -public static class GZip -{ - private static readonly JsonSerializerOptions JsonSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }; - - public static void Compress(Stream inputStream, Stream outputStream) - { - using var gzipStream = new GZipStream(outputStream, CompressionMode.Compress, leaveOpen: true); - inputStream.CopyTo(gzipStream); - } - - // New method to decompress data directly from one stream to another - public static void Decompress(Stream inputStream, Stream outputStream) - { - using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress, leaveOpen: true); - gzipStream.CopyTo(outputStream); - } - - public static byte[] Compress(T obj) - { - var jsonString = JsonSerializer.Serialize(obj, JsonSerializerOptions); - return Compress(jsonString); - } - - public static byte[] Compress(string data) - { - using var memoryStream = new MemoryStream(); - using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) - { - using (var writer = new StreamWriter(gzipStream, Encoding.UTF8)) - { - writer.Write(data); - } - } - - var compressedData = memoryStream.ToArray(); - - return compressedData; - } - - public static byte[] Compress(byte[] data) - { - using var memoryStream = new MemoryStream(); - using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) - { - gzipStream.Write(data, 0, data.Length); - } - - return memoryStream.ToArray(); - } - - public static T? Decompress(byte[] compressedData) - { - var decompressed = Decompress(compressedData); - var jsonString = Encoding.UTF8.GetString(decompressed); - return JsonSerializer.Deserialize(jsonString, JsonSerializerOptions); - } - - public static T? Decompress(string compressedData) - { - var decompressed = Decompress(compressedData); - var jsonString = Encoding.UTF8.GetString(decompressed); - return JsonSerializer.Deserialize(jsonString, JsonSerializerOptions); - } - - public static byte[] Decompress(string compressedBase64) - { - var compressedData = Convert.FromBase64String(compressedBase64); - return Decompress(compressedData); - } - - public static byte[] Decompress(byte[] data) - { - using var compressedStream = new MemoryStream(data); - using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress); - using var reader = new StreamReader(gzipStream, Encoding.UTF8); - var decompressedString = reader.ReadToEnd(); - return Encoding.UTF8.GetBytes(decompressedString); - } -} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Helpers/Aes256.cs b/src/Pandatech.Crypto/Helpers/Aes256.cs new file mode 100644 index 0000000..01258fd --- /dev/null +++ b/src/Pandatech.Crypto/Helpers/Aes256.cs @@ -0,0 +1,204 @@ +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +namespace Pandatech.Crypto.Helpers; + +public static class Aes256 +{ + private const int KeySize = 256; + private const int IvSize = 16; + private const int HashSize = 64; + private static string? Key { get; set; } + + public static byte[] Encrypt(string plainText) + { + return EncryptWithHashInner(plainText); + } + + public static byte[] EncryptWithoutHash(string plainText) + { + return EncryptWithoutHashInner(plainText, null); + } + + public static byte[] Encrypt(string plainText, string key) + { + ValidateKey(key); + + return EncryptWithHashInner(plainText, key); + } + + public static byte[] EncryptWithoutHash(string plainText, string key) + { + ValidateKey(key); + + return EncryptWithoutHashInner(plainText, key); + } + + public static void Encrypt(Stream inputStream, Stream outputStream, string? key = null) + { + key ??= Key; + ValidateKey(key); + using var aesAlg = Aes.Create(); + aesAlg.KeySize = KeySize; + aesAlg.Padding = PaddingMode.PKCS7; + aesAlg.Key = Convert.FromBase64String(key); + aesAlg.GenerateIV(); + + outputStream.Write(aesAlg.IV, 0, aesAlg.IV.Length); + + using var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + using var cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write, true); + inputStream.CopyTo(cryptoStream); + } + + private static byte[] EncryptWithHashInner(string plainText, string? key = null) + { + key ??= Key; + var encryptedBytes = EncryptWithoutHashInner(plainText, key); + var hashBytes = Sha3.Hash(plainText); + return hashBytes.Concat(encryptedBytes) + .ToArray(); + } + + private static byte[] EncryptWithoutHashInner(string plainText, string? key) + { + key ??= Key; + if (plainText == "") + { + return []; + } + + ArgumentNullException.ThrowIfNull(key); + + using var aesAlg = Aes.Create(); + aesAlg.KeySize = KeySize; + aesAlg.Padding = PaddingMode.PKCS7; + aesAlg.Key = Convert.FromBase64String(key); + + var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + + using var msEncrypt = new MemoryStream(); + using var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write); + using var swEncrypt = new StreamWriter(csEncrypt); + swEncrypt.Write(plainText); + swEncrypt.Flush(); + csEncrypt.FlushFinalBlock(); + + var encryptedPasswordByte = msEncrypt.ToArray(); + + var result = aesAlg.IV + .Concat(encryptedPasswordByte) + .ToArray(); + return result; + } + + public static string Decrypt(byte[] cipherText) + { + return cipherText.Length == 0 + ? "" + : DecryptSkippingHashInner(cipherText); + } + + public static string DecryptWithoutHash(byte[] cipherText) + { + return cipherText.Length == 0 + ? "" + : DecryptWithoutSkippingHashInner(cipherText, null); + } + + public static string Decrypt(byte[] cipherText, string key) + { + ValidateKey(key); + return cipherText.Length == 0 + ? "" + : DecryptSkippingHashInner(cipherText, key); + } + + public static string DecryptWithoutHash(byte[] cipherText, string key) + { + ValidateKey(key); + return cipherText.Length == 0 + ? "" + : DecryptWithoutSkippingHashInner(cipherText, key); + } + + public static void Decrypt(Stream inputStream, Stream outputStream, string? key = null) + { + key ??= Key; + ValidateKey(key); + + var iv = new byte[IvSize]; + if (inputStream.Read(iv, 0, IvSize) != IvSize) + { + throw new ArgumentException("Input stream does not contain a complete IV."); + } + + using var aesAlg = Aes.Create(); + aesAlg.KeySize = KeySize; + aesAlg.Padding = PaddingMode.PKCS7; + aesAlg.Key = Convert.FromBase64String(key); + aesAlg.IV = iv; + + using var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); + using var cryptoStream = new CryptoStream(inputStream, decryptor, CryptoStreamMode.Read, true); + cryptoStream.CopyTo(outputStream); + } + + private static string DecryptWithoutSkippingHashInner(byte[] cipherText, string? key) + { + key ??= Key; + if (cipherText.Length == 0) + { + return ""; + } + + ArgumentNullException.ThrowIfNull(key); + + var iv = cipherText.Take(IvSize) + .ToArray(); + var encrypted = cipherText.Skip(IvSize) + .ToArray(); + + using var aesAlg = Aes.Create(); + aesAlg.KeySize = KeySize; + aesAlg.Padding = PaddingMode.PKCS7; + aesAlg.Key = Convert.FromBase64String(key); + aesAlg.IV = iv; + + var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); + + using var msDecrypt = new MemoryStream(encrypted); + using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read); + using var srDecrypt = new StreamReader(csDecrypt); + return srDecrypt.ReadToEnd(); + } + + internal static void RegisterKey(string key) + { + ValidateKey(key); + Key = key; + } + + private static string DecryptSkippingHashInner(IEnumerable cipherTextWithHash, string? key = null) + { + key ??= Key; + var cipherText = cipherTextWithHash.Skip(HashSize) + .ToArray(); + return DecryptWithoutSkippingHashInner(cipherText, key); + } + + private static void ValidateKey([NotNull] string? key) + { + if (string.IsNullOrEmpty(key) || !IsBase64String(key) || Convert.FromBase64String(key) + .Length != 32) + { + throw new ArgumentException("Invalid key."); + } + } + + private static bool IsBase64String(string s) + { + var buffer = new Span(new byte[s.Length]); + return Convert.TryFromBase64String(s, buffer, out _); + } +} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Helpers/Argon2Id.cs b/src/Pandatech.Crypto/Helpers/Argon2Id.cs new file mode 100644 index 0000000..d5d7896 --- /dev/null +++ b/src/Pandatech.Crypto/Helpers/Argon2Id.cs @@ -0,0 +1,73 @@ +using System.Text; +using Konscious.Security.Cryptography; + +namespace Pandatech.Crypto.Helpers; + +public static class Argon2Id +{ + internal static int SaltSize { get; private set; } = 16; + internal static int DegreeOfParallelism { get; private set; } = 8; + internal static int Iterations { get; private set; } = 5; + internal static int MemorySize { get; private set; } = 128 * 1024; // 128 MB + + + public static byte[] HashPassword(string password) + { + var salt = Random.GenerateBytes(SaltSize); + return HashPassword(password, salt); + } + + private static byte[] HashPassword(string password, byte[] salt) + { + using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)) + { + Salt = salt, + DegreeOfParallelism = DegreeOfParallelism, + Iterations = Iterations, + MemorySize = MemorySize + }; + + var result = salt.Concat(argon2.GetBytes(32)) + .ToArray(); + + return result; + } + + public static bool VerifyHash(string password, byte[] passwordHash) + { + if (passwordHash.Length <= SaltSize) + { + throw new ArgumentException($"Hash must be at least {SaltSize} bytes.", nameof(passwordHash)); + } + + var salt = passwordHash.Take(SaltSize) + .ToArray(); + + var newHash = HashPassword(password, salt); + return ConstantTimeComparison(passwordHash, newHash); + } + + private static bool ConstantTimeComparison(ReadOnlySpan a, ReadOnlySpan b) + { + if (a.Length != b.Length) + { + return false; + } + + var diff = 0; + for (var i = 0; i < a.Length; i++) + { + diff |= a[i] ^ b[i]; + } + + return diff == 0; + } + + internal static void Configure(Argon2IdOptions options) + { + SaltSize = options.SaltSize; + DegreeOfParallelism = options.DegreeOfParallelism; + Iterations = options.Iterations; + MemorySize = options.MemorySize; + } +} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Helpers/GZip.cs b/src/Pandatech.Crypto/Helpers/GZip.cs new file mode 100644 index 0000000..ac4f659 --- /dev/null +++ b/src/Pandatech.Crypto/Helpers/GZip.cs @@ -0,0 +1,88 @@ +using System.IO.Compression; +using System.Text; +using System.Text.Json; + +namespace Pandatech.Crypto.Helpers; + +public static class GZip +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public static void Compress(Stream inputStream, Stream outputStream) + { + using var gzipStream = new GZipStream(outputStream, CompressionMode.Compress, true); + inputStream.CopyTo(gzipStream); + } + + // New method to decompress data directly from one stream to another + public static void Decompress(Stream inputStream, Stream outputStream) + { + using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress, true); + gzipStream.CopyTo(outputStream); + } + + public static byte[] Compress(T obj) + { + var jsonString = JsonSerializer.Serialize(obj, JsonSerializerOptions); + return Compress(jsonString); + } + + public static byte[] Compress(string data) + { + using var memoryStream = new MemoryStream(); + using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) + { + using (var writer = new StreamWriter(gzipStream, Encoding.UTF8)) + { + writer.Write(data); + } + } + + var compressedData = memoryStream.ToArray(); + + return compressedData; + } + + public static byte[] Compress(byte[] data) + { + using var memoryStream = new MemoryStream(); + using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) + { + gzipStream.Write(data, 0, data.Length); + } + + return memoryStream.ToArray(); + } + + public static T? Decompress(byte[] compressedData) + { + var decompressed = Decompress(compressedData); + var jsonString = Encoding.UTF8.GetString(decompressed); + return JsonSerializer.Deserialize(jsonString, JsonSerializerOptions); + } + + public static T? Decompress(string compressedData) + { + var decompressed = Decompress(compressedData); + var jsonString = Encoding.UTF8.GetString(decompressed); + return JsonSerializer.Deserialize(jsonString, JsonSerializerOptions); + } + + public static byte[] Decompress(string compressedBase64) + { + var compressedData = Convert.FromBase64String(compressedBase64); + return Decompress(compressedData); + } + + public static byte[] Decompress(byte[] data) + { + using var compressedStream = new MemoryStream(data); + using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + using var reader = new StreamReader(gzipStream, Encoding.UTF8); + var decompressedString = reader.ReadToEnd(); + return Encoding.UTF8.GetBytes(decompressedString); + } +} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Helpers/Mask.cs b/src/Pandatech.Crypto/Helpers/Mask.cs new file mode 100644 index 0000000..4392403 --- /dev/null +++ b/src/Pandatech.Crypto/Helpers/Mask.cs @@ -0,0 +1,34 @@ +using RegexBox; + +namespace Pandatech.Crypto.Helpers; + +public static class Mask +{ + public static string MaskEmail(this string email) + { + if (!PandaValidator.IsEmail(email)) + { + throw new ArgumentException("Invalid email address", nameof(email)); + } + + var parts = email.Split('@'); + var localPart = parts[0]; + var domainPart = parts[1]; + + var maskedLocalPart = + localPart.Length <= 2 ? localPart : localPart[..2] + new string('*', localPart.Length - 2); + return $"{maskedLocalPart}@{domainPart}"; + } + + public static string MaskPhoneNumber(this string phoneNumber) + { + if (string.IsNullOrEmpty(phoneNumber)) + { + throw new ArgumentException("Invalid phone number", nameof(phoneNumber)); + } + + return phoneNumber.Length <= 4 + ? phoneNumber + : string.Concat(new string('*', phoneNumber.Length - 4), phoneNumber.AsSpan(phoneNumber.Length - 4)); + } +} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Helpers/Password.cs b/src/Pandatech.Crypto/Helpers/Password.cs new file mode 100644 index 0000000..0061276 --- /dev/null +++ b/src/Pandatech.Crypto/Helpers/Password.cs @@ -0,0 +1,149 @@ +namespace Pandatech.Crypto.Helpers; + +public static class Password +{ + private const string UppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private const string LowercaseChars = "abcdefghijklmnopqrstuvwxyz"; + private const string DigitChars = "0123456789"; + private const string SpecialChars = "!@$*()-_=+[]{}|;:,."; + + public static string GenerateRandom(int length, + bool includeUppercase, + bool includeLowercase, + bool includeDigits, + bool includeSpecialChars) + { + var typesCount = ValidateInput(length, includeUppercase, includeLowercase, includeDigits, includeSpecialChars); + + var charSet = ""; + if (includeUppercase) + { + charSet += UppercaseChars; + } + + if (includeLowercase) + { + charSet += LowercaseChars; + } + + if (includeDigits) + { + charSet += DigitChars; + } + + if (includeSpecialChars) + { + charSet += SpecialChars; + } + + + var buffer = Random.GenerateBytes(length - typesCount); + var requiredBuffer = Random.GenerateBytes(typesCount); + + var password = new char[length]; + for (var i = 0; i < buffer.Length; i++) + { + var index = buffer[i] % charSet.Length; + password[i] = charSet[index]; + } + + var bufferIndex = 0; + + if (includeUppercase) + { + var index = requiredBuffer[bufferIndex++] % UppercaseChars.Length; + password[buffer.Length + bufferIndex - 1] = UppercaseChars[index]; + } + + if (includeLowercase) + { + var index = requiredBuffer[bufferIndex++] % LowercaseChars.Length; + password[buffer.Length + bufferIndex - 1] = LowercaseChars[index]; + } + + if (includeDigits) + { + var index = requiredBuffer[bufferIndex++] % DigitChars.Length; + password[buffer.Length + bufferIndex - 1] = DigitChars[index]; + } + + if (includeSpecialChars) + { + var index = requiredBuffer[bufferIndex++] % SpecialChars.Length; + password[buffer.Length + bufferIndex - 1] = SpecialChars[index]; + } + + return ShuffleString(password); + } + + public static bool Validate(string password, + int minLength, + bool requireUppercase, + bool requireLowercase, + bool requireDigits, + bool requireSpecialChars) + { + if (password.Length < minLength) + { + return false; + } + + if (requireUppercase && !password.Any(char.IsUpper)) + { + return false; + } + + if (requireLowercase && !password.Any(char.IsLower)) + { + return false; + } + + if (requireDigits && !password.Any(char.IsDigit)) + { + return false; + } + + if (requireSpecialChars && !password.Any(c => SpecialChars.Contains(c))) + { + return false; + } + + return true; + } + + private static int ValidateInput(int length, + bool includeUppercase, + bool includeLowercase, + bool includeDigits, + bool includeSpecialChars) + { + var typesCount = (includeUppercase ? 1 : 0) + (includeLowercase ? 1 : 0) + (includeDigits ? 1 : 0) + + (includeSpecialChars ? 1 : 0); + + if (typesCount == 0) + { + throw new ArgumentException("At least one character set must be selected."); + } + + if (length < typesCount) + { + throw new ArgumentException($"Password length must be at least {typesCount}."); + } + + return typesCount; + } + + private static string ShuffleString(char[] array) + { + var n = array.Length; + var randomBuffer = Random.GenerateBytes(n); + + for (var i = n - 1; i >= 1; i--) + { + var j = randomBuffer[i] % (i + 1); + (array[i], array[j]) = (array[j], array[i]); + } + + return new string(array); + } +} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Helpers/Random.cs b/src/Pandatech.Crypto/Helpers/Random.cs new file mode 100644 index 0000000..97393e5 --- /dev/null +++ b/src/Pandatech.Crypto/Helpers/Random.cs @@ -0,0 +1,60 @@ +using System.Security.Cryptography; + +namespace Pandatech.Crypto.Helpers; + +public static class Random +{ + public static byte[] GenerateBytes(int length) + { + using var rng = RandomNumberGenerator.Create(); + var buffer = new byte[length]; + rng.GetBytes(buffer); + return buffer; + } + + public static string GenerateAes256KeyString() + { + using var rng = RandomNumberGenerator.Create(); + var buffer = new byte[32]; + rng.GetBytes(buffer); + return Convert.ToBase64String(buffer); + } + + public static long GenerateIdWithVariableSequence(long previousId, int approximateSequenceVariability = 100) + { + var minimumRandRange = approximateSequenceVariability / 25; + var random = System.Random.Shared.NextInt64(minimumRandRange, approximateSequenceVariability + 1); + + return previousId + random; + } + + public static string GenerateSecureToken() + { + const int length = 32; // 32 bytes = 256 bits + var bytes = new byte[length]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(bytes); + } + + return Convert.ToBase64String(bytes) + .Replace("+", "-") // Make URL-safe + .Replace("/", "_") // Make URL-safe + .TrimEnd('='); // Remove padding + } + + public static string GenerateShortUniqueString() + { + const int length = 12; // 12 bytes = 96 bits + var bytes = new byte[length]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(bytes); + } + + return Convert.ToBase64String(bytes) + .Replace("+", "-") // Make URL-safe + .Replace("/", "_") // Make URL-safe + .TrimEnd('='); // Remove padding + } +} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Sha2.cs b/src/Pandatech.Crypto/Helpers/Sha2.cs similarity index 81% rename from src/Pandatech.Crypto/Sha2.cs rename to src/Pandatech.Crypto/Helpers/Sha2.cs index 4f0e9e6..6c3b6dc 100644 --- a/src/Pandatech.Crypto/Sha2.cs +++ b/src/Pandatech.Crypto/Helpers/Sha2.cs @@ -1,7 +1,7 @@ using System.Security.Cryptography; using System.Text; -namespace Pandatech.Crypto; +namespace Pandatech.Crypto.Helpers; public static class Sha2 { @@ -16,9 +16,11 @@ public static byte[] ComputeHmacSha256(byte[] key, params string[] messages) public static string GetHmacSha256Hex(byte[] key, params string[] messages) { var hash = ComputeHmacSha256(key, messages); - return BitConverter.ToString(hash).Replace("-", "").ToLower(); + return BitConverter.ToString(hash) + .Replace("-", "") + .ToLower(); } - + public static string GetHmacSha256Base64(byte[] key, params string[] messages) { var hash = ComputeHmacSha256(key, messages); diff --git a/src/Pandatech.Crypto/Helpers/Sha3.cs b/src/Pandatech.Crypto/Helpers/Sha3.cs new file mode 100644 index 0000000..6edffe2 --- /dev/null +++ b/src/Pandatech.Crypto/Helpers/Sha3.cs @@ -0,0 +1,49 @@ +using System.Text; +using Org.BouncyCastle.Crypto.Digests; + +namespace Pandatech.Crypto.Helpers; + +public static class Sha3 +{ + public static byte[] Hash(string data) + { + var bytes = Encoding.UTF8.GetBytes(data); + + var digest = new KeccakDigest(512); + digest.BlockUpdate(bytes, 0, bytes.Length); + + var result = new byte[digest.GetDigestSize()]; + digest.DoFinal(result, 0); + + return result; + } + + public static byte[] Hash(byte[] bytes) + { + var digest = new KeccakDigest(512); + digest.BlockUpdate(bytes, 0, bytes.Length); + + var result = new byte[digest.GetDigestSize()]; + digest.DoFinal(result, 0); + + return result; + } + + public static bool VerifyHash(string data, byte[] hash) + { + var newHash = Hash(data); + + return ConstantTimeComparison(hash, newHash); + } + + private static bool ConstantTimeComparison(IReadOnlyList a, IReadOnlyList b) + { + var diff = (ushort)a.Count ^ (ushort)b.Count; + for (var i = 0; i < a.Count && i < b.Count; i++) + { + diff |= (ushort)(a[i] ^ b[i]); + } + + return diff == 0; + } +} \ No newline at end of file diff --git a/src/Pandatech.Crypto/HostBuilderExtensions.cs b/src/Pandatech.Crypto/HostBuilderExtensions.cs deleted file mode 100644 index a57971a..0000000 --- a/src/Pandatech.Crypto/HostBuilderExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Pandatech.Crypto; - -public static class HostBuilderExtensions -{ - public static IServiceCollection AddPandatechCryptoAes256(this IServiceCollection services, Action configure) - { - var options = new Aes256Options(); - configure(options); - ValidateKey(options.Key); - services.AddSingleton(options); - services.AddSingleton(); - return services; - - } - - public static IServiceCollection AddPandatechCryptoArgon2Id(this IServiceCollection services, Action configure) - { - var options = new Argon2IdOptions(); - configure(options); - services.AddSingleton(options); - services.AddSingleton(); - return services; - - } - - private static void ValidateKey(string key) - { - if (string.IsNullOrEmpty(key) || !IsBase64String(key) || Convert.FromBase64String(key).Length != 32) - throw new ArgumentException("Invalid key."); - } - - public static IServiceCollection AddPandatechCryptoArgon2Id(this IServiceCollection services) - { - var options = new Argon2IdOptions(); - services.AddSingleton(options); - services.AddSingleton(); - return services; - } - - private static bool IsBase64String(string s) - { - var buffer = new Span(new byte[s.Length]); - return Convert.TryFromBase64String(s, buffer, out _); - } -} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Mask.cs b/src/Pandatech.Crypto/Mask.cs deleted file mode 100644 index 970a321..0000000 --- a/src/Pandatech.Crypto/Mask.cs +++ /dev/null @@ -1,34 +0,0 @@ -using RegexBox; - -namespace Pandatech.Crypto; - -public static class Mask -{ - public static string MaskEmail(this string email) - { - if (!PandaValidator.IsEmail(email)) - { - throw new ArgumentException("Invalid email address", nameof(email)); - } - - var parts = email.Split('@'); - var localPart = parts[0]; - var domainPart = parts[1]; - - var maskedLocalPart = - localPart.Length <= 2 ? localPart : localPart[..2] + new string('*', localPart.Length - 2); - return $"{maskedLocalPart}@{domainPart}"; - } - - public static string MaskPhoneNumber(this string phoneNumber) - { - if (string.IsNullOrEmpty(phoneNumber)) - { - throw new ArgumentException("Invalid phone number", nameof(phoneNumber)); - } - - return phoneNumber.Length <= 4 - ? phoneNumber - : string.Concat(new string('*', phoneNumber.Length - 4), phoneNumber.AsSpan(phoneNumber.Length - 4)); - } -} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Options.cs b/src/Pandatech.Crypto/Options.cs index 82cfa19..9a9ff0f 100644 --- a/src/Pandatech.Crypto/Options.cs +++ b/src/Pandatech.Crypto/Options.cs @@ -1,14 +1,11 @@ -namespace Pandatech.Crypto; +using Pandatech.Crypto.Helpers; -public class Aes256Options -{ - public string Key { get; set; } = null!; -} +namespace Pandatech.Crypto; public class Argon2IdOptions { - public int SaltSize { get; set; } = 16; - public int DegreeOfParallelism { get; set; } = 8; - public int Iterations { get; set; } = 5; - public int MemorySize { get; set; } = 128 * 1024; // 128 MB + public int SaltSize { get; set; } = Argon2Id.SaltSize; + public int DegreeOfParallelism { get; set; } = Argon2Id.DegreeOfParallelism; + public int Iterations { get; set; } = Argon2Id.Iterations; + public int MemorySize { get; set; } = Argon2Id.MemorySize; } \ No newline at end of file diff --git a/src/Pandatech.Crypto/Pandatech.Crypto.csproj b/src/Pandatech.Crypto/Pandatech.Crypto.csproj index 4090b94..a0af647 100644 --- a/src/Pandatech.Crypto/Pandatech.Crypto.csproj +++ b/src/Pandatech.Crypto/Pandatech.Crypto.csproj @@ -8,12 +8,12 @@ MIT pandatech.png Readme.md - 2.6.1 + 3.0.0 Pandatech.Crypto Pandatech, library, encryption, hash, algorythms, security PandaTech.Crypto is a .NET library simplifying common cryptograhic functions. https://github.com/PandaTechAM/be-lib-pandatech-crypto - Readme fix + Argon2id and Aes256 migrated from singleton service to static class. There will be huge breaking change but luckily it will be very easy to fix. @@ -22,10 +22,11 @@ - - - - + + + + + diff --git a/src/Pandatech.Crypto/Password.cs b/src/Pandatech.Crypto/Password.cs deleted file mode 100644 index 76244fb..0000000 --- a/src/Pandatech.Crypto/Password.cs +++ /dev/null @@ -1,128 +0,0 @@ -namespace Pandatech.Crypto; - -public static class Password -{ - private const string UppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - private const string LowercaseChars = "abcdefghijklmnopqrstuvwxyz"; - private const string DigitChars = "0123456789"; - private const string SpecialChars = "!@$*()-_=+[]{}|;:,."; - - public static string GenerateRandom(int length, bool includeUppercase, bool includeLowercase, bool includeDigits, - bool includeSpecialChars) - { - var typesCount = ValidateInput(length, includeUppercase, includeLowercase, includeDigits, includeSpecialChars); - - var charSet = ""; - if (includeUppercase) - charSet += UppercaseChars; - if (includeLowercase) - charSet += LowercaseChars; - if (includeDigits) - charSet += DigitChars; - if (includeSpecialChars) - charSet += SpecialChars; - - - var buffer = Random.GenerateBytes(length - typesCount); - var requiredBuffer = Random.GenerateBytes(typesCount); - - var password = new char[length]; - for (var i = 0; i < buffer.Length; i++) - { - var index = buffer[i] % charSet.Length; - password[i] = charSet[index]; - } - - var bufferIndex = 0; - - if (includeUppercase) - { - var index = requiredBuffer[bufferIndex++] % UppercaseChars.Length; - password[buffer.Length + bufferIndex - 1] = UppercaseChars[index]; - } - - if (includeLowercase) - { - var index = requiredBuffer[bufferIndex++] % LowercaseChars.Length; - password[buffer.Length + bufferIndex - 1] = LowercaseChars[index]; - } - - if (includeDigits) - { - var index = requiredBuffer[bufferIndex++] % DigitChars.Length; - password[buffer.Length + bufferIndex - 1] = DigitChars[index]; - } - - if (includeSpecialChars) - { - var index = requiredBuffer[bufferIndex++] % SpecialChars.Length; - password[buffer.Length + bufferIndex - 1] = SpecialChars[index]; - } - - return ShuffleString(password); - } - - public static bool Validate(string password, int minLength, bool requireUppercase, bool requireLowercase, - bool requireDigits, bool requireSpecialChars) - { - if (password.Length < minLength) - { - return false; - } - - if (requireUppercase && !password.Any(char.IsUpper)) - { - return false; - } - - if (requireLowercase && !password.Any(char.IsLower)) - { - return false; - } - - if (requireDigits && !password.Any(char.IsDigit)) - { - return false; - } - - if (requireSpecialChars && !password.Any(c => SpecialChars.Contains(c))) - { - return false; - } - - return true; - } - - private static int ValidateInput(int length, bool includeUppercase, bool includeLowercase, bool includeDigits, - bool includeSpecialChars) - { - var typesCount = (includeUppercase ? 1 : 0) + (includeLowercase ? 1 : 0) + (includeDigits ? 1 : 0) + - (includeSpecialChars ? 1 : 0); - - if (typesCount == 0) - { - throw new ArgumentException("At least one character set must be selected."); - } - - if (length < typesCount) - { - throw new ArgumentException($"Password length must be at least {typesCount}."); - } - - return typesCount; - } - - private static string ShuffleString(char[] array) - { - var n = array.Length; - var randomBuffer = Random.GenerateBytes(n); - - for (var i = n - 1; i >= 1; i--) - { - var j = randomBuffer[i] % (i + 1); - (array[i], array[j]) = (array[j], array[i]); - } - - return new string(array); - } -} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Random.cs b/src/Pandatech.Crypto/Random.cs deleted file mode 100644 index 5e7b7d8..0000000 --- a/src/Pandatech.Crypto/Random.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Security.Cryptography; - -namespace Pandatech.Crypto; - -public static class Random -{ - public static byte[] GenerateBytes(int length) - { - using var rng = RandomNumberGenerator.Create(); - var buffer = new byte[length]; - rng.GetBytes(buffer); - return buffer; - } - - public static string GenerateAes256KeyString() - { - using var rng = RandomNumberGenerator.Create(); - var buffer = new byte[32]; - rng.GetBytes(buffer); - return Convert.ToBase64String(buffer); - } - - public static long GenerateIdWithVariableSequence(long previousId, int approximateSequenceVariability = 100) - { - var minimumRandRange = approximateSequenceVariability / 25; - var random = System.Random.Shared.NextInt64(minimumRandRange, approximateSequenceVariability + 1); - - return previousId + random; - } - - public static string GenerateSecureToken() - { - const int length = 32; // 32 bytes = 256 bits - var bytes = new byte[length]; - using (var rng = RandomNumberGenerator.Create()) - { - rng.GetBytes(bytes); - } - return Convert.ToBase64String(bytes) - .Replace("+", "-") // Make URL-safe - .Replace("/", "_") // Make URL-safe - .TrimEnd('='); // Remove padding - } -} \ No newline at end of file diff --git a/src/Pandatech.Crypto/Sha3.cs b/src/Pandatech.Crypto/Sha3.cs deleted file mode 100644 index 22b6a14..0000000 --- a/src/Pandatech.Crypto/Sha3.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Text; -using Org.BouncyCastle.Crypto.Digests; - -namespace Pandatech.Crypto; - -public static class Sha3 -{ - public static byte[] Hash(string data) - { - var bytes = Encoding.UTF8.GetBytes(data); - - var digest = new KeccakDigest(512); - digest.BlockUpdate(bytes, 0, bytes.Length); - - var result = new byte[digest.GetDigestSize()]; - digest.DoFinal(result, 0); - - return result; - } - - public static byte[] Hash(byte[] bytes) - { - var digest = new KeccakDigest(512); - digest.BlockUpdate(bytes, 0, bytes.Length); - - var result = new byte[digest.GetDigestSize()]; - digest.DoFinal(result, 0); - - return result; - } - - public static bool VerifyHash(string data, byte[] hash) - { - var newHash = Hash(data); - - return ConstantTimeComparison(hash, newHash); - } - - private static bool ConstantTimeComparison(IReadOnlyList a, IReadOnlyList b) - { - var diff = (ushort)a.Count ^ (ushort)b.Count; - for (var i = 0; i < a.Count && i < b.Count; i++) - { - diff |= (ushort)(a[i] ^ b[i]); - } - - return diff == 0; - } -} \ No newline at end of file diff --git a/test/Pandatech.Crypto.Tests/Aes256Tests.cs b/test/Pandatech.Crypto.Tests/Aes256Tests.cs index c957629..87151c3 100644 --- a/test/Pandatech.Crypto.Tests/Aes256Tests.cs +++ b/test/Pandatech.Crypto.Tests/Aes256Tests.cs @@ -1,207 +1,184 @@ using System.Text; +using Pandatech.Crypto.Helpers; +using Random = Pandatech.Crypto.Helpers.Random; namespace Pandatech.Crypto.Tests; public class Aes256Tests { - [Fact] - public void EncryptDecryptWithHash_ShouldReturnOriginalString() - { - var aes256 = new Aes256(new Aes256Options()); - - var key = Random.GenerateAes256KeyString(); - const string original = "MySensitiveData"; - var encrypted = aes256.Encrypt(original, key); - var decrypted = aes256.Decrypt(encrypted, key); - - Assert.Equal(original, decrypted); - } - - [Fact] - public void EncryptDecryptWithoutHash_ShouldReturnOriginalString() - { - var aes256Options = new Aes256Options { Key = Random.GenerateAes256KeyString() }; - var aes256 = new Aes256(aes256Options); - const string original = "MySensitiveData"; - var encrypted = aes256.EncryptWithoutHash(original); - var decrypted = aes256.DecryptWithoutHash(encrypted); - - Assert.Equal(original, decrypted); - } - - [Fact] - public void EncryptWithHash_ShouldReturnByteArrayWithHash() - { - var aes256 = new Aes256(new Aes256Options()); - var key = Random.GenerateAes256KeyString(); - const string original = "MySensitiveData"; - var encryptedWithHash = aes256.Encrypt(original, key); - - Assert.NotNull(encryptedWithHash); - Assert.True(encryptedWithHash.Length > original.Length); - Assert.True(encryptedWithHash.Length > 64); - } - - [Fact] - public void EncryptAndHash_ShouldReturnByteArrayWithHash() - { - var aes256Options = new Aes256Options { Key = Random.GenerateAes256KeyString() }; - var aes256 = new Aes256(aes256Options); - const string original = "MySensitiveData"; - var encryptedWithHash = aes256.Encrypt(original); - - Assert.NotNull(encryptedWithHash); - Assert.True(encryptedWithHash.Length > original.Length); - Assert.True(encryptedWithHash.Length > 64); - } - - [Fact] - public void DecryptWithParameterAndIgnoringHash_ShouldReturnOriginalString() - { - var aes256 = new Aes256(new Aes256Options()); - var key = Random.GenerateAes256KeyString(); - const string original = "MySensitiveData"; - var encryptedWithHash = aes256.Encrypt(original, key); - var decrypted = aes256.Decrypt(encryptedWithHash, key); - - Assert.Equal(original, decrypted); - } - - [Fact] - public void DecryptWithoutParameterAndIgnoringHash_ShouldReturnOriginalString() - { - var aes256Options = new Aes256Options { Key = Random.GenerateAes256KeyString() }; - var aes256 = new Aes256(aes256Options); - const string original = "MySensitiveData"; - var encryptedWithHash = aes256.Encrypt(original); - var decrypted = aes256.Decrypt(encryptedWithHash); - - Assert.Equal(original, decrypted); - } - - [Fact] - public void DecryptIgnoringHashWithInvalidData_ShouldThrowException() - { - var aes256 = new Aes256(new Aes256Options()); - const string invalidKey = "InvalidKey"; - var invalidData = new byte[50]; - - Assert.Throws(() => aes256.Decrypt(invalidData, invalidKey)); - } - - [Fact] - public void EncryptDecryptWithInvalidKey_ShouldThrowException() - { - var aes256 = new Aes256(new Aes256Options()); - const string invalidKey = "InvalidKey"; - const string original = "MySensitiveData"; - - Assert.Throws(() => aes256.Encrypt(original, invalidKey)); - } - - [Fact] - public void EncryptDecryptWithShortKey_ShouldThrowException() - { - var aes256 = new Aes256(new Aes256Options()); - var shortKey = Convert.ToBase64String(new byte[15]); // Less than 256 bits - const string original = "MySensitiveData"; - - Assert.Throws(() => aes256.Encrypt(original, shortKey)); - Assert.Throws(() => aes256.Decrypt([], shortKey)); - } - - [Fact] - public void EncryptDecryptWithNullCipher_ShouldReturnEmptyString() - { - var aes256 = new Aes256(new Aes256Options()); - var key = Random.GenerateAes256KeyString(); - - Assert.Equal("", aes256.Decrypt([], key)); - } - - [Fact] - public void GenerateAes256KeyIsValidInLoop() - { - for (var i = 0; i < 1_000; i++) - { - var aes256 = new Aes256(new Aes256Options() - { - Key = Random.GenerateAes256KeyString() - }); - var encrypt = aes256.Encrypt("MySensitiveData"); - var decrypt = aes256.Decrypt(encrypt); - Assert.Equal("MySensitiveData", decrypt); - } - } - - [Fact] - public void EncryptDecryptStream_ShouldReturnOriginalData() - { - // Arrange - var aes256Options = new Aes256Options { Key = Random.GenerateAes256KeyString() }; - var aes256 = new Aes256(aes256Options); - const string originalData = "MySensitiveData"; - var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(originalData)); - var outputStream = new MemoryStream(); - - // Act - aes256.Encrypt(inputStream, outputStream, aes256Options.Key); - outputStream.Seek(0, SeekOrigin.Begin); - - var resultStream = new MemoryStream(); - aes256.Decrypt(outputStream, resultStream, aes256Options.Key); - resultStream.Seek(0, SeekOrigin.Begin); - var decryptedData = new StreamReader(resultStream).ReadToEnd(); - - // Assert - Assert.Equal(originalData, decryptedData); - } - - [Fact] - public void EncryptDecryptStreamWithEmptyContent_ShouldHandleGracefully() - { - // Arrange - var aes256Options = new Aes256Options { Key = Random.GenerateAes256KeyString() }; - var aes256 = new Aes256(aes256Options); - var inputStream = new MemoryStream(); - var outputStream = new MemoryStream(); - - // Act - aes256.Encrypt(inputStream, outputStream, aes256Options.Key); - outputStream.Seek(0, SeekOrigin.Begin); // Reset the position for reading. - - var resultStream = new MemoryStream(); - aes256.Decrypt(outputStream, resultStream, aes256Options.Key); - resultStream.Seek(0, SeekOrigin.Begin); - var decryptedData = new StreamReader(resultStream).ReadToEnd(); - - // Assert - Assert.Empty(decryptedData); - } - - [Fact] - public void EncryptStreamWithInvalidKey_ShouldThrowException() - { - // Arrange - var aes256 = new Aes256(new Aes256Options()); - const string invalidKey = "InvalidKey"; - var inputStream = new MemoryStream(Encoding.UTF8.GetBytes("MySensitiveData")); - var outputStream = new MemoryStream(); - - // Act & Assert - Assert.Throws(() => aes256.Encrypt(inputStream, outputStream, invalidKey)); - } - - [Fact] - public void DecryptStreamWithInvalidKey_ShouldThrowException() - { - // Arrange - var aes256 = new Aes256(new Aes256Options()); - const string invalidKey = "InvalidKey"; - var inputStream = new MemoryStream(); - var outputStream = new MemoryStream(); - - // Act & Assert - Assert.Throws(() => aes256.Decrypt(inputStream, outputStream, invalidKey)); - } + [Fact] + public void EncryptDecryptWithHash_ShouldReturnOriginalString() + { + var key = Random.GenerateAes256KeyString(); + const string original = "MySensitiveData"; + var encrypted = Aes256.Encrypt(original, key); + var decrypted = Aes256.Decrypt(encrypted, key); + + Assert.Equal(original, decrypted); + } + + [Fact] + public void EncryptDecryptWithoutHash_ShouldReturnOriginalString() + { + Aes256.RegisterKey(Random.GenerateAes256KeyString()); + const string original = "MySensitiveData"; + var encrypted = Aes256.EncryptWithoutHash(original); + var decrypted = Aes256.DecryptWithoutHash(encrypted); + + Assert.Equal(original, decrypted); + } + + [Fact] + public void EncryptWithHash_ShouldReturnByteArrayWithHash() + { + var key = Random.GenerateAes256KeyString(); + const string original = "MySensitiveData"; + var encryptedWithHash = Aes256.Encrypt(original, key); + + Assert.NotNull(encryptedWithHash); + Assert.True(encryptedWithHash.Length > original.Length); + Assert.True(encryptedWithHash.Length > 64); + } + + [Fact] + public void EncryptAndHash_ShouldReturnByteArrayWithHash() + { + Aes256.RegisterKey(Random.GenerateAes256KeyString()); + + const string original = "MySensitiveData"; + var encryptedWithHash = Aes256.Encrypt(original); + + Assert.NotNull(encryptedWithHash); + Assert.True(encryptedWithHash.Length > original.Length); + Assert.True(encryptedWithHash.Length > 64); + } + + [Fact] + public void DecryptWithParameterAndIgnoringHash_ShouldReturnOriginalString() + { + var key = Random.GenerateAes256KeyString(); + const string original = "MySensitiveData"; + var encryptedWithHash = Aes256.Encrypt(original, key); + var decrypted = Aes256.Decrypt(encryptedWithHash, key); + + Assert.Equal(original, decrypted); + } + + [Fact] + public void DecryptWithoutParameterAndIgnoringHash_ShouldReturnOriginalString() + { + Aes256.RegisterKey(Random.GenerateAes256KeyString()); + + const string original = "MySensitiveData"; + var encryptedWithHash = Aes256.Encrypt(original); + var decrypted = Aes256.Decrypt(encryptedWithHash); + + Assert.Equal(original, decrypted); + } + + [Fact] + public void DecryptIgnoringHashWithInvalidData_ShouldThrowException() + { + const string invalidKey = "InvalidKey"; + var invalidData = new byte[50]; + + Assert.Throws(() => Aes256.Decrypt(invalidData, invalidKey)); + } + + [Fact] + public void EncryptDecryptWithInvalidKey_ShouldThrowException() + { + const string invalidKey = "InvalidKey"; + const string original = "MySensitiveData"; + + Assert.Throws(() => Aes256.Encrypt(original, invalidKey)); + } + + [Fact] + public void EncryptDecryptWithShortKey_ShouldThrowException() + { + var shortKey = Convert.ToBase64String(new byte[15]); // Less than 256 bits + const string original = "MySensitiveData"; + + Assert.Throws(() => Aes256.Encrypt(original, shortKey)); + Assert.Throws(() => Aes256.Decrypt([], shortKey)); + } + + [Fact] + public void EncryptDecryptWithNullCipher_ShouldReturnEmptyString() + { + var key = Random.GenerateAes256KeyString(); + + Assert.Equal("", Aes256.Decrypt([], key)); + } + + + [Fact] + public void EncryptDecryptStream_ShouldReturnOriginalData() + { + // Arrange + Aes256.RegisterKey(Random.GenerateAes256KeyString()); + + const string originalData = "MySensitiveData"; + var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(originalData)); + var outputStream = new MemoryStream(); + + // Act + Aes256.Encrypt(inputStream, outputStream); + outputStream.Seek(0, SeekOrigin.Begin); + + var resultStream = new MemoryStream(); + Aes256.Decrypt(outputStream, resultStream); + resultStream.Seek(0, SeekOrigin.Begin); + var decryptedData = new StreamReader(resultStream).ReadToEnd(); + + // Assert + Assert.Equal(originalData, decryptedData); + } + + [Fact] + public void EncryptDecryptStreamWithEmptyContent_ShouldHandleGracefully() + { + // Arrange + + var key = Random.GenerateAes256KeyString(); + var inputStream = new MemoryStream(); + var outputStream = new MemoryStream(); + + // Act + Aes256.Encrypt(inputStream, outputStream, key); + outputStream.Seek(0, SeekOrigin.Begin); // Reset the position for reading. + + var resultStream = new MemoryStream(); + Aes256.Decrypt(outputStream, resultStream, key); + resultStream.Seek(0, SeekOrigin.Begin); + var decryptedData = new StreamReader(resultStream).ReadToEnd(); + + // Assert + Assert.Empty(decryptedData); + } + + [Fact] + public void EncryptStreamWithInvalidKey_ShouldThrowException() + { + // Arrange + const string invalidKey = "InvalidKey"; + var inputStream = new MemoryStream(Encoding.UTF8.GetBytes("MySensitiveData")); + var outputStream = new MemoryStream(); + + // Act & Assert + Assert.Throws(() => Aes256.Encrypt(inputStream, outputStream, invalidKey)); + } + + [Fact] + public void DecryptStreamWithInvalidKey_ShouldThrowException() + { + // Arrange + const string invalidKey = "InvalidKey"; + var inputStream = new MemoryStream(); + var outputStream = new MemoryStream(); + + // Act & Assert + Assert.Throws(() => Aes256.Decrypt(inputStream, outputStream, invalidKey)); + } } \ No newline at end of file diff --git a/test/Pandatech.Crypto.Tests/Argon2IdTests.cs b/test/Pandatech.Crypto.Tests/Argon2IdTests.cs index 95beee1..f6cfc0f 100644 --- a/test/Pandatech.Crypto.Tests/Argon2IdTests.cs +++ b/test/Pandatech.Crypto.Tests/Argon2IdTests.cs @@ -1,60 +1,58 @@ -namespace Pandatech.Crypto.Tests; +using Pandatech.Crypto.Helpers; + +namespace Pandatech.Crypto.Tests; public class Argon2IdTests { - [Fact] - public void HashVerify_ShouldFailForDifferentArgonConfigs() - { - var argon2Id = new Argon2Id(); - var argon2Id2 = new Argon2Id(new Argon2IdOptions { SaltSize = 16, MemorySize = 128, DegreeOfParallelism = 1, Iterations = 1 }); - var password = Password.GenerateRandom(32, true, true, true, true); - var hash = argon2Id.HashPassword(password); - Assert.False(argon2Id2.VerifyHash(password, hash)); - } - - [Fact] - public void HashVerify_ShouldBeValid() - { - var argon2Id = new Argon2Id(); - - var password = Password.GenerateRandom(32, true, true, true, true); - var hash = argon2Id.HashPassword(password); - - Assert.True(argon2Id.VerifyHash(password, hash)); - } - - [Fact] - public void HashVerify_InvalidPassword_ShouldBeInvalid() - { - var argon2Id = new Argon2Id(); - var password = Password.GenerateRandom(32, true, true, true, true); - var hash = argon2Id.HashPassword(password); - Assert.False(argon2Id.VerifyHash("SomePassword", hash)); - } - - [Fact] - public void DifferentPasswords_ShouldHaveDifferentHashes() - { - var argon2Id = new Argon2Id(); - var password1 = Password.GenerateRandom(32, true, true, true, true); - var password2 = Password.GenerateRandom(32, true, true, true, true); - var hash1 = argon2Id.HashPassword(password1); - var hash2 = argon2Id.HashPassword(password2); - - Assert.NotEqual(hash1, hash2); - } - - [Fact] - public void HashPassword_EmptyPassword_ShouldThrowException() - { - var argon2Id = new Argon2Id(); - Assert.Throws(() => argon2Id.HashPassword("")); - } - - [Fact] - public void VerifyHash_NullHash_ShouldThrowException() - { - var argon2Id = new Argon2Id(); - Assert.Throws(() => argon2Id.VerifyHash("password", null!)); - } + [Fact] + public void HashVerify_ShouldFailForDifferentArgonConfigs() + { + var password = Password.GenerateRandom(32, true, true, true, true); + var options = new Argon2IdOptions + { + SaltSize = 16, + DegreeOfParallelism = 3, + Iterations = 3, + MemorySize = 1024 + }; + Argon2Id.Configure(options); + var hash = Argon2Id.HashPassword(password); + options.DegreeOfParallelism = 4; + Argon2Id.Configure(options); + Assert.False(Argon2Id.VerifyHash(password, hash)); + } + + [Fact] + public void HashVerify_ShouldBeValid() + { + var password = Password.GenerateRandom(32, true, true, true, true); + var hash = Argon2Id.HashPassword(password); + + Assert.True(Argon2Id.VerifyHash(password, hash)); + } + + [Fact] + public void HashVerify_InvalidPassword_ShouldBeInvalid() + { + var password = Password.GenerateRandom(32, true, true, true, true); + var hash = Argon2Id.HashPassword(password); + Assert.False(Argon2Id.VerifyHash("SomePassword", hash)); + } + + [Fact] + public void DifferentPasswords_ShouldHaveDifferentHashes() + { + var password1 = Password.GenerateRandom(32, true, true, true, true); + var password2 = Password.GenerateRandom(32, true, true, true, true); + var hash1 = Argon2Id.HashPassword(password1); + var hash2 = Argon2Id.HashPassword(password2); + + Assert.NotEqual(hash1, hash2); + } + + [Fact] + public void HashPassword_EmptyPassword_ShouldThrowException() + { + Assert.Throws(() => Argon2Id.HashPassword("")); + } } \ No newline at end of file diff --git a/test/Pandatech.Crypto.Tests/GZipTests.cs b/test/Pandatech.Crypto.Tests/GZipTests.cs index 23ba0d1..07e57e3 100644 --- a/test/Pandatech.Crypto.Tests/GZipTests.cs +++ b/test/Pandatech.Crypto.Tests/GZipTests.cs @@ -1,211 +1,218 @@ using System.Text; +using Pandatech.Crypto.Helpers; namespace Pandatech.Crypto.Tests; public class GZipTests { - private class TestClass - { - public int SomeLongId { get; init; } - public string? FullName { get; init; } - } - - [Fact] - public void CompressDecompressStream_ShouldReturnOriginalData() - { - // Arrange - var originalData = "MySensitiveData"; - var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(originalData)); - var compressedStream = new MemoryStream(); - var decompressedStream = new MemoryStream(); - - // Act - Compress - GZip.Compress(inputStream, compressedStream); - compressedStream.Seek(0, SeekOrigin.Begin); // Reset stream position for reading - - // Act - Decompress - GZip.Decompress(compressedStream, decompressedStream); - decompressedStream.Seek(0, SeekOrigin.Begin); // Reset stream position for reading - var resultData = new StreamReader(decompressedStream).ReadToEnd(); - - // Assert - Assert.Equal(originalData, resultData); - } - - [Fact] - public void CompressStream_ShouldReduceSizeForCompressibleData() - { - // Arrange - var originalData = new string('a', 1024); // Highly compressible data - var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(originalData)); - var compressedStream = new MemoryStream(); - - // Act - GZip.Compress(inputStream, compressedStream); - - // Assert - Assert.True(compressedStream.Length < inputStream.Length); - } - - [Fact] - public void DecompressStream_WithCorruptedData_ShouldThrow() - { - // Arrange - var corruptedData = new byte[] { 0x0, 0x1, 0x2, 0x3 }; // Not valid compressed data - var inputStream = new MemoryStream(corruptedData); - var decompressedStream = new MemoryStream(); - - // Act & Assert - Assert.Throws(() => GZip.Decompress(inputStream, decompressedStream)); - } - - [Fact] - public void CompressDecompressEmptyStream_ShouldHandleGracefully() - { - // Arrange - var emptyStream = new MemoryStream(); - var compressedStream = new MemoryStream(); - var decompressedStream = new MemoryStream(); - - // Act - GZip.Compress(emptyStream, compressedStream); - compressedStream.Seek(0, SeekOrigin.Begin); // Reset the compressed stream position for reading - - // Act - GZip.Decompress(compressedStream, decompressedStream); - decompressedStream.Seek(0, SeekOrigin.Begin); // Reset the decompressed stream position for reading - var resultData = new StreamReader(decompressedStream).ReadToEnd(); - - // Assert - Assert.Empty(resultData); - } - - [Fact] - public void CompressAndDecompress_ShouldReturnOriginalObject() - { - var originalObject = new TestClass - { - SomeLongId = 1, - FullName = "Test" - }; - - // Act - var compressedData = GZip.Compress(originalObject); - var decompressedObject = GZip.Decompress(compressedData); - - // Assert - Assert.NotNull(decompressedObject); - Assert.Equal(originalObject.SomeLongId, decompressedObject.SomeLongId); - Assert.Equal(originalObject.FullName, decompressedObject.FullName); - } - - [Fact] - public void CompressAndDecompress_ShouldReturnOriginalObject2() - { - var originalObject = new TestClass - { - SomeLongId = 1, - FullName = "Test" - }; - - // Act - var compressedData = GZip.Compress(originalObject); - var stringData = Convert.ToBase64String(compressedData); - var decompressedObject = GZip.Decompress(stringData); - - // Assert - Assert.NotNull(decompressedObject); - Assert.Equal(originalObject.SomeLongId, decompressedObject.SomeLongId); - Assert.Equal(originalObject.FullName, decompressedObject.FullName); - } - - [Fact] - public void Decompress_WithInvalidData_ShouldReturnNull() - { - // Arrange - var invalidData = Encoding.UTF8.GetBytes("Invalid compressed data"); - - // Act & Assert - var exception = Record.Exception(() => GZip.Decompress(invalidData)); - Assert.NotNull(exception); - Assert.IsType(exception); - } - - - [Fact] - public void CompressAndDecompress_String_ReturnsOriginalData() - { - // Arrange - const string input = "Hello, world!"; - - // Act - var compressed = GZip.Compress(input); - var decompressedBytes = GZip.Decompress(compressed); - - // Convert decompressed bytes back to string - var decompressedString = Encoding.UTF8.GetString(decompressedBytes); - - // Assert - Assert.Equal(input, decompressedString); - } - - [Fact] - public void Decompress_Base64_ReturnsOriginalData() - { - // Arrange - const string input = "Hello, world!"; - var compressed = GZip.Compress(input); - var compressedBase64 = Convert.ToBase64String(compressed); - - // Act - var result = GZip.Decompress(compressedBase64); - var resultString = Encoding.UTF8.GetString(result); - - // Assert - Assert.Equal(input, resultString); - } - - [Fact] - public void Compress_And_Decompress_Byte_Array_ReturnsOriginalData() - { - // Arrange - var input = "Hello, world!"; - - // Act - var compressed = GZip.Compress(input); - var decompressed = GZip.Decompress(compressed); - - // Assert - Assert.Equal(input, Encoding.UTF8.GetString(decompressed)); - } - - [Fact] - public void CompressAndDecompress_ByteArray_ReturnsOriginalData() - { - // Arrange - var input = Encoding.UTF8.GetBytes("Sample text for compression"); - - // Act - var compressed = GZip.Compress(input); - var decompressed = GZip.Decompress(compressed); - - // Assert - Assert.Equal(input, decompressed); - } - - - [Theory] - [InlineData("")] - [InlineData("Short string")] - [InlineData("The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.")] - public void Compress_Decompress_String_VariousLengths(string input) - { - // Act - var compressed = GZip.Compress(input); - var decompressed = GZip.Decompress(Convert.ToBase64String(compressed)); - var resultString = Encoding.UTF8.GetString(decompressed); - - // Assert - Assert.Equal(input, resultString); - } + [Fact] + public void CompressDecompressStream_ShouldReturnOriginalData() + { + // Arrange + var originalData = "MySensitiveData"; + var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(originalData)); + var compressedStream = new MemoryStream(); + var decompressedStream = new MemoryStream(); + + // Act - Compress + GZip.Compress(inputStream, compressedStream); + compressedStream.Seek(0, SeekOrigin.Begin); // Reset stream position for reading + + // Act - Decompress + GZip.Decompress(compressedStream, decompressedStream); + decompressedStream.Seek(0, SeekOrigin.Begin); // Reset stream position for reading + var resultData = new StreamReader(decompressedStream).ReadToEnd(); + + // Assert + Assert.Equal(originalData, resultData); + } + + [Fact] + public void CompressStream_ShouldReduceSizeForCompressibleData() + { + // Arrange + var originalData = new string('a', 1024); // Highly compressible data + var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(originalData)); + var compressedStream = new MemoryStream(); + + // Act + GZip.Compress(inputStream, compressedStream); + + // Assert + Assert.True(compressedStream.Length < inputStream.Length); + } + + [Fact] + public void DecompressStream_WithCorruptedData_ShouldThrow() + { + // Arrange + var corruptedData = new byte[] + { + 0x0, + 0x1, + 0x2, + 0x3 + }; // Not valid compressed data + var inputStream = new MemoryStream(corruptedData); + var decompressedStream = new MemoryStream(); + + // Act & Assert + Assert.Throws(() => GZip.Decompress(inputStream, decompressedStream)); + } + + [Fact] + public void CompressDecompressEmptyStream_ShouldHandleGracefully() + { + // Arrange + var emptyStream = new MemoryStream(); + var compressedStream = new MemoryStream(); + var decompressedStream = new MemoryStream(); + + // Act + GZip.Compress(emptyStream, compressedStream); + compressedStream.Seek(0, SeekOrigin.Begin); // Reset the compressed stream position for reading + + // Act + GZip.Decompress(compressedStream, decompressedStream); + decompressedStream.Seek(0, SeekOrigin.Begin); // Reset the decompressed stream position for reading + var resultData = new StreamReader(decompressedStream).ReadToEnd(); + + // Assert + Assert.Empty(resultData); + } + + [Fact] + public void CompressAndDecompress_ShouldReturnOriginalObject() + { + var originalObject = new TestClass + { + SomeLongId = 1, + FullName = "Test" + }; + + // Act + var compressedData = GZip.Compress(originalObject); + var decompressedObject = GZip.Decompress(compressedData); + + // Assert + Assert.NotNull(decompressedObject); + Assert.Equal(originalObject.SomeLongId, decompressedObject.SomeLongId); + Assert.Equal(originalObject.FullName, decompressedObject.FullName); + } + + [Fact] + public void CompressAndDecompress_ShouldReturnOriginalObject2() + { + var originalObject = new TestClass + { + SomeLongId = 1, + FullName = "Test" + }; + + // Act + var compressedData = GZip.Compress(originalObject); + var stringData = Convert.ToBase64String(compressedData); + var decompressedObject = GZip.Decompress(stringData); + + // Assert + Assert.NotNull(decompressedObject); + Assert.Equal(originalObject.SomeLongId, decompressedObject.SomeLongId); + Assert.Equal(originalObject.FullName, decompressedObject.FullName); + } + + [Fact] + public void Decompress_WithInvalidData_ShouldReturnNull() + { + // Arrange + var invalidData = Encoding.UTF8.GetBytes("Invalid compressed data"); + + // Act & Assert + var exception = Record.Exception(() => GZip.Decompress(invalidData)); + Assert.NotNull(exception); + Assert.IsType(exception); + } + + + [Fact] + public void CompressAndDecompress_String_ReturnsOriginalData() + { + // Arrange + const string input = "Hello, world!"; + + // Act + var compressed = GZip.Compress(input); + var decompressedBytes = GZip.Decompress(compressed); + + // Convert decompressed bytes back to string + var decompressedString = Encoding.UTF8.GetString(decompressedBytes); + + // Assert + Assert.Equal(input, decompressedString); + } + + [Fact] + public void Decompress_Base64_ReturnsOriginalData() + { + // Arrange + const string input = "Hello, world!"; + var compressed = GZip.Compress(input); + var compressedBase64 = Convert.ToBase64String(compressed); + + // Act + var result = GZip.Decompress(compressedBase64); + var resultString = Encoding.UTF8.GetString(result); + + // Assert + Assert.Equal(input, resultString); + } + + [Fact] + public void Compress_And_Decompress_Byte_Array_ReturnsOriginalData() + { + // Arrange + var input = "Hello, world!"; + + // Act + var compressed = GZip.Compress(input); + var decompressed = GZip.Decompress(compressed); + + // Assert + Assert.Equal(input, Encoding.UTF8.GetString(decompressed)); + } + + [Fact] + public void CompressAndDecompress_ByteArray_ReturnsOriginalData() + { + // Arrange + var input = Encoding.UTF8.GetBytes("Sample text for compression"); + + // Act + var compressed = GZip.Compress(input); + var decompressed = GZip.Decompress(compressed); + + // Assert + Assert.Equal(input, decompressed); + } + + + [Theory] + [InlineData("")] + [InlineData("Short string")] + [InlineData("The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.")] + public void Compress_Decompress_String_VariousLengths(string input) + { + // Act + var compressed = GZip.Compress(input); + var decompressed = GZip.Decompress(Convert.ToBase64String(compressed)); + var resultString = Encoding.UTF8.GetString(decompressed); + + // Assert + Assert.Equal(input, resultString); + } + + private class TestClass + { + public int SomeLongId { get; init; } + public string? FullName { get; init; } + } } \ No newline at end of file diff --git a/test/Pandatech.Crypto.Tests/HostBuilderTests.cs b/test/Pandatech.Crypto.Tests/HostBuilderTests.cs deleted file mode 100644 index 18395b8..0000000 --- a/test/Pandatech.Crypto.Tests/HostBuilderTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Pandatech.Crypto.Tests; - -public class HostBuilderTests -{ - [Fact] - public void AddPandatechCryptoAes256_RegistersServicesCorrectly() - { - // Arrange - var services = new ServiceCollection(); - - // Act - var key = Random.GenerateAes256KeyString(); - services.AddPandatechCryptoAes256(options => - { - options.Key = key; - }); - - // Assert - var serviceProvider = services.BuildServiceProvider(); - var aes256Options = serviceProvider.GetRequiredService(); - var aes256 = serviceProvider.GetRequiredService(); - - Assert.NotNull(aes256Options); - Assert.Equal(key, aes256Options.Key); - Assert.NotNull(aes256); - } - [Fact] - public void AddPandatechCryptoAes256_RegistersAsSingleton() - { - // Arrange - var services = new ServiceCollection(); - - // Act - services.AddPandatechCryptoAes256(options => - { - - options.Key = Random.GenerateAes256KeyString(); - }); - - var serviceProvider = services.BuildServiceProvider(); - - // Assert - var aes256Instance1 = serviceProvider.GetRequiredService(); - var aes256Instance2 = serviceProvider.GetRequiredService(); - - Assert.Same(aes256Instance1, aes256Instance2); - } - [Fact] - public void AddPandatechCryptoArgon2Id_RegistersServicesCorrectly() - { - // Arrange - var services = new ServiceCollection(); - - // Act - services.AddPandatechCryptoArgon2Id(options => - { - options.Iterations = 4; - // ... other configurations - }); - - // Assert - var serviceProvider = services.BuildServiceProvider(); - var argon2IdOptions = serviceProvider.GetRequiredService(); - var argon2Id = serviceProvider.GetRequiredService(); - - Assert.NotNull(argon2IdOptions); - Assert.Equal(4, argon2IdOptions.Iterations); - Assert.NotNull(argon2Id); - } - [Fact] - public void AddPandatechCryptoArgon2Id_RegistersAsSingleton() - { - // Arrange - var services = new ServiceCollection(); - - // Act - services.AddPandatechCryptoArgon2Id(options => - { - options.Iterations = 4; - // ... other configurations - }); - - var serviceProvider = services.BuildServiceProvider(); - - // Assert - var argon2IdInstance1 = serviceProvider.GetRequiredService(); - var argon2IdInstance2 = serviceProvider.GetRequiredService(); - - Assert.Same(argon2IdInstance1, argon2IdInstance2); - } - - - [Fact] - public void AddPandatechCryptoArgon2Id_RegistersServicesCorrectly2() - { - // Arrange - var services = new ServiceCollection(); - - // Act - services.AddPandatechCryptoArgon2Id(); - - // Assert - var serviceProvider = services.BuildServiceProvider(); - Assert.NotNull(serviceProvider.GetService()); - Assert.NotNull(serviceProvider.GetService()); - } - -} \ No newline at end of file diff --git a/test/Pandatech.Crypto.Tests/MaskTests.cs b/test/Pandatech.Crypto.Tests/MaskTests.cs index 11a4b45..4dc9bb1 100644 --- a/test/Pandatech.Crypto.Tests/MaskTests.cs +++ b/test/Pandatech.Crypto.Tests/MaskTests.cs @@ -1,45 +1,43 @@ -namespace Pandatech.Crypto.Tests; +using Pandatech.Crypto.Helpers; -using Xunit; -using Crypto; -using System; +namespace Pandatech.Crypto.Tests; public class MaskTests { - [Theory] - [InlineData("vazgen.Sargsyan@vazgen.com", "va*************@vazgen.com")] - [InlineData("test@example.com", "te**@example.com")] - [InlineData("ab@c.com", "ab@c.com")] - [InlineData("a@b.com", "a@b.com")] - public void MaskEmail_ValidEmails_ReturnsMaskedEmail(string input, string expected) - { - var result = input.MaskEmail(); - Assert.Equal(expected, result); - } + [Theory] + [InlineData("vazgen.Sargsyan@vazgen.com", "va*************@vazgen.com")] + [InlineData("test@example.com", "te**@example.com")] + [InlineData("ab@c.com", "ab@c.com")] + [InlineData("a@b.com", "a@b.com")] + public void MaskEmail_ValidEmails_ReturnsMaskedEmail(string input, string expected) + { + var result = input.MaskEmail(); + Assert.Equal(expected, result); + } - [Theory] - [InlineData("")] - [InlineData("notanemail")] - public void MaskEmail_InvalidEmails_ThrowsArgumentException(string input) - { - Assert.Throws(input.MaskEmail); - } + [Theory] + [InlineData("")] + [InlineData("notanemail")] + public void MaskEmail_InvalidEmails_ThrowsArgumentException(string input) + { + Assert.Throws(input.MaskEmail); + } - [Theory] - [InlineData("1234567890", "******7890")] - [InlineData("1234", "1234")] - [InlineData("12", "12")] - public void MaskPhoneNumber_ValidPhoneNumbers_ReturnsMaskedPhone(string input, string expected) - { - var result = input.MaskPhoneNumber(); - Assert.Equal(expected, result); - } + [Theory] + [InlineData("1234567890", "******7890")] + [InlineData("1234", "1234")] + [InlineData("12", "12")] + public void MaskPhoneNumber_ValidPhoneNumbers_ReturnsMaskedPhone(string input, string expected) + { + var result = input.MaskPhoneNumber(); + Assert.Equal(expected, result); + } - [Theory] - [InlineData(null)] - [InlineData("")] - public void MaskPhoneNumber_InvalidPhoneNumbers_ThrowsArgumentException(string input) - { - Assert.Throws(input.MaskPhoneNumber); - } + [Theory] + [InlineData(null)] + [InlineData("")] + public void MaskPhoneNumber_InvalidPhoneNumbers_ThrowsArgumentException(string input) + { + Assert.Throws(input.MaskPhoneNumber); + } } \ No newline at end of file diff --git a/test/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj b/test/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj index f9d5af8..8408695 100644 --- a/test/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj +++ b/test/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj @@ -10,9 +10,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,7 +24,7 @@ - + diff --git a/test/Pandatech.Crypto.Tests/PasswordTests.cs b/test/Pandatech.Crypto.Tests/PasswordTests.cs index 92a2573..16519c7 100644 --- a/test/Pandatech.Crypto.Tests/PasswordTests.cs +++ b/test/Pandatech.Crypto.Tests/PasswordTests.cs @@ -1,91 +1,118 @@ -namespace Pandatech.Crypto.Tests; +using Pandatech.Crypto.Helpers; + +namespace Pandatech.Crypto.Tests; public class PasswordTests { - [Theory] - [InlineData(7, true, true, true, true)] - [InlineData(4, false, true, false, false)] - [InlineData(5, true, true, true, false)] - public void Generate_ShouldReturnPasswordWithCorrectProperties( - int length, - bool includeUppercase, - bool includeLowercase, - bool includeDigits, - bool includeSpecialChars) - { - // Generate a random password - var password = Password.GenerateRandom(length, includeUppercase, includeLowercase, includeDigits, - includeSpecialChars); + [Theory] + [InlineData(7, true, true, true, true)] + [InlineData(4, false, true, false, false)] + [InlineData(5, true, true, true, false)] + public void Generate_ShouldReturnPasswordWithCorrectProperties(int length, + bool includeUppercase, + bool includeLowercase, + bool includeDigits, + bool includeSpecialChars) + { + // Generate a random password + var password = Password.GenerateRandom(length, + includeUppercase, + includeLowercase, + includeDigits, + includeSpecialChars); + + // Check if the password length is correct + Assert.Equal(length, password.Length); + + // Check if the password contains the correct character sets + if (includeUppercase) + { + Assert.Contains(password, char.IsUpper); + } + + if (includeLowercase) + { + Assert.Contains(password, char.IsLower); + } - // Check if the password length is correct - Assert.Equal(length, password.Length); + if (includeDigits) + { + Assert.Contains(password, char.IsDigit); + } - // Check if the password contains the correct character sets - if (includeUppercase) - Assert.Contains(password, char.IsUpper); - if (includeLowercase) - Assert.Contains(password, char.IsLower); - if (includeDigits) - Assert.Contains(password, char.IsDigit); - if (includeSpecialChars) - Assert.Contains(password, c => "!@#$%^&*()-_=+[]{}|;:'\",.<>?".Contains(c)); - } + if (includeSpecialChars) + { + Assert.Contains(password, c => "!@#$%^&*()-_=+[]{}|;:'\",.<>?".Contains(c)); + } + } - [Fact] - public void Generate_ShouldReturnDifferentPasswords() - { - var password1 = Password.GenerateRandom(12, true, true, true, true); - var password2 = Password.GenerateRandom(12, true, true, true, true); + [Fact] + public void Generate_ShouldReturnDifferentPasswords() + { + var password1 = Password.GenerateRandom(12, true, true, true, true); + var password2 = Password.GenerateRandom(12, true, true, true, true); - Assert.NotEqual(password1, password2); - } + Assert.NotEqual(password1, password2); + } - [Theory] - [InlineData(7, true, false, true, true)] - [InlineData(4, false, true, false, false)] - [InlineData(5, true, false, false, false)] - [InlineData(13, true, true, false, false)] - [InlineData(25, true, true, true, false)] - [InlineData(35, true, true, true, true)] - public void ValidationTestForGeneratedPasswords( - int length, - bool includeUppercase, - bool includeLowercase, - bool includeDigits, - bool includeSpecialChars) - { - // Generate a random password - var password = Password.GenerateRandom(length, includeUppercase, includeLowercase, includeDigits, - includeSpecialChars); - Assert.True(Password.Validate(password, length, includeUppercase, includeLowercase, includeDigits, - includeSpecialChars)); - } + [Theory] + [InlineData(7, true, false, true, true)] + [InlineData(4, false, true, false, false)] + [InlineData(5, true, false, false, false)] + [InlineData(13, true, true, false, false)] + [InlineData(25, true, true, true, false)] + [InlineData(35, true, true, true, true)] + public void ValidationTestForGeneratedPasswords(int length, + bool includeUppercase, + bool includeLowercase, + bool includeDigits, + bool includeSpecialChars) + { + // Generate a random password + var password = Password.GenerateRandom(length, + includeUppercase, + includeLowercase, + includeDigits, + includeSpecialChars); + Assert.True(Password.Validate(password, + length, + includeUppercase, + includeLowercase, + includeDigits, + includeSpecialChars)); + } - [Theory] - [InlineData(7, true, false, true, true)] - [InlineData(4, false, true, false, false)] - [InlineData(5, true, false, false, false)] - [InlineData(13, true, true, false, false)] - [InlineData(25, true, true, true, false)] - [InlineData(35, false, true, true, true)] - public void ValidationTestForGeneratedPasswordsOpposite( - int length, - bool includeUppercase, - bool includeLowercase, - bool includeDigits, - bool includeSpecialChars) - { - // Generate a random password - var password = Password.GenerateRandom(length, includeUppercase, includeLowercase, includeDigits, - includeSpecialChars); - Assert.False(Password.Validate(password, length, !includeUppercase, !includeLowercase, !includeDigits, - !includeSpecialChars)); - } + [Theory] + [InlineData(7, true, false, true, true)] + [InlineData(4, false, true, false, false)] + [InlineData(5, true, false, false, false)] + [InlineData(13, true, true, false, false)] + [InlineData(25, true, true, true, false)] + [InlineData(35, false, true, true, true)] + public void ValidationTestForGeneratedPasswordsOpposite(int length, + bool includeUppercase, + bool includeLowercase, + bool includeDigits, + bool includeSpecialChars) + { + // Generate a random password + var password = Password.GenerateRandom(length, + includeUppercase, + includeLowercase, + includeDigits, + includeSpecialChars); + Assert.False(Password.Validate(password, + length, + !includeUppercase, + !includeLowercase, + !includeDigits, + !includeSpecialChars)); + } - [Fact] - public void PasswordValidationTests() - { - var password1 = "Qwerty123!"; - Assert.True(Password.Validate(password1, 8, false, false, false, false)); - } + [Fact] + public void PasswordValidationTests() + { + var password1 = "Qwerty123!"; + Assert.True(Password.Validate(password1, 8, false, false, false, false)); + } } \ No newline at end of file diff --git a/test/Pandatech.Crypto.Tests/RandomTests.cs b/test/Pandatech.Crypto.Tests/RandomTests.cs index f76da8b..347e217 100644 --- a/test/Pandatech.Crypto.Tests/RandomTests.cs +++ b/test/Pandatech.Crypto.Tests/RandomTests.cs @@ -1,52 +1,66 @@ -namespace Pandatech.Crypto.Tests; +using Random = Pandatech.Crypto.Helpers.Random; + +namespace Pandatech.Crypto.Tests; public class RandomTests { - [Fact] - public void Generate_ShouldReturnByteArray() - { - const int length = 16; - var randomBytes = Random.GenerateBytes(length); - - Assert.NotNull(randomBytes); - Assert.Equal(length, randomBytes.Length); - } - - - [Fact] - public void GeneratePandaId_WithNonZeroPreviousId_ReturnsIncrementedId() - { - const long previousId = 1_000_000; - for (var i = 0; i < 1_000_000; ++i) - { - var newId = Random.GenerateIdWithVariableSequence(previousId); - - Assert.True(newId > previousId); - } - } - - [Fact] - public void GeneratePandaId_WithinReasonableIterations_DoesNotProduceDuplicates() - { - long previousId = 0; - - for (var i = 0; i < 1_000_000; ++i) - { - var id = Random.GenerateIdWithVariableSequence(previousId); - Assert.NotEqual(previousId, id); - previousId = id; - } - } - - [Fact] - public void GenerateSecureToken_ShouldReturnValidUrlSafeString() - { - var token = Random.GenerateSecureToken(); - - Assert.NotNull(token); - Assert.Equal(43, token.Length); // 32 bytes => 43 Base64 characters (without padding) - Assert.DoesNotContain("+", token); - Assert.DoesNotContain("/", token); - Assert.DoesNotContain("=", token); - } + [Fact] + public void Generate_ShouldReturnByteArray() + { + const int length = 16; + var randomBytes = Random.GenerateBytes(length); + + Assert.NotNull(randomBytes); + Assert.Equal(length, randomBytes.Length); + } + + + [Fact] + public void GeneratePandaId_WithNonZeroPreviousId_ReturnsIncrementedId() + { + const long previousId = 1_000_000; + for (var i = 0; i < 1_000_000; ++i) + { + var newId = Random.GenerateIdWithVariableSequence(previousId); + + Assert.True(newId > previousId); + } + } + + [Fact] + public void GeneratePandaId_WithinReasonableIterations_DoesNotProduceDuplicates() + { + long previousId = 0; + + for (var i = 0; i < 1_000_000; ++i) + { + var id = Random.GenerateIdWithVariableSequence(previousId); + Assert.NotEqual(previousId, id); + previousId = id; + } + } + + [Fact] + public void GenerateSecureToken_ShouldReturnValidUrlSafeString() + { + var token = Random.GenerateSecureToken(); + + Assert.NotNull(token); + Assert.Equal(43, token.Length); // 32 bytes => 43 Base64 characters (without padding) + Assert.DoesNotContain("+", token); + Assert.DoesNotContain("/", token); + Assert.DoesNotContain("=", token); + } + + [Fact] + public void GenerateShortUniqueString_ShouldReturnValidUrlSafeString() + { + var uniqueString = Random.GenerateShortUniqueString(); + + Assert.NotNull(uniqueString); + Assert.Equal(16, uniqueString.Length); // 12 bytes -> 16 Base64 characters without padding + Assert.DoesNotContain("+", uniqueString); + Assert.DoesNotContain("/", uniqueString); + Assert.DoesNotContain("=", uniqueString); + } } \ No newline at end of file diff --git a/test/Pandatech.Crypto.Tests/Sha2Tests.cs b/test/Pandatech.Crypto.Tests/Sha2Tests.cs index 9a60e11..33e2153 100644 --- a/test/Pandatech.Crypto.Tests/Sha2Tests.cs +++ b/test/Pandatech.Crypto.Tests/Sha2Tests.cs @@ -1,4 +1,6 @@ -namespace Pandatech.Crypto.Tests; +using Pandatech.Crypto.Helpers; + +namespace Pandatech.Crypto.Tests; public class Sha2Tests { @@ -7,7 +9,11 @@ public void HmacSha256_ValidInput_ReturnsExpectedHash() { // Arrange var key = "secret"u8.ToArray(); - var messages = new[] { "Hello", "World" }; + var messages = new[] + { + "Hello", + "World" + }; const string expectedHashHex = "2e91612bb72b29d82f32789d063de62d5897a4ee5d3b5d34459801b94397b099"; // Act @@ -37,7 +43,11 @@ public void HmacSha256_ConsistentOutput_ForSameInputs() { // Arrange var key = "secret"u8.ToArray(); - var messages = new[] { "Test", "Message" }; + var messages = new[] + { + "Test", + "Message" + }; // Act var hash1 = Sha2.GetHmacSha256Hex(key, messages); @@ -46,13 +56,17 @@ public void HmacSha256_ConsistentOutput_ForSameInputs() // Assert Assert.Equal(hash1, hash2); } - + [Fact] public void HmacSha256Base64_ValidInput_ReturnsExpectedBase64() { // Arrange var key = "secret"u8.ToArray(); - var messages = new[] { "Hello", "World" }; + var messages = new[] + { + "Hello", + "World" + }; const string expectedBase64 = "LpFhK7crKdgvMnidBj3mLViXpO5dO100RZgBuUOXsJk="; // Act diff --git a/test/Pandatech.Crypto.Tests/Sha3Tests.cs b/test/Pandatech.Crypto.Tests/Sha3Tests.cs index ef35322..75f8e2f 100644 --- a/test/Pandatech.Crypto.Tests/Sha3Tests.cs +++ b/test/Pandatech.Crypto.Tests/Sha3Tests.cs @@ -1,50 +1,51 @@ using System.Text; +using Pandatech.Crypto.Helpers; namespace Pandatech.Crypto.Tests; public class Sha3Tests { - [Fact] - public void Hash_IsNotNull() - { - var hash = Sha3.Hash("Hello, world!"); - Assert.NotNull(hash); - } - - [Fact] - public void Hash_Length_IsCorrect() - { - var hash = Sha3.Hash("Hello, world!"); - Assert.Equal(64, hash.Length); // 512 bits = 64 bytes - } - - [Fact] - public void Hash_VerifyHash_IsTrue() - { - const string data = "Hello, world!"; - var hash = Sha3.Hash(data); - - var result = Sha3.VerifyHash(data, hash); - Assert.True(result); - } - - [Fact] - public void Hash_VerifyHash_WithBytes_IsTrue() - { - const string data = "Hello, world!"; - var bytes = Encoding.UTF8.GetBytes(data); - var hash = Sha3.Hash(bytes); - - var result = Sha3.VerifyHash(data, hash); - Assert.True(result); - } - - [Fact] - public void Hash_VerifyHash_IsFalse() - { - var hash = Sha3.Hash("Hello, world!"); - - var result = Sha3.VerifyHash("Hello, universe!", hash); - Assert.False(result); - } + [Fact] + public void Hash_IsNotNull() + { + var hash = Sha3.Hash("Hello, world!"); + Assert.NotNull(hash); + } + + [Fact] + public void Hash_Length_IsCorrect() + { + var hash = Sha3.Hash("Hello, world!"); + Assert.Equal(64, hash.Length); // 512 bits = 64 bytes + } + + [Fact] + public void Hash_VerifyHash_IsTrue() + { + const string data = "Hello, world!"; + var hash = Sha3.Hash(data); + + var result = Sha3.VerifyHash(data, hash); + Assert.True(result); + } + + [Fact] + public void Hash_VerifyHash_WithBytes_IsTrue() + { + const string data = "Hello, world!"; + var bytes = Encoding.UTF8.GetBytes(data); + var hash = Sha3.Hash(bytes); + + var result = Sha3.VerifyHash(data, hash); + Assert.True(result); + } + + [Fact] + public void Hash_VerifyHash_IsFalse() + { + var hash = Sha3.Hash("Hello, world!"); + + var result = Sha3.VerifyHash("Hello, universe!", hash); + Assert.False(result); + } } \ No newline at end of file