diff --git a/README.md b/README.md index 5c5c7df..af6ad3f 100644 --- a/README.md +++ b/README.md @@ -1,342 +1,2552 @@ -# MaksIT.Core - -MaksIT.Core is a collection of helper methods and extensions for .NET projects, designed to simplify common tasks and improve code readability. The library includes extensions for Guid, string, Object, security methods for password hashing, and a base class for creating enumeration types. +# MaksIT.Core Library Documentation ## Table of Contents -- [MaksIT.Core](#maksitcore) - - [Table of Contents](#table-of-contents) - - [Installation](#installation) - - [Usage](#usage) - - [Enumeration](#enumeration) - - [Guid Extensions](#guid-extensions) - - [Object Extensions](#object-extensions) - - [String Extensions](#string-extensions) - - [Password Hasher](#password-hasher) - - [DataTable Extensions](#datatable-extensions) - - [DateTime Extensions](#datetime-extensions) - - [JWT Token Generation and Validation](#jwt-token-generation-and-validation) - - [Generate a JWT Token](#generate-a-jwt-token) - - [Validate a JWT Token](#validate-a-jwt-token) - - [Generate a Refresh Token](#generate-a-refresh-token) - - [Available Methods](#available-methods) - - [Enumeration Methods](#enumeration-methods) - - [Guid Methods](#guid-methods) - - [Object Methods](#object-methods) - - [String Methods](#string-methods) - - [DataTable Methods](#datatable-methods) - - [DateTime Methods](#datetime-methods) - - [JWT Methods](#jwt-methods) - - [Contributing](#contributing) - - [Contact](#contact) - - [License](#license) - - [MIT License](#mit-license) +- [Abstractions](#abstractions) + - [Base Classes](#base-classes) + - [Eunumeration](#eunumeration) +- [Extesnions](#extensions) +- [Logging](#logging) +- [Networking](#networking) +- [Security](#security) +- [Web API Models](#web-api-models) +- [Others](#others) -## Installation -To install MaksIT.Core, add the package to your project via NuGet: +## Abstractions -```sh -dotnet add package MaksIT.Core +### Base Classes + +The following base classes in the `MaksIT.Core.Abstractions` namespace provide a foundation for implementing domain, DTO, and Web API models, ensuring consistency and maintainability in application design. + +--- + +##### 1. **`DomainObjectBase`** + +###### Summary +Represents the base class for all domain objects in the application. + +###### Purpose +- Serves as the foundation for all domain objects. +- Provides a place to include shared logic or properties for domain-level entities in the future. + +--- + +##### 2. **`DomainDocumentBase`** + +###### Summary +Represents a base class for domain documents with a unique identifier. + +###### Purpose +- Extends `DomainObjectBase` to include an identifier. +- Provides a common structure for domain entities that need unique IDs. + +###### Example Usage +```csharp +public class UserDomainDocument : DomainDocumentBase { + public UserDomainDocument(Guid id) : base(id) { + } +} ``` -Or manually add it to your `.csproj` file: +--- -```xml - +##### 3. **`DtoObjectBase`** + +###### Summary +Represents the base class for all Data Transfer Objects (DTOs). + +###### Purpose +- Serves as the foundation for all DTOs. +- Provides a place to include shared logic or properties for DTOs in the future. + +--- + +##### 4. **`DtoDocumentBase`** + +###### Summary +Represents a base class for DTOs with a unique identifier. + +###### Purpose +- Extends `DtoObjectBase` to include an identifier. +- Provides a common structure for DTOs that need unique IDs. + +###### Example Usage +```csharp +public class UserDto : DtoDocumentBase { + public required string Name { get; set; } +} ``` -## Usage +--- -### Enumeration +##### 5. **`RequestModelBase`** -The Enumeration base class provides a way to create strongly-typed enums in C#. This is useful for scenarios where you need more functionality than the default enum type. +###### Summary +Represents the base class for Web API request models. -**Example:** +###### Purpose +- Serves as a foundation for request models used in Web API endpoints. +- Provides a common structure for request validation or shared properties. + +###### Example Usage +```csharp +public class CreateUserRequest : RequestModelBase { + public required string Name { get; set; } +} +``` + +--- + +##### 6. **`ResponseModelBase`** + +###### Summary +Represents the base class for Web API response models. + +###### Purpose +- Serves as a foundation for response models returned by Web API endpoints. +- Provides a common structure for standardizing API responses. + +###### Example Usage +```csharp +public class UserResponse : ResponseModelBase { + public required Guid Id { get; set; } + public required string Name { get; set; } +} +``` + +--- + +#### Features and Benefits + +1. **Consistency**: + - Ensures a uniform structure for domain, DTO, and Web API models. + +2. **Extensibility**: + - Base classes can be extended to include shared properties or methods as needed. + +3. **Type Safety**: + - Generic identifiers (`T`) ensure type safety for domain documents and DTOs. + +4. **Reusability**: + - Common logic or properties can be added to base classes and reused across the application. + +--- + +#### Example End-to-End Usage ```csharp -public class Status : Enumeration -{ - public static readonly Status Active = new Status(1, "Active"); - public static readonly Status Inactive = new Status(2, "Inactive"); - - private Status(int id, string name) : base(id, name) { } +// Domain Class +public class ProductDomain : DomainDocumentBase { + public ProductDomain(int id) : base(id) { } + public string Name { get; set; } = string.Empty; } -// Usage -var activeStatus = Status.FromValue(1); -Console.WriteLine(activeStatus.Name); // Output: Active +// DTO Class +public class ProductDto : DtoDocumentBase { + public required string Name { get; set; } +} + +// Web API Request Model +public class CreateProductRequest : RequestModelBase { + public required string Name { get; set; } +} + +// Web API Response Model +public class ProductResponse : ResponseModelBase { + public required int Id { get; set; } + public required string Name { get; set; } +} ``` -### Guid Extensions +--- -The `GuidExtensions` class contains extensions for working with Guid types. +#### Best Practices -**Example:** +1. **Keep Base Classes Lightweight**: + - Avoid adding unnecessary properties or methods to base classes. +2. **Encapsulation**: + - Use base classes to enforce encapsulation and shared behavior across entities. + +3. **Validation**: + - Extend `RequestModelBase` or `ResponseModelBase` to include validation logic if needed. + +--- + +This structure promotes clean code principles, reducing redundancy and improving maintainability across the application layers. + +--- + +### Eunumeration + +The `Enumeration` class in the `MaksIT.Core.Abstractions` namespace provides a base class for creating strongly-typed enumerations. It enables you to define enumerable constants with additional functionality, such as methods for querying, comparing, and parsing enumerations. + +--- + +#### Features and Benefits + +1. **Strongly-Typed Enumerations**: + - Combines the clarity of enums with the extensibility of classes. + - Supports additional fields, methods, or logic as needed. + +2. **Reflection Support**: + - Dynamically retrieve all enumeration values with `GetAll`. + +3. **Parsing Capabilities**: + - Retrieve enumeration values by ID or display name. + +4. **Comparison and Equality**: + - Fully implements equality and comparison operators for use in collections and sorting. + +--- + +#### Example Usage + +#### Defining an Enumeration ```csharp -Guid guid = Guid.NewGuid(); -Guid? nullableGuid = guid.ToNullable(); +public class MyEnumeration : Enumeration { + public static readonly MyEnumeration Value1 = new(1, "Value One"); + public static readonly MyEnumeration Value2 = new(2, "Value Two"); + + private MyEnumeration(int id, string name) : base(id, name) { } +} ``` -### Object Extensions +#### Retrieving All Values +```csharp +var allValues = Enumeration.GetAll(); +allValues.ToList().ForEach(Console.WriteLine); +``` -The `ObjectExtensions` class provides extensions for working with objects. +#### Parsing by ID or Name +```csharp +var valueById = Enumeration.FromValue(1); +var valueByName = Enumeration.FromDisplayName("Value One"); -**Example:** +Console.WriteLine(valueById); // Output: Value One +Console.WriteLine(valueByName); // Output: Value One +``` +#### Comparing Enumeration Values +```csharp +var difference = Enumeration.AbsoluteDifference(MyEnumeration.Value1, MyEnumeration.Value2); +Console.WriteLine($"Absolute Difference: {difference}"); // Output: 1 +``` + +#### Using in Collections +```csharp +var values = new List { MyEnumeration.Value2, MyEnumeration.Value1 }; +values.Sort(); // Orders by ID +``` + +--- + +#### Best Practices + +1. **Extend for Specific Enums**: + - Create specific subclasses for each enumeration type. + +2. **Avoid Duplicates**: + - Ensure unique IDs and names for each enumeration value. + +3. **Use Reflection Sparingly**: + - Avoid calling `GetAll` in performance-critical paths. + +--- + +The `Enumeration` class provides a powerful alternative to traditional enums, offering flexibility and functionality for scenarios requiring additional metadata or logic. + +--- + +## Extensions + +### DataTableExtensions + +The `DataTableExtensions` class is a static utility class in the `MaksIT.Core.Extensions` namespace. It provides extension methods for working with `DataTable` objects, enabling functionality such as counting duplicate rows between two `DataTable` instances and retrieving distinct records based on specified columns. + +--- + +#### Methods + +##### 1. **`DuplicatesCount`** + +###### Summary +Compares two `DataTable` instances (`dt1` and `dt2`) row by row and counts the number of duplicate rows. + +###### Usage +```csharp +DataTable table1 = new DataTable(); +DataTable table2 = new DataTable(); +// Populate table1 and table2... + +int duplicateCount = table1.DuplicatesCount(table2); +Console.WriteLine($"Number of duplicate rows: {duplicateCount}"); +``` + +--- + +##### 2. **`DistinctRecords`** + +###### Summary +Filters a `DataTable` to return distinct rows based on specified columns. + +###### Usage +```csharp +DataTable table = new DataTable(); +table.Columns.Add("Name"); +table.Columns.Add("Age"); +// Populate the table... + +string[] columns = { "Name", "Age" }; +DataTable distinctTable = table.DistinctRecords(columns); + +Console.WriteLine("Distinct rows based on Name and Age:"); +foreach (DataRow row in distinctTable.Rows) +{ + Console.WriteLine($"{row["Name"]}, {row["Age"]}"); +} +``` + +--- + +#### Notes +- **Performance Considerations**: + - The `DuplicatesCount` method uses nested loops, which may impact performance on large data sets. Consider optimization for large tables. + - The `DistinctRecords` method leverages `DataView.ToTable`, which is efficient for retrieving distinct records but requires valid column names. + +- **Edge Cases**: + - Ensure both `DataTable` instances in `DuplicatesCount` have compatible schemas for meaningful results. + - For `DistinctRecords`, columns that do not exist in the source `DataTable` will throw an exception. + +- **Null Handling**: + - Both methods ensure that `null` inputs result in a clear exception message. + +--- + +This class adds flexibility when working with `DataTable` objects, making it easier to handle duplicate detection and record filtration tasks programmatically. + +--- + +### DateTimeExtensions + +The `DateTimeExtensions` class in the `MaksIT.Core.Extensions` namespace provides a set of extension methods for the `DateTime` type, enabling enhanced date manipulation capabilities. These methods simplify common date-related tasks, such as adding workdays, finding specific weekdays, or determining month/year boundaries. + +--- + +#### Methods + +##### 1. **`AddWorkdays`** + +###### Summary +Adds a specified number of workdays (excluding weekends and holidays) to the given date. + +###### Usage +```csharp +DateTime today = DateTime.Today; +IHolidayCalendar holidayCalendar = new CustomHolidayCalendar(); +DateTime futureWorkday = today.AddWorkdays(5, holidayCalendar); +``` + +--- + +##### 2. **`NextWeekday`** + +###### Summary +Finds the next occurrence of a specified weekday starting from a given date. + +###### Usage +```csharp +DateTime today = DateTime.Today; +DateTime nextMonday = today.NextWeekday(DayOfWeek.Monday); +``` + +--- + +##### 3. **`ToNextWeekday`** + +###### Summary +Calculates the time span until the next occurrence of a specified weekday. + +###### Usage +```csharp +DateTime today = DateTime.Today; +TimeSpan timeToNextFriday = today.ToNextWeekday(DayOfWeek.Friday); +``` + +--- + +##### 4. **`NextEndOfMonth`** + +###### Summary +Gets the date of the last day of the next month, preserving the time of the given date. + +###### Usage +```csharp +DateTime today = DateTime.Today; +DateTime nextMonthEnd = today.NextEndOfMonth(); +``` + +--- + +##### 5. **`EndOfMonth`** + +###### Summary +Gets the date of the last day of the current month, preserving the time of the given date. + +--- + +##### 6. **`BeginOfMonth`** + +###### Summary +Gets the first day of the current month, preserving the time of the given date. + +--- + +##### 7. **`StartOfYear` / `EndOfYear`** + +###### Summary +Gets the first or last day of the current year, preserving the time of the given date. + +--- + +##### 8. **`IsEndOfMonth` / `IsBeginOfMonth` / `IsEndOfYear`** + +###### Summary +Checks whether the given date is at the end of the month, the beginning of the month, or the end of the year. + +--- + +##### 9. **`IsSameMonth`** + +###### Summary +Checks whether two dates belong to the same month and year. + +--- + +##### 10. **`GetDifferenceInYears`** + +###### Summary +Calculates the number of complete calendar years between two dates. + +###### Usage +```csharp +DateTime birthDate = new DateTime(1990, 5, 15); +DateTime today = DateTime.Today; +int age = birthDate.GetDifferenceInYears(today); +``` + +--- + +#### Interface: `IHolidayCalendar` + +The `IHolidayCalendar` interface defines a method to determine if a specific date is a holiday. + +#### Example Implementation +```csharp +public class CustomHolidayCalendar : IHolidayCalendar { + private readonly HashSet holidays = new() { new DateTime(2024, 1, 1) }; + + public bool Contains(DateTime date) => holidays.Contains(date.Date); +} +``` + +This class, with its utility methods, simplifies date calculations for workday management, boundaries, and comparisons. It is especially useful for business and scheduling applications. + +--- + +### ExpressionExtensions + +The `ExpressionExtensions` class in the `MaksIT.Core.Extensions` namespace provides extension methods for working with LINQ expressions. These methods simplify tasks like combining multiple expressions into a single logical predicate, which is especially useful in building dynamic queries. + +--- + +#### Methods + +##### 1. **`CombineWith`** + +###### How It Works +1. The method reuses the parameter from the `first` expression. +2. It replaces the parameter in the `second` expression with the same parameter from the `first` expression using the `SubstituteParameterVisitor` class. +3. Combines the bodies of `first` and `second` using `Expression.AndAlso`. + +###### Usage +```csharp +Expression> isEven = x => x % 2 == 0; +Expression> isPositive = x => x > 0; + +var combinedExpression = isEven.CombineWith(isPositive); + +var numbers = new[] { -2, -1, 0, 1, 2, 3, 4 }; +var filteredNumbers = numbers.AsQueryable().Where(combinedExpression).ToList(); + +Console.WriteLine(string.Join(", ", filteredNumbers)); // Output: 2, 4 +``` + +--- + +#### Notes + +- **Use Cases**: + - Dynamically building LINQ queries with multiple predicates. + - Simplifying expression tree manipulation in repository patterns or query builders. + +- **Performance**: + - The method uses expression visitors, which is efficient for modifying expression trees without evaluating them. + +- **Limitations**: + - Works only with `Expression>` predicates. + - Both `first` and `second` must target the same type `T`. + +--- + +This utility makes it easy to compose complex logical expressions dynamically, a common need in advanced querying scenarios like filtering or search functionalities. + +--- + +### GuidExtensions + +The `GuidExtensions` class in the `MaksIT.Core.Extensions` namespace provides an extension method for converting a `Guid` to a nullable `Guid?`. This is useful in scenarios where the default value of `Guid.Empty` needs to be treated as `null`. + +--- + +#### Methods + +##### 1. **`ToNullable`** + +###### Summary +Converts a `Guid` to a nullable `Guid?`. If the `Guid` is `Guid.Empty`, the method returns `null`; otherwise, it returns the `Guid`. + +###### Usage +```csharp +Guid id1 = Guid.NewGuid(); +Guid id2 = Guid.Empty; + +Guid? nullableId1 = id1.ToNullable(); // Returns the value of id1 +Guid? nullableId2 = id2.ToNullable(); // Returns null + +Console.WriteLine(nullableId1); // Outputs the GUID value +Console.WriteLine(nullableId2); // Outputs nothing (null) +``` + +--- + +#### Notes + +- **Use Cases**: + - Simplifies handling of `Guid` values in APIs, databases, or validation logic where `Guid.Empty` is treated as `null`. + - Useful in scenarios where nullable values improve data handling or reduce ambiguity in business logic. + +- **Edge Cases**: + - If the input `Guid` is `Guid.Empty`, this method always returns `null`, regardless of context. + - Any valid `Guid` other than `Guid.Empty` is returned unchanged. + +- **Performance**: + - This method performs a simple comparison and is highly efficient. + +--- + +This extension adds clarity and reduces boilerplate code when working with `Guid` values, particularly in data validation or transformation workflows. + +--- + +### ObjectExtensions + +The `ObjectExtensions` class in the `MaksIT.Core.Extensions` namespace provides extension methods for serializing objects to JSON strings using `System.Text.Json`. These methods allow flexible JSON serialization with optional custom converters. + +#### Methods + +##### 1. **`ToJson()`** + +###### Summary +Converts an object of type `T` to a JSON string using default serialization options. + +###### Usage ```csharp var person = new { Name = "John", Age = 30 }; string json = person.ToJson(); Console.WriteLine(json); // Output: {"name":"John","age":30} + +object? nullObject = null; +string nullJson = nullObject.ToJson(); +Console.WriteLine(nullJson); // Output: {} ``` -### String Extensions - -The `StringExtensions` class provides a variety of useful string manipulation methods. - -**Example:** - -```csharp -string text = "Hello World"; -bool isLike = text.Like("Hello*"); // SQL-like matching -Console.WriteLine(isLike); // Output: True -``` - -### Password Hasher - -The `PasswordHasher` class provides methods for creating salted hashes and verifying passwords using PBKDF2 with SHA-256. This is essential for securely storing and validating user passwords. - -**Example:** - -```csharp -using MaksIT.Core.Security; - -string password = "MySecurePassword123!"; - -// Hash the password -if (PasswordHasher.TryHashPassword(password, out string? hashedPassword)) -{ - Console.WriteLine("Hashed Password: " + hashedPassword); - // Store hashedPassword securely, e.g., in a database -} -else -{ - Console.WriteLine("Failed to hash password."); -} - -// Later, when verifying the password during login -bool isValid = PasswordHasher.VerifyPassword(hashedPassword, password); -Console.WriteLine("Password is valid: " + isValid); // Output: True -``` - -### DataTable Extensions - -The `DataTableExtensions` class provides useful extensions for working with `DataTable` objects. - -**Example:** - -```csharp -DataTable dt1 = new DataTable(); -dt1.Columns.Add("Id"); -dt1.Columns.Add("Name"); -dt1.Rows.Add("1", "Alice"); -dt1.Rows.Add("2", "Bob"); - -DataTable dt2 = new DataTable(); -dt2.Columns.Add("Id"); -dt2.Columns.Add("Name"); -dt2.Rows.Add("1", "Alice"); -dt2.Rows.Add("3", "Charlie"); - -int duplicateCount = dt1.DuplicatesCount(dt2); // Count duplicates -Console.WriteLine(duplicateCount); // Output: 1 - -DataTable distinctRecords = dt1.DistinctRecords(new[] { "Id", "Name" }); // Get distinct records -Console.WriteLine(distinctRecords.Rows.Count); // Output: 2 -``` - -### DateTime Extensions - -The `DateTimeExtensions` class provides a variety of helpful methods for working with `DateTime` objects, such as adding workdays, finding the end of the month, or determining if a date is a holiday. - -**Example:** - -```csharp -DateTime startDate = new DateTime(2023, 12, 22); // Friday -IHolidayCalendar holidayCalendar = new TestHolidayCalendar(new[] { new DateTime(2023, 12, 25) }); - -DateTime resultDate = startDate.AddWorkdays(5, holidayCalendar); // Skip weekends and holidays -Console.WriteLine(resultDate); // Output: 2023-12-29 - -DateTime nextThursday = DateTime.Now.NextWeekday(DayOfWeek.Thursday); -Console.WriteLine(nextThursday); // Output: Date of the next Thursday -``` - -### JWT Token Generation and Validation - -The `JwtGenerator` class provides methods for generating and validating JWT tokens, as well as generating refresh tokens. This is useful for implementing secure authentication and authorization mechanisms in your applications. - -#### Generate a JWT Token - -```csharp -using MaksIT.Core.Security; - -string secret = "your_secret_key"; -string issuer = "your_issuer"; -string audience = "your_audience"; -double expiration = 30; // Token expiration in minutes -string username = "user123"; -List roles = new List { "Admin", "User" }; - -(string token, JWTTokenClaims claims) = JwtGenerator.GenerateToken(secret, issuer, audience, expiration, username, roles); -Console.WriteLine("Generated JWT Token: " + token); -``` - -#### Validate a JWT Token - -```csharp -string tokenToValidate = "your_jwt_token"; - -JWTTokenClaims? claims = JwtGenerator.ValidateToken(secret, issuer, audience, tokenToValidate); -if (claims != null) -{ - Console.WriteLine($"Username: {claims.Username}"); - Console.WriteLine("Roles: " + string.Join(", ", claims.Roles)); -} -else -{ - Console.WriteLine("Invalid token."); -} -``` - -#### Generate a Refresh Token - -```csharp -string refreshToken = JwtGenerator.GenerateRefreshToken(); -Console.WriteLine("Generated Refresh Token: " + refreshToken); -``` - -## Available Methods - -### Enumeration Methods - -- **GetAll()**: Retrieves all static fields of a given type T that derive from Enumeration. -- **Equals(object? obj)**: Determines whether the specified object is equal to the current object. -- **GetHashCode()**: Returns the hash code for the current object. -- **AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)**: Computes the absolute difference between two enumeration values. -- **FromValue(int value)**: Retrieves an instance of type T from its integer value. -- **FromDisplayName(string displayName)**: Retrieves an instance of type T from its display name. -- **CompareTo(object? other)**: Compares the current instance with another object of the same type. - -### Guid Methods - -- **ToNullable(this Guid id)**: Converts a Guid to a nullable `Guid?`. Returns null if the Guid is `Guid.Empty`. - -### Object Methods - -- **ToJson(this T? obj)**: Converts an object to a JSON string using default serialization options. -- **ToJson(this T? obj, List? converters)**: Converts an object to a JSON string using custom converters. - -### String Methods - -- **Like(this string? text, string? wildcardedText)**: Determines if a string matches a given wildcard pattern (SQL LIKE). -- **Left(this string s, int count)**: Returns the left substring of the specified length. -- **Right(this string s, int count)**: Returns the right substring of the specified length. -- **Mid(this string s, int index, int count)**: Returns a substring starting from the specified index with the specified length. -- **ToInteger(this string s)**: Converts a string to an integer, returning zero if conversion fails. -- **IsInteger(this string s)**: Determines whether the string represents an integer. -- **Prepend(this StringBuilder sb, string content)**: Prepends content to the beginning of a `StringBuilder`. -- **ToEnum(this string input)**: Converts a string to an enum value of type T. -- **ToNullableEnum(this string input)**: Converts a string to a nullable enum value of type T. -- **ToNull(this string s)**: Returns null if the string is empty or whitespace. -- **ToLong(this string s)**: Converts a string to a long, returning a hash code if conversion fails. -- **ToNullableLong(this string s)**: Converts a string to a nullable long, returning null if conversion fails. -- **ToInt(this string s)**: Converts a string to an int, returning a hash code if conversion fails. -- **ToNullableInt(this string s)**: Converts a string to a nullable int, returning null if conversion fails. -- **ToDecimal(this string s)**: Converts a string to a decimal, returning a hash code if conversion fails. -- **ToNullableDecimal(this string s)**: Converts a string to a nullable decimal, returning null if conversion fails. -- **ToDouble(this string s)**: Converts a string to a double, returning a hash code if conversion fails. -- **ToNullableDouble(this string s)**: Converts a string to a nullable double, returning null if conversion fails. -- **ToDate(this string s, string[] formats)**: Converts a string to a `DateTime` object using specified formats. -- **ToDate(this string s)**: Converts a string to a `DateTime` object using the default format. -- **ToNullableDate(this string s)**: Converts a string to a nullable `DateTime` object using the default format. -- **ToNullableDate(this string s, string[] formats)**: Converts a string to a nullable `DateTime` object using specified formats. -- **ToBool(this string s)**: Converts a string to a boolean. -- **ToNullableBool(this string s)**: Converts a string to a nullable boolean. -- **ToGuid(this string text)**: Converts a string to a Guid. -- **ToNullableGuid(this string s)**: Converts a string to a nullable Guid. -- **StringSplit(this string s, char c)**: - - Splits a string by a specified character and trims each resulting element. -- **ToTitle(this string s)**: Converts the first character of the string to uppercase. -- **ExtractUrls(this string s)**: Extracts all URLs from a string. -- **Format(this string s, params object[] args)**: Formats a string using specified arguments. -- **Excerpt(this string s, int length = 60)**: Truncates a string to a specified length, adding ellipses if necessary. -- **ToObject(this string s)**: Deserializes a JSON string into an object of type T. -- **ToObject(this string s, List converters)**: Deserializes a JSON string into an object of type T using custom converters. -- **IsValidEmail(this string? s)**: Validates whether the string is a valid email format. -- **HtmlToPlainText(this string htmlCode)**: Converts HTML content to plain text. -- **ToCamelCase(this string input)**: Converts a string to camel case. - -### DataTable Methods - -- **DuplicatesCount(this DataTable dt1, DataTable dt2)**: Counts the number of duplicate rows between two `DataTable` objects. -- **DistinctRecords(this DataTable dt, string[] columns)**: Returns distinct records based on the specified columns. - -### DateTime Methods - -- **AddWorkdays(this DateTime date, int days, IHolidayCalendar holidayCalendar)**: Adds a specified number of workdays to a date, skipping weekends and holidays. -- **NextWeekday(this DateTime start, DayOfWeek day)**: Finds the next specified weekday from the given date. -- **IsEndOfMonth(this DateTime date)**: Checks if the date is the end of the month. -- **IsSameMonth(this DateTime date, DateTime targetDate)**: Checks if two dates are in the same month and year. -- **GetDifferenceInYears(this DateTime startDate, DateTime endDate)**: Returns the difference in years between two dates. - -### JWT Methods - -- **GenerateToken**: Generates jwt-token-generation-and-validationa JWT token. -- **ValidateToken**: Validates a JWT token and extracts claims. -- **GenerateRefreshToken**: Generates a secure refresh token. - -## Contributing - -Contributions to this project are welcome! Please fork the repository and submit a pull request with your changes. If you encounter any issues or have feature requests, feel free to open an issue on GitHub. - -## Contact - -If you have any questions or need further assistance, feel free to reach out: - -- **Email**: [maksym.sadovnychyy@gmail.com](mailto:maksym.sadovnychyy@gmail.com) -- **Reddit**: [MaksIT.Core: Enhance Your .NET Development with Efficient Extensions and Helpers](https://www.reddit.com/r/MaksIT/comments/1f8zgqa/maksitcore_enhance_your_net_development_with/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button) - -## License - -This project is licensed under the MIT License. See the full license text below. - --- -### MIT License +##### 2. **`ToJson(List?)`** +###### Summary +Converts an object of type `T` to a JSON string using default serialization options and additional custom converters. + +###### Usage +```csharp +var person = new { Name = "Alice", BirthDate = DateTime.UtcNow }; + +var converters = new List { + new JsonStringEnumConverter(), // Example custom converter +}; + +string jsonWithConverters = person.ToJson(converters); +Console.WriteLine(jsonWithConverters); + +// Output with converters may vary depending on implementation. ``` -MIT License -Copyright (c) 2024 Maksym Sadovnychyy (MAKS-IT) +--- -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +#### Notes -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +- **Use Cases**: + - Simplifies JSON serialization for objects with optional custom converters. + - Handles `null` objects gracefully by returning `"{}"`. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +- **Edge Cases**: + - When `obj` is `null`, the method always returns `"{}"`, even if custom converters are provided. + - If `converters` is `null` or empty, serialization uses only the default options. + +- **Performance**: + - Serialization leverages `System.Text.Json`, which is optimized for .NET applications. + +--- + +These extension methods simplify object-to-JSON serialization while offering flexibility with custom serialization options, making them ideal for use in APIs and other data exchange scenarios. + +--- + +### StringExtensions + +The `StringExtensions` class in the `MaksIT.Core.Extensions` namespace is an extensive library of utility methods for string manipulation, type conversions, and various transformations. It includes functionality for handling substrings, parsing, validations, and formatting, among other capabilities. + +--- + +#### Methods Overview + +##### 1. **Wildcard and Pattern Matching** + +###### **`Like`** +Checks if a string matches a pattern with SQL-like wildcard syntax (`*` and `?`). + +**Usage**: +```csharp +bool result = "example".Like("exa*e"); ``` + +--- + +##### 2. **Substring Methods** + +###### **`Left`**, **`Right`**, **`Mid`** +Extract portions of a string based on character count or position. + +**Usage**: +```csharp +string result = "example".Left(3); // "exa" +``` + +--- + +##### 3. **Conversion Methods** + +###### **`ToInteger`**, **`ToLong`**, **`ToDecimal`**, etc. +Convert strings to numeric types or nullable equivalents. + +**Usage**: +```csharp +int value = "123".ToInteger(); // 123 +int? nullableValue = "".ToNullableInt(); // null +``` + +--- + +##### 4. **Date Parsing** + +###### **`ToDate`**, **`ToDateTime`** +Parse strings into `DateTime` or `DateTime?` with optional formats. + +**Usage**: +```csharp +DateTime date = "01/01/2023".ToDate(); +DateTime? nullableDate = "".ToNullableDateTime(); +``` + +--- + +##### 5. **Boolean Conversion** + +###### **`ToBool`**, **`ToNullableBool`** +Converts strings like `"true"`, `"yes"`, or `"1"` to `bool`. + +**Usage**: +```csharp +bool result = "yes".ToBool(); // true +bool? nullableResult = "".ToNullableBool(); // null +``` + +--- + +##### 6. **Enums** + +###### **`ToEnum`**, **`ToNullableEnum`** +Parse strings to enums, with support for `[Display(Name = "...")]` attributes. + +**Usage**: +```csharp +DayOfWeek day = "Monday".ToEnum(); +DayOfWeek? nullableDay = "".ToNullableEnum(); +``` + +--- + +##### 7. **String Utilities** + +###### **`ToTitle`**, **`ToCamelCase`** +Transforms the casing of strings. + +**Usage**: +```csharp +string result = "hello world".ToTitle(); // "Hello world" +``` + +--- + +##### 8. **CSV Parsing** + +###### **`CSVToDataTable`** +Reads a CSV file and converts it into a `DataTable`. + +**Usage**: +```csharp +DataTable table = "path/to/file.csv".CSVToDataTable(); +``` + +--- + +##### 9. **Validation** + +###### **`IsInteger`**, **`IsValidEmail`**, **`IsBase32String`** +Checks whether a string matches specific formats or conditions. + +**Usage**: +```csharp +bool isEmail = "test@example.com".IsValidEmail(); // true +``` + +--- + +##### 10. **HTML Utilities** + +###### **`HtmlToPlainText`** +Converts HTML content to plain text by removing tags and entities. + +**Usage**: +```csharp +string plainText = "

Hello

".HtmlToPlainText(); // "Hello" +``` + +--- + +##### 11. **URL Extraction** + +###### **`ExtractUrls`** +Extracts distinct URLs from a string. + +**Usage**: +```csharp +var urls = "Visit http://example.com".ExtractUrls(); +``` + +--- + +##### 12. **Formatting** + +###### **`Format`** +Formats a string with specified arguments. + +```csharp +public static string Format(this string s, params object[] args); +``` + +**Usage**: +```csharp +string formatted = "Hello {0}".Format("World"); // "Hello World" +``` + +--- + +##### 13. **Serialization** + +###### **`ToObject`** +Deserializes a JSON string into an object of type `T`. + +**Usage**: +```csharp +var obj = "{\"Name\":\"John\"}".ToObject(); +``` + +--- + +##### 14. **Hashing** + +###### **`ToGuid`** +Generates a `Guid` from a string using MD5 hashing. + +**Usage**: +```csharp +Guid guid = "example".ToGuid(); +``` + +--- + +##### 15. **String Splitting** + +###### **`StringSplit`** +Splits a string by a character and trims each segment. + +**Usage**: +```csharp +string[] parts = "a, b, c".StringSplit(','); +``` + +--- + +#### Notes + +- **Performance**: Methods are optimized for common scenarios. +- **Error Handling**: Includes graceful null handling in many cases. +- **Versatility**: Suitable for APIs, data processing, and utility libraries. + +These methods enhance string handling capabilities and simplify common operations. + +--- + +### Logging + +The file logging system in the `MaksIT.Core.Logging` namespace provides a custom implementation of the `ILogger` interface. It includes a `FileLogger` to log messages to a file, a `FileLoggerProvider` to create logger instances, and an extension method to integrate the file logger into the `Microsoft.Extensions.Logging` framework. + +--- + +#### Example Usage +```csharp +var services = new ServiceCollection(); +services.AddLogging(builder => builder.AddFile("logs.txt")); + +var serviceProvider = services.BuildServiceProvider(); +var logger = serviceProvider.GetRequiredService>(); +logger.LogInformation("Logging to file!"); +``` + +--- + +#### Features + +- **Thread Safety**: Ensures thread-safe writes using a lock mechanism. +- **Customizable File Path**: Specify the log file location during configuration. +- **Integration**: Easily integrates with `Microsoft.Extensions.Logging`. +- **Log Levels**: Supports all standard log levels. + +--- + +#### Notes + +- **Performance**: The file operations use `File.AppendAllText`, which may have overhead for high-frequency logging. For high-throughput scenarios, consider using a buffered approach. +- **File Management**: Ensure proper file rotation and cleanup to avoid large log files. +- **Scoping**: Scoping is not supported in this implementation. + +--- + +This logging system is ideal for lightweight, file-based logging needs and integrates seamlessly into existing .NET applications using the `Microsoft.Extensions.Logging` framework. + +--- + +## Networking + +### NetworkConnection + +The `NetworkConnection` class in the `MaksIT.Core.Networking.Windows` namespace provides a Windows-only implementation for managing connections to network shares. It uses the Windows `mpr.dll` to create and manage connections. + +--- + +#### Namespace +`MaksIT.Core.Networking.Windows` + +#### Class +`public class NetworkConnection : IDisposable` + +--- + +#### Overview + +This class is specifically designed for Windows platforms and allows connecting to and disconnecting from network resources such as shared drives or directories. + +--- + +#### Usage +```csharp +ILogger logger = new LoggerFactory().CreateLogger(); +NetworkCredential credentials = new NetworkCredential("username", "password"); + +if (NetworkConnection.TryCreate(logger, @"\\server\share", credentials, out var connection, out var errorMessage)) { + Console.WriteLine("Connected successfully."); + connection.Dispose(); // Always dispose after use. +} else { + Console.WriteLine($"Failed to connect: {errorMessage}"); +} +``` + +--- + +#### Features + +1. **Windows-Only**: + - This class is supported only on Windows platforms. A check ensures it is not used on other operating systems. + +2. **Thread-Safe**: + - Proper thread-safety measures are in place for connection and disconnection. + +3. **Error Handling**: + - Detailed error messages are provided for connection failures. + +--- + +#### Notes + +- **Performance**: + - The implementation directly interacts with the Windows API, making it lightweight but platform-dependent. + +- **Usage**: + - Always dispose of the connection using `Dispose` to ensure proper resource cleanup. + +- **Limitations**: + - The class does not support non-Windows platforms and will throw a `PlatformNotSupportedException` if used elsewhere. + +#### Overview + +This class is ideal for managing secure connections to shared network resources in Windows environments. + +--- + +### PingPort + +The `PingPort` class in the `MaksIT.Core.Networking` namespace provides utility methods to check the reachability of a host on specified TCP or UDP ports. These methods help in network diagnostics by attempting to establish a connection to the target host and port. + +--- + +#### Methods + +##### 1. **`TryHostPort`** + +###### Summary +Tries to establish a connection to a host on a specified TCP port. + +###### Usage +```csharp +if (PingPort.TryHostPort("example.com", 80, out var errorMessage)) { + Console.WriteLine("TCP port is reachable."); +} else { + Console.WriteLine($"Failed to reach TCP port: {errorMessage}"); +} +``` + +--- + +##### 2. **`TryUDPPort`** + +###### Summary +Tries to send and receive a message to/from a host on a specified UDP port. + +###### Usage +```csharp +if (PingPort.TryUDPPort("example.com", 123, out var errorMessage)) { + Console.WriteLine("UDP port is reachable."); +} else { + Console.WriteLine($"Failed to reach UDP port: {errorMessage}"); +} +``` + +--- + +###### Features + +1. **Protocol Support**: + - `TryHostPort`: For TCP ports. + - `TryUDPPort`: For UDP ports. + +2. **Timeout Handling**: + - `TryHostPort`: Waits for up to 5 seconds for a connection. + - `TryUDPPort`: Sets a receive timeout of 5 seconds. + +3. **Error Reporting**: + - Returns a clear error message if the operation fails. + +4. **Lightweight**: + - Both methods are simple and do not require additional dependencies. + +--- + +###### Notes + +- **Performance**: + - Designed for quick diagnostics and has a default timeout of 5 seconds to avoid blocking indefinitely. + +- **Error Handling**: + - Gracefully handles exceptions and provides detailed error messages for debugging. + +- **Limitations**: + - The reachability of a UDP port depends on whether the target host sends a response to the initial message. + - Cannot guarantee service availability, only the ability to establish a connection. + +--- + +This class is ideal for quick and lightweight network port checks, particularly in diagnostic or troubleshooting tools. + +--- + +## Security + +### AESGCMUtility + +The `AESGCMUtility` class in the `MaksIT.Core.Security` namespace provides utility methods for encryption and decryption using AES-GCM (Advanced Encryption Standard in Galois/Counter Mode). It includes functionality to securely encrypt and decrypt data and generate encryption keys. + +--- + +#### Methods + +##### 1. **`TryEncryptData`** + +###### Summary +Encrypts data using AES-GCM with a base64-encoded key. The encrypted data includes the ciphertext, authentication tag, and IV. + +###### Usage +```csharp +var key = AESGCMUtility.GenerateKeyBase64(); +var data = Encoding.UTF8.GetBytes("Sensitive data"); + +if (AESGCMUtility.TryEncryptData(data, key, out var encryptedData, out var error)) { + Console.WriteLine("Encryption successful."); +} else { + Console.WriteLine($"Encryption failed: {error}"); +} +``` + +##### 2. **`TryDecryptData`** + +###### Summary +Decrypts data encrypted with AES-GCM using a base64-encoded key. The data must include the ciphertext, authentication tag, and IV. + +###### Usage +```csharp +if (AESGCMUtility.TryDecryptData(encryptedData, key, out var decryptedData, out var error)) { + Console.WriteLine($"Decryption successful: {Encoding.UTF8.GetString(decryptedData)}"); +} else { + Console.WriteLine($"Decryption failed: {error}"); +} +``` + + +##### 3. **`GenerateKeyBase64`** + +###### Summary +Generates a secure, random 256-bit AES key and returns it as a base64-encoded string. + +###### Usage +```csharp +var key = AESGCMUtility.GenerateKeyBase64(); +Console.WriteLine($"Generated Key: {key}"); +``` + +--- + +#### Features + +1. **Secure Encryption**: + - Uses AES-GCM, a secure encryption mode with built-in authentication. + +2. **Data Integrity**: + - Includes an authentication tag to ensure the ciphertext has not been tampered with. + +3. **Key Generation**: + - Supports generating random, secure AES keys. + +4. **Error Handling**: + - Provides descriptive error messages for failed encryption or decryption. + +5. **Flexible Output**: + - Encrypted data includes the IV, tag, and ciphertext, ensuring portability and compatibility. + +--- + +#### Notes + +1. **Security**: + - Always store keys securely, e.g., in a hardware security module (HSM) or a secure configuration service. + - Avoid reusing IVs with the same key, as this compromises security. + +2. **Performance**: + - AES-GCM is optimized for modern hardware and offers efficient encryption and decryption. + +3. **Error Handling**: + - The methods return `false` and provide detailed error messages if any issue occurs. + +--- + +#### Example End-to-End Usage + +```csharp +// Generate a key +var key = AESGCMUtility.GenerateKeyBase64(); + +// Data to encrypt +var plaintext = Encoding.UTF8.GetBytes("Confidential data"); + +// Encrypt +if (AESGCMUtility.TryEncryptData(plaintext, key, out var encryptedData, out var encryptError)) { + Console.WriteLine("Encryption successful."); + + // Decrypt + if (AESGCMUtility.TryDecryptData(encryptedData, key, out var decryptedData, out var decryptError)) { + Console.WriteLine($"Decryption successful: {Encoding.UTF8.GetString(decryptedData)}"); + } else { + Console.WriteLine($"Decryption failed: {decryptError}"); + } +} else { + Console.WriteLine($"Encryption failed: {encryptError}"); +} +``` + +--- + +This utility simplifies secure encryption and decryption with AES-GCM and is suitable for sensitive data in .NET applications. + +--- + +### Base32Encoder + +The `Base32Encoder` class in the `MaksIT.Core.Security` namespace provides methods to encode binary data into Base32 and decode Base32 strings back into binary data. Base32 encoding is commonly used in scenarios where binary data needs to be represented in a textual format. + +--- + +#### Methods + +##### 1. **`TryEncode`** + +###### Summary +Encodes binary data into a Base32 string. + +###### How It Works +1. Converts binary data into Base32 using 5-bit blocks. +2. Adds padding (`=`) to ensure the encoded string length is a multiple of 8. + +###### Usage +```csharp +byte[] data = Encoding.UTF8.GetBytes("Hello World"); +if (Base32Encoder.TryEncode(data, out var encoded, out var errorMessage)) { + Console.WriteLine($"Encoded: {encoded}"); +} else { + Console.WriteLine($"Encoding failed: {errorMessage}"); +} +``` + +--- + +##### 2. **`TryDecode`** + +###### Summary +Decodes a Base32 string back into binary data. + +###### How It Works +1. Removes any padding characters (`=`) from the input string. +2. Converts the Base32 string back into binary data using 5-bit blocks. + +###### Usage +```csharp +string base32 = "JBSWY3DPEBLW64TMMQ======"; +if (Base32Encoder.TryDecode(base32, out var decoded, out var errorMessage)) { + Console.WriteLine($"Decoded: {Encoding.UTF8.GetString(decoded)}"); +} else { + Console.WriteLine($"Decoding failed: {errorMessage}"); +} +``` + +--- + +#### Features + +1. **Encoding and Decoding**: + - Converts binary data into a Base32 string and vice versa. + +2. **Padding**: + - Ensures Base32-encoded strings are properly padded to be a multiple of 8 characters. + +3. **Error Handling**: + - Provides descriptive error messages if encoding or decoding fails. + +4. **Compatibility**: + - Adheres to standard Base32 encoding, making it compatible with other Base32 implementations. + +--- + +#### Notes + +1. **Use Cases**: + - Suitable for encoding binary data such as keys, tokens, and identifiers in a human-readable format. + - Commonly used in applications like TOTP (Time-Based One-Time Passwords). + +2. **Limitations**: + - Input strings for decoding must strictly follow the Base32 format. + - Encoding may include padding characters (`=`), which are not always required in certain applications. + +3. **Performance**: + - Efficient for small to medium-sized data. For large data, consider stream-based approaches. + +--- + +#### Example End-to-End Usage + +```csharp +// Original data +byte[] originalData = Encoding.UTF8.GetBytes("Example Data"); + +// Encode to Base32 +if (Base32Encoder.TryEncode(originalData, out var encoded, out var encodeError)) { + Console.WriteLine($"Encoded: {encoded}"); + + // Decode back to binary + if (Base32Encoder.TryDecode(encoded, out var decodedData, out var decodeError)) { + Console.WriteLine($"Decoded: {Encoding.UTF8.GetString(decodedData)}"); + } else { + Console.WriteLine($"Decoding failed: {decodeError}"); + } +} else { + Console.WriteLine($"Encoding failed: {encodeError}"); +} +``` + +--- + +This utility provides a robust implementation of Base32 encoding and decoding, making it ideal for use in security and data encoding scenarios. + +--- + + +### ChecksumUtility + +The `ChecksumUtility` class in the `MaksIT.Core.Security` namespace provides methods to calculate and verify CRC32 checksums for byte arrays and files. This utility is designed for efficient data integrity checks and supports chunk-based processing for large files. + +--- + +#### Methods + +##### 1. **`TryCalculateCRC32Checksum`** + +###### Summary +Calculates the CRC32 checksum for a given byte array. + +###### Usage +```csharp +byte[] data = Encoding.UTF8.GetBytes("Sample data"); +if (ChecksumUtility.TryCalculateCRC32Checksum(data, out var checksum, out var errorMessage)) { + Console.WriteLine($"CRC32 Checksum: {checksum}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +##### 2. **`TryCalculateCRC32ChecksumFromFile`** + +###### Summary +Calculates the CRC32 checksum for a file. + +###### Usage +```csharp +if (ChecksumUtility.TryCalculateCRC32ChecksumFromFile("sample.txt", out var checksum, out var errorMessage)) { + Console.WriteLine($"File CRC32 Checksum: {checksum}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +##### 3. **`TryCalculateCRC32ChecksumFromFileInChunks`** + +###### Summary +Calculates the CRC32 checksum for a file in chunks, optimizing for large files. + +###### Usage +```csharp +if (ChecksumUtility.TryCalculateCRC32ChecksumFromFileInChunks("largefile.txt", out var checksum, out var errorMessage)) { + Console.WriteLine($"Chunked File CRC32 Checksum: {checksum}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +##### 4. **`VerifyCRC32Checksum`** + +###### Summary +Verifies that a byte array matches a given CRC32 checksum. + +###### Usage +```csharp +if (ChecksumUtility.VerifyCRC32Checksum(data, "5d41402abc4b2a76b9719d911017c592")) { + Console.WriteLine("Checksum matches."); +} else { + Console.WriteLine("Checksum does not match."); +} +``` + +--- + +##### 5. **`VerifyCRC32ChecksumFromFile`** + +###### Summary +Verifies that a file matches a given CRC32 checksum. + +###### Usage +```csharp +if (ChecksumUtility.VerifyCRC32ChecksumFromFile("sample.txt", "5d41402abc4b2a76b9719d911017c592")) { + Console.WriteLine("File checksum matches."); +} else { + Console.WriteLine("File checksum does not match."); +} +``` + +--- + +##### 6. **`VerifyCRC32ChecksumFromFileInChunks`** + +###### Summary +Verifies that a file matches a given CRC32 checksum using chunk-based processing. + +###### Usage +```csharp +if (ChecksumUtility.VerifyCRC32ChecksumFromFileInChunks("largefile.txt", "5d41402abc4b2a76b9719d911017c592")) { + Console.WriteLine("Chunked file checksum matches."); +} else { + Console.WriteLine("Chunked file checksum does not match."); +} +``` + +--- + +#### Features + +1. **CRC32 Checksum Calculation**: + - Supports byte arrays and files. + - Provides chunk-based processing for large files to optimize memory usage. + +2. **Verification**: + - Verifies whether data or files match a given CRC32 checksum. + +3. **Error Handling**: + - Outputs descriptive error messages for all failures. + +4. **Flexible File Handling**: + - Offers both full-file and chunked approaches for checksum calculations. + +--- + +#### Notes + +1. **Use Cases**: + - Validate data integrity during file transfers or storage. + - Verify file authenticity by comparing checksums. + +2. **Performance**: + - Chunked methods are recommended for large files to reduce memory consumption. + +3. **Limitations**: + - CRC32 is not suitable for cryptographic purposes due to its vulnerability to collisions. + +--- + +#### Example End-to-End Usage + +```csharp +string filePath = "testfile.txt"; +string expectedChecksum = "5d41402abc4b2a76b9719d911017c592"; + +// Calculate checksum +if (ChecksumUtility.TryCalculateCRC32ChecksumFromFile(filePath, out var checksum, out var error)) { + Console.WriteLine($"Calculated Checksum: {checksum}"); + + // Verify checksum + if (ChecksumUtility.VerifyCRC32ChecksumFromFile(filePath, expectedChecksum)) { + Console.WriteLine("Checksum verification successful."); + } else { + Console.WriteLine("Checksum verification failed."); + } +} else { + Console.WriteLine($"Error calculating checksum: {error}"); +} +``` + +--- + +This utility provides efficient and reliable CRC32 checksum calculation and verification for various data integrity scenarios. + +--- + +### Crc32 + +The `Crc32` class in the `MaksIT.Core.Security` namespace provides an implementation of the CRC32 (Cyclic Redundancy Check) hash algorithm. This class extends the `HashAlgorithm` base class, enabling usage in cryptographic workflows while also providing utility methods for directly computing CRC32 checksums. + +--- + +#### Overview + +The CRC32 algorithm is a commonly used error-detection mechanism for digital data. This class supports the default CRC32 polynomial and seed, with options to specify custom values. It is designed to compute CRC32 checksums for data efficiently and includes both instance-based and static methods. + +--- + +#### Methods + +##### 1. **`Initialize`** + +Resets the hash computation to the initial state. + +##### 2. **`HashCore`** + +Processes a segment of data and updates the hash state. + +##### 3. **`HashFinal`** + +Finalizes the hash computation and returns the CRC32 checksum. + +--- + +##### 4. **Static Methods for Direct CRC32 Computation** + +###### **`TryCompute(byte[] buffer, out uint result, out string? errorMessage)`** +Computes the CRC32 checksum for a given byte array using the default polynomial and seed. + +--- + +###### **`TryCompute(uint seed, byte[] buffer, out uint result, out string? errorMessage)`** +Computes the CRC32 checksum for a given byte array using the default polynomial and a custom seed. + +```csharp +public static bool TryCompute(uint seed, byte[] buffer, out uint result, out string? errorMessage); +``` + +--- + +###### **`TryCompute(uint polynomial, uint seed, byte[] buffer, out uint result, out string? errorMessage)`** +Computes the CRC32 checksum for a given byte array using a custom polynomial and seed. + +--- + +#### Example Usage + +##### Instance-Based Hashing +```csharp +byte[] data = Encoding.UTF8.GetBytes("Example data"); +using var crc32 = new Crc32(); +byte[] hash = crc32.ComputeHash(data); +Console.WriteLine($"CRC32 Hash: {BitConverter.ToString(hash).Replace("-", "").ToLower()}"); +``` + +##### Static Method for Direct Calculation +```csharp +byte[] data = Encoding.UTF8.GetBytes("Example data"); +if (Crc32.TryCompute(data, out var result, out var errorMessage)) { + Console.WriteLine($"CRC32 Checksum: {result:X}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +##### Using Custom Polynomial and Seed +```csharp +byte[] data = Encoding.UTF8.GetBytes("Custom polynomial example"); +uint polynomial = 0x04C11DB7; // Example polynomial +uint seed = 0xFFFFFFFF; + +if (Crc32.TryCompute(polynomial, seed, data, out var result, out var errorMessage)) { + Console.WriteLine($"Custom CRC32 Checksum: {result:X}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +#### Features + +1. **Customizable**: + - Supports custom polynomials and seeds for flexibility. + +2. **Integration**: + - Extends `HashAlgorithm`, making it compatible with cryptographic workflows in .NET. + +3. **Static Utility**: + - Includes static methods for direct computation without creating an instance. + +4. **Efficiency**: + - Uses a precomputed lookup table for fast CRC32 calculation. + +5. **Error Handling**: + - Provides `TryCompute` methods with detailed error messages. + +--- + +#### Notes + +- **Use Cases**: + - Commonly used for data integrity checks in file transfers and storage. + - Can be used in networking protocols for error detection. + +- **Performance**: + - Optimized for performance using a lookup table. + +- **Limitations**: + - CRC32 is not suitable for cryptographic purposes due to its vulnerability to collisions. + +--- + +This implementation provides a robust and efficient solution for CRC32 checksum computation, suitable for data integrity and verification workflows. + +--- + +### JwtGenerator + +The `JwtGenerator` class in the `MaksIT.Core.Security` namespace provides methods for generating, validating, and managing JSON Web Tokens (JWTs). It supports creating tokens with claims, validating tokens, and generating secure secrets and refresh tokens. + +--- + +#### Purpose +Represents the claims of a JWT, including metadata such as username, roles, and token issuance and expiration times. + +--- + +#### Methods + +#### 1. **`TryGenerateToken`** + +###### Summary +Generates a JWT with the specified claims and metadata. + +###### Usage +```csharp +if (JwtGenerator.TryGenerateToken("mysecret", "myissuer", "myaudience", 60, "user123", new List { "Admin" }, out var tokenData, out var errorMessage)) { + Console.WriteLine($"Token: {tokenData?.Item1}"); + Console.WriteLine($"Expires At: {tokenData?.Item2.ExpiresAt}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +#### 2. **`GenerateSecret`** + +###### Summary +Generates a secure, random secret key for JWT signing. + +###### Usage +```csharp +string secret = JwtGenerator.GenerateSecret(); +Console.WriteLine($"Generated Secret: {secret}"); +``` + +--- + +#### 3. **`TryValidateToken`** + +###### Summary +Validates a JWT against the provided secret, issuer, and audience. + +###### Usage +```csharp +if (JwtGenerator.TryValidateToken("mysecret", "myissuer", "myaudience", "jwtTokenHere", out var claims, out var errorMessage)) { + Console.WriteLine($"Token is valid for user: {claims?.Username}"); +} else { + Console.WriteLine($"Token validation failed: {errorMessage}"); +} +``` + +--- + +#### 4. **`GenerateRefreshToken`** + +###### Summary +Generates a secure, random refresh token. + +###### Usage +```csharp +string refreshToken = JwtGenerator.GenerateRefreshToken(); +Console.WriteLine($"Refresh Token: {refreshToken}"); +``` + +--- + +#### Features + +1. **Token Generation**: + - Create JWTs with configurable claims and expiration times. + +2. **Token Validation**: + - Validate JWTs against the expected issuer, audience, and secret. + +3. **Security**: + - Uses HMAC-SHA256 for signing. + - Supports generating secure secrets and refresh tokens. + +4. **Claims Management**: + - Embed and extract custom claims such as roles and metadata. + +--- + +#### Example End-to-End Usage + +```csharp +string secret = JwtGenerator.GenerateSecret(); +string issuer = "myissuer"; +string audience = "myaudience"; + +// Generate a token +if (JwtGenerator.TryGenerateToken(secret, issuer, audience, 60, "user123", new List { "Admin" }, out var tokenData, out var errorMessage)) { + Console.WriteLine($"Generated Token: {tokenData?.Item1}"); + + // Validate the token + if (JwtGenerator.TryValidateToken(secret, issuer, audience, tokenData?.Item1, out var claims, out var validationError)) { + Console.WriteLine($"Token valid for user: {claims?.Username}"); + } else { + Console.WriteLine($"Validation failed: {validationError}"); + } +} else { + Console.WriteLine($"Token generation failed: {errorMessage}"); +} +``` + +--- + +#### Notes + +- **Use Cases**: + - API authentication and authorization. + - Session management with short-lived tokens and refresh tokens. + +- **Best Practices**: + - Store secrets securely, e.g., in a configuration service or environment variables. + - Use HTTPS to protect tokens in transit. + +- **Limitations**: + - Ensure token expiration times are appropriate for your security model. + +--- + +This class provides a comprehensive implementation for secure JWT generation and validation, suitable for modern .NET applications. + +--- + +### PasswordHasher + +The `PasswordHasher` class in the `MaksIT.Core.Security` namespace provides functionality for securely hashing passwords with salt and validating hashed passwords using the PBKDF2 (Password-Based Key Derivation Function 2) algorithm with HMAC-SHA512. + +--- + +#### Overview + +The `PasswordHasher` class includes methods for: +- Generating a secure, salted hash for a password. +- Validating a password against a stored salted hash. +- Ensuring security using best practices, such as high iteration counts and fixed-time comparison. + +--- + +#### Methods + +##### 1. **`TryCreateSaltedHash`** + +###### Summary +Creates a salted hash for the given password. + +###### Usage +```csharp +if (PasswordHasher.TryCreateSaltedHash("mypassword", out var saltedHash, out var errorMessage)) { + Console.WriteLine($"Salt: {saltedHash?.Salt}"); + Console.WriteLine($"Hash: {saltedHash?.Hash}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +##### 2. **`TryValidateHash`** + +###### Summary +Validates a password against a stored hash and salt. + +###### Usage +```csharp +if (PasswordHasher.TryValidateHash("mypassword", saltedHash.Salt, saltedHash.Hash, out var isValid, out var errorMessage)) { + Console.WriteLine(isValid ? "Password is valid." : "Password is invalid."); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +#### Features + +1. **Salted Hashing**: + - Ensures uniqueness for each hash by combining a password with a unique salt. + +2. **Secure Validation**: + - Uses `CryptographicOperations.FixedTimeEquals` for constant-time comparison, preventing timing attacks. + +3. **High Security**: + - Implements PBKDF2 with HMAC-SHA512. + - Uses 100,000 iterations for strong resistance against brute force attacks. + +4. **Error Handling**: + - Provides detailed error messages for any failure during hashing or validation. + +--- + +#### Notes + +- **Performance**: + - PBKDF2 with 100,000 iterations ensures a good balance between security and performance. The iteration count can be adjusted for specific security requirements. + +- **Use Cases**: + - Ideal for securely storing and validating user passwords in web applications and APIs. + +- **Limitations**: + - This implementation does not include password policies (e.g., minimum length, complexity), which should be enforced separately. + +--- + +#### Example End-to-End Usage + +```csharp +// Step 1: Hash a password +if (PasswordHasher.TryCreateSaltedHash("securepassword", out var saltedHash, out var errorMessage)) { + Console.WriteLine($"Salt: {saltedHash?.Salt}"); + Console.WriteLine($"Hash: {saltedHash?.Hash}"); + + // Step 2: Validate the password + if (PasswordHasher.TryValidateHash("securepassword", saltedHash.Salt, saltedHash.Hash, out var isValid, out var validationError)) { + Console.WriteLine(isValid ? "Password is valid." : "Password is invalid."); + } else { + Console.WriteLine($"Validation Error: {validationError}"); + } +} else { + Console.WriteLine($"Hashing Error: {errorMessage}"); +} +``` + +--- + +#### Best Practices + +- **Key Stretching**: + - Use high iteration counts (e.g., 100,000 or more) to make brute force attacks computationally expensive. + +- **Salt Storage**: + - Store salts separately or alongside the hash in the database. + +- **Encryption**: + - Use a secure channel (e.g., HTTPS) when transmitting passwords to prevent interception. + +--- + +The `PasswordHasher` class provides a secure and efficient implementation for password hashing and validation, adhering to modern security standards. + +--- + +### TotpGenerator + +The `TotpGenerator` class in the `MaksIT.Core.Security` namespace provides methods for generating and validating Time-Based One-Time Passwords (TOTP) as per the TOTP standard (RFC 6238). It also includes utility methods for generating shared secrets, recovery codes, and TOTP authentication links. + +--- + +#### Overview + +This class supports: +- TOTP generation and validation. +- Generation of base32-encoded shared secrets for TOTP. +- Recovery code generation. +- TOTP authentication link generation for integration with authenticator apps. + +--- + +#### Methods + +##### 1. **`TryValidate`** + +##### Summary +Validates a given TOTP code against a shared secret with time tolerance. + +##### Usage +```csharp +if (TotpGenerator.TryValidate("123456", "MZXW6YTBOI======", 1, out var isValid, out var errorMessage)) { + Console.WriteLine(isValid ? "TOTP is valid." : "TOTP is invalid."); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +##### 2. **`TryGenerate`** + +##### Summary +Generates a TOTP code for a given shared secret and time step. + +##### Usage +```csharp +if (TotpGenerator.TryGenerate("MZXW6YTBOI======", TotpGenerator.GetCurrentTimeStepNumber(), out var totpCode, out var errorMessage)) { + Console.WriteLine($"Generated TOTP: {totpCode}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +##### 3. **`GetCurrentTimeStepNumber`** + +##### Summary +Calculates the current time step number based on the current Unix timestamp. + +##### Usage +```csharp +long timestep = TotpGenerator.GetCurrentTimeStepNumber(); +Console.WriteLine($"Current Time Step: {timestep}"); +``` + +--- + +##### 4. **`TryGenerateSecret`** + +##### Summary +Generates a random shared secret encoded in base32. + +##### Usage +```csharp +if (TotpGenerator.TryGenerateSecret(out var secret, out var errorMessage)) { + Console.WriteLine($"Generated Secret: {secret}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +##### 5. **`TryGenerateRecoveryCodes`** + +##### Summary +Generates recovery codes for account recovery in case of TOTP unavailability. + +##### Usage +```csharp +if (TotpGenerator.TryGenerateRecoveryCodes(5, out var recoveryCodes, out var errorMessage)) { + Console.WriteLine("Recovery Codes:"); + recoveryCodes?.ForEach(Console.WriteLine); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +##### 6. **`TryGenerateTotpAuthLink`** + +##### Summary +Generates an OTPAuth link for use with authenticator apps. + +##### Usage +```csharp +if (TotpGenerator.TryGenerateTotpAuthLink("MyApp", "user@example.com", "MZXW6YTBOI======", "MyIssuer", "SHA1", 6, 30, out var authLink, out var errorMessage)) { + Console.WriteLine($"Auth Link: {authLink}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} +``` + +--- + +#### Features + +1. **TOTP Support**: + - Generate and validate TOTPs with configurable tolerance. + +2. **Shared Secrets**: + - Generate base32-encoded secrets for use in TOTP. + +3. **Recovery Codes**: + - Generate recovery codes for account recovery scenarios. + +4. **Integration**: + - Create OTPAuth links for easy integration with authenticator apps. + +--- + +#### Notes + +- **Use Cases**: + - Two-factor authentication (2FA) for applications. + - Secure account recovery workflows. + +- **Security**: + - Use HTTPS for secure transmission of secrets and TOTPs. + - Store shared secrets securely in a trusted configuration or key vault. + +--- + +This class provides a robust implementation for managing TOTP workflows and integrating with authenticator apps. + +--- + +## Web API Models + +This documentation provides an overview of classes in the `MaksIT.Core.Webapi.Models` namespace, which are designed to facilitate Web API request and response handling. These models include functionality for pagination, filtering, patching operations, and dynamic LINQ expressions. + +--- + +### Classes and Enums + +#### **1. `PagedRequest`** + +#### Summary +Represents a request model for paginated data retrieval, with support for filtering, sorting, and pagination. + +#### Usage +```csharp +var request = new PagedRequest { + PageNumber = 1, + PageSize = 10, + Filters = "Name='John' && Age>30" +}; + +var filterExpression = request.BuildFilterExpression("Name='John' && Age>30"); +``` + +--- + +#### **2. `PagedResponse`** + +##### Summary +Represents a response model for paginated data, including metadata about pagination and the data itself. + +##### Usage +```csharp +var response = new PagedResponse(users, totalCount: 100, pageNumber: 1, pageSize: 10); +``` + +--- + +#### **3. `PatchField`** + +##### Summary +Represents a patch operation on a specific field or value, used for partial updates in Web APIs. + +##### Usage +```csharp +var patchField = new PatchField { + Operation = PatchOperation.Replace, + Value = "New Value" +}; +``` + +--- + +#### **4. `PatchOperation`** + +##### Summary +Enumerates the types of patch operations that can be performed on a field or collection. + +##### Usage +```csharp +var operation = PatchOperation.Replace; +``` + +--- + +### Features and Benefits + +1. **Pagination**: + - Simplifies paginated data handling with consistent request and response models. + +2. **Dynamic Filtering**: + - Supports dynamic LINQ expressions for filtering datasets. + +3. **Partial Updates**: + - Facilitates patch operations for efficient updates without replacing entire objects. + +4. **Extensibility**: + - Designed as base models, allowing customization and extension. + +--- + +### Example End-to-End Usage + +#### Paginated Data Retrieval +```csharp +var request = new PagedRequest { + PageNumber = 1, + PageSize = 10, + Filters = "Age>25 && IsActive=true" +}; + +// Build a LINQ expression +var filterExpression = request.BuildFilterExpression("Age>25"); + +// Generate a paginated response +var pagedResponse = new PagedResponse(users, totalCount: 100, pageNumber: 1, pageSize: 10); +Console.WriteLine($"Total Pages: {pagedResponse.TotalPages}"); +``` + +### Partial Updates Using PatchField +```csharp +var patch = new PatchField { + Operation = PatchOperation.Replace, + Value = "Updated Name" +}; + +// Deconstruct the patch field +var (operation, value) = patch; +Console.WriteLine($"Operation: {operation}, Value: {value}"); +``` + +--- + +#### Best Practices + +1. **Validation**: + - Validate `PagedRequest` properties to ensure proper filtering and sorting inputs. + +2. **Error Handling**: + - Handle edge cases, such as invalid filters or unsupported patch operations. + +3. **Custom Extensions**: + - Extend `PagedRequest` and `PagedResponse` to include additional metadata or logic as needed. + +--- + +This set of classes provides a robust framework for managing Web API requests and responses, focusing on dynamic query capabilities, efficient data handling, and flexible update mechanisms. + +--- + +## Others + +### Culture + +The `Culture` class in the `MaksIT.Core` namespace provides a utility method for managing and setting the culture for the current thread. This is particularly useful for applications that need to support multiple cultures or require culture-specific behavior. + +--- + +#### Overview + +The `Culture` class simplifies culture management for the current thread by allowing the culture and UI culture to be set dynamically. It ensures thread safety and uses the invariant culture as a fallback when no specific culture is provided. + +--- + +#### Methods + +##### **`TrySet`** + +###### Summary +Sets the culture and UI culture for the current thread. + +--- + +###### Usage +```csharp +if (Culture.TrySet("fr-FR", out var errorMessage)) { + Console.WriteLine("Culture set to French (France)."); +} else { + Console.WriteLine($"Failed to set culture: {errorMessage}"); +} + +// Set to the invariant culture +if (Culture.TrySet(null, out errorMessage)) { + Console.WriteLine("Culture set to invariant culture."); +} else { + Console.WriteLine($"Failed to set culture: {errorMessage}"); +} +``` + +--- + +#### Features + +1. **Dynamic Culture Setting**: + - Allows changing the culture for the current thread dynamically. + +2. **Fallback to Invariant Culture**: + - Automatically uses the invariant culture when no specific culture is provided. + +3. **Error Handling**: + - Provides detailed error messages if the culture setting fails. + +4. **Thread Safety**: + - Ensures that the culture changes only affect the current thread. + +--- + +#### Notes + +- **Use Cases**: + - Localizing applications for different regions and languages. + - Ensuring consistent culture settings for thread-specific operations, such as formatting or parsing dates and numbers. + +- **Error Handling**: + - If an invalid culture string is provided, an exception will be caught, and the error message will describe the issue. + +- **Limitations**: + - This class only affects the current thread. For global culture settings, consider setting the culture for the entire application domain. + +--- + +#### Example End-to-End Usage + +```csharp +// Set culture to French (France) +if (Culture.TrySet("fr-FR", out var errorMessage)) { + Console.WriteLine("Culture set to French (France)."); + Console.WriteLine($"CurrentCulture: {Thread.CurrentThread.CurrentCulture}"); + Console.WriteLine($"CurrentUICulture: {Thread.CurrentThread.CurrentUICulture}"); +} else { + Console.WriteLine($"Error: {errorMessage}"); +} + +// Attempt to set an invalid culture +if (!Culture.TrySet("invalid-culture", out errorMessage)) { + Console.WriteLine($"Expected failure: {errorMessage}"); +} + +// Reset to invariant culture +if (Culture.TrySet(null, out errorMessage)) { + Console.WriteLine("Culture reset to invariant culture."); + Console.WriteLine($"CurrentCulture: {Thread.CurrentThread.CurrentCulture}"); +} +``` + +--- + +#### Best Practices + +- Always validate input culture strings to ensure they conform to supported culture formats. +- Use this utility method in localized applications where culture settings need to be adjusted dynamically for individual threads. + +--- + +This class provides a simple and effective way to manage culture settings for thread-specific operations, making it a valuable tool for applications that require localization or dynamic culture management. + + + +### EnvVar + +The `EnvVar` class in the `MaksIT.Core` namespace provides utilities for managing environment variables, including adding paths to the `PATH` environment variable and setting or unsetting environment variables at different scopes. + +--- + +#### Overview + +The `EnvVar` class supports: +- Adding a path to the `PATH` environment variable. +- Setting and unsetting environment variables for `Process`, `User`, or `Machine` scopes. +- Cross-platform compatibility with appropriate checks for platform-specific operations. + +--- + +#### Methods + +##### 1. **`TryAddToPath`** + +###### Summary +Adds a new path to the `PATH` environment variable if it is not already present. + +###### Usage +```csharp +if (EnvVar.TryAddToPath("/usr/local/bin", out var errorMessage)) { + Console.WriteLine("Path added successfully."); +} else { + Console.WriteLine($"Failed to add path: {errorMessage}"); +} +``` + +--- + +##### 2. **`TrySet`** + +###### Summary +Sets an environment variable at a specified scope. + +###### Usage +```csharp +if (EnvVar.TrySet("MY_ENV_VAR", "my_value", "user", out var errorMessage)) { + Console.WriteLine("Environment variable set successfully."); +} else { + Console.WriteLine($"Failed to set environment variable: {errorMessage}"); +} +``` + +--- + +##### 3. **`TryUnSet`** + +###### Summary +Unsets (removes) an environment variable at a specified scope. + +###### Usage +```csharp +if (EnvVar.TryUnSet("MY_ENV_VAR", "user", out var errorMessage)) { + Console.WriteLine("Environment variable unset successfully."); +} else { + Console.WriteLine($"Failed to unset environment variable: {errorMessage}"); +} +``` + +--- + +#### Features + +1. **Environment Variable Management**: + - Supports setting, unsetting, and updating environment variables dynamically. + +2. **Platform Awareness**: + - Handles platform-specific differences (e.g., path separators and machine-level variables on Windows only). + +3. **Error Handling**: + - Provides descriptive error messages for failed operations. + +4. **Cross-Scope Support**: + - Operates on `Process`, `User`, or `Machine` scopes, depending on requirements. + +--- + +#### Notes + +- **Platform-Specific Operations**: + - Machine-level operations are only supported on Windows. + - Path separators (`;` or `:`) are handled automatically based on the operating system. + +- **Thread-Safety**: + - Changes to environment variables affect the entire process and may impact other threads. + +- **Use Cases**: + - Dynamically configuring environment variables for applications. + - Modifying the `PATH` variable for runtime dependencies. + +--- + +#### Example End-to-End Usage + +```csharp +// Add a new path to PATH +if (EnvVar.TryAddToPath("/usr/local/bin", out var pathError)) { + Console.WriteLine("Path added to PATH successfully."); +} else { + Console.WriteLine($"Failed to add path: {pathError}"); +} + +// Set an environment variable +if (EnvVar.TrySet("MY_ENV_VAR", "value123", "user", out var setError)) { + Console.WriteLine("Environment variable set successfully."); +} else { + Console.WriteLine($"Failed to set variable: {setError}"); +} + +// Unset an environment variable +if (EnvVar.TryUnSet("MY_ENV_VAR", "user", out var unsetError)) { + Console.WriteLine("Environment variable unset successfully."); +} else { + Console.WriteLine($"Failed to unset variable: {unsetError}"); +} +``` + +--- + +#### Best Practices + +- Use **process-level** variables for temporary configurations that do not require persistence. +- Use **user-level** variables for configurations specific to a user account. +- Avoid modifying **machine-level** variables unless necessary, and always test the impact on the system. + +--- + +This class provides a convenient and platform-aware way to manage environment variables, making it suitable for dynamic runtime configurations. + +--- + +### FileSystem + +The `FileSystem` class in the `MaksIT.Core` namespace provides utility methods for working with the file system, including file and folder operations, resolving wildcard paths, and handling duplicate file names. + +--- + +#### Overview + +The `FileSystem` class supports: +- Copying files and folders to a target directory. +- Deleting files and directories. +- Resolving paths with wildcards. +- Handling file name duplication by appending a counter. +- Ensures cross-platform compatibility with platform-specific handling for Windows and other OSes. + +--- + +#### Methods + +#### 1. **`TryCopyToFolder`** + +##### Summary +Copies a file or the contents of a folder to a specified destination folder. + +##### Usage +```csharp +if (FileSystem.TryCopyToFolder("sourcePath", "destinationPath", true, out var errorMessage)) { + Console.WriteLine("Copy operation successful."); +} else { + Console.WriteLine($"Copy operation failed: {errorMessage}"); +} +``` + +--- + +#### 2. **`TryDeleteFileOrDirectory`** + +##### Summary +Deletes a file or directory at the specified path. + +##### Usage +```csharp +if (FileSystem.TryDeleteFileOrDirectory("pathToItem", out var errorMessage)) { + Console.WriteLine("Delete operation successful."); +} else { + Console.WriteLine($"Delete operation failed: {errorMessage}"); +} +``` + +--- + +#### 3. **`ResolveWildcardedPath`** + +##### Summary +Resolves paths with wildcards and returns all matching paths. + +##### Usage +```csharp +var resolvedPaths = FileSystem.ResolveWildcardedPath("?:\\Users\\*\\AppData\\Roaming\\*"); +resolvedPaths.ForEach(Console.WriteLine); +``` + +--- + +#### 4. **`DuplicateFileNameCheck`** + +##### Summary +Checks for file name duplication and appends a counter if a duplicate is found. + +##### Usage +```csharp +var uniqueFilePath = FileSystem.DuplicateFileNameCheck("path/to/file.txt"); +Console.WriteLine($"Unique file path: {uniqueFilePath}"); +``` + +--- + +#### Features + +1. **File and Folder Operations**: + - Copy files or entire folders with an option to overwrite existing files. + - Delete files and directories, including recursive deletion for directories. + +2. **Wildcard Path Resolution**: + - Resolve and return all matching paths for wildcard-based patterns. + +3. **Duplicate File Name Handling**: + - Automatically appends a counter to duplicate file names to ensure uniqueness. + +4. **Cross-Platform Support**: + - Handles platform-specific path separators and behaviors. + +--- + +#### Notes + +- **Platform Awareness**: + - Uses `RuntimeInformation` to differentiate between Windows and non-Windows environments for platform-specific operations. + +- **Error Handling**: + - All methods provide detailed error messages for failed operations. + +- **Use Cases**: + - Managing file and folder content dynamically. + - Handling file system paths with wildcard patterns. + - Avoiding file name collisions during file operations. + +--- + +#### Example End-to-End Usage + +```csharp +// Example 1: Copy a folder +if (FileSystem.TryCopyToFolder("sourceFolderPath", "destinationFolderPath", true, out var copyError)) { + Console.WriteLine("Folder copied successfully."); +} else { + Console.WriteLine($"Error copying folder: {copyError}"); +} + +// Example 2: Delete a file or folder +if (FileSystem.TryDeleteFileOrDirectory("pathToDelete", out var deleteError)) { + Console.WriteLine("Item deleted successfully."); +} else { + Console.WriteLine($"Error deleting item: {deleteError}"); +} + +// Example 3: Resolve wildcard paths +var paths = FileSystem.ResolveWildcardedPath("?:\\Users\\*\\Documents\\*"); +paths.ForEach(Console.WriteLine); + +// Example 4: Handle duplicate file names +var uniqueFile = FileSystem.DuplicateFileNameCheck("path/to/myFile.txt"); +Console.WriteLine($"Unique file path: {uniqueFile}"); +``` + +--- + +#### Best Practices + +- Ensure proper permissions for file and directory operations. +- Handle exceptions for inaccessible paths when using wildcard resolutions. +- Use `DuplicateFileNameCheck` to prevent overwriting existing files unintentionally. + +--- + +The `FileSystem` class provides a comprehensive set of utilities for dynamic file system operations, making it ideal for applications requiring flexible and robust file management capabilities. + + +### Processes + +The `Processes` class in the `MaksIT.Core` namespace provides helper methods to manage system processes, allowing you to start new processes or kill existing ones by name. + +--- + +#### Overview + +The `Processes` class supports: +- Starting new processes with optional arguments and timeout handling. +- Killing processes by name, with support for wildcard matching (`*` and `?`). + +#### Key Features: +- **Start Process**: Launch a process with customizable settings. +- **Kill Process**: Terminate processes by name, including wildcard support. + +--- + +#### Methods + +##### 1. **`TryStart`** + +###### Summary +Starts a new process with specified arguments and settings. + +###### Usage +```csharp +if (Processes.TryStart("notepad.exe", "", 0, false, out var errorMessage)) { + Console.WriteLine("Process started successfully."); +} else { + Console.WriteLine($"Failed to start process: {errorMessage}"); +} +``` + +--- + +##### 2. **`TryKill`** + +###### Summary +Terminates processes matching the specified name. + +###### Usage +```csharp +if (Processes.TryKill("notepad*", out var errorMessage)) { + Console.WriteLine("Processes killed successfully."); +} else { + Console.WriteLine($"Failed to kill processes: {errorMessage}"); +} +``` + +--- + +#### Features + +1. **Process Management**: + - Start new processes with optional timeout and window visibility settings. + - Kill processes by name, with wildcard matching for flexible targeting. + +2. **Error Handling**: + - Provides detailed error messages for any failed operations. + +3. **Wildcard Support**: + - Matches process names using patterns (`*` and `?`) to handle dynamic process names. + +4. **Customizable Execution**: + - Configure process settings such as visibility (`silent`) and wait time (`timeout`). + +--- + +#### Notes + +- **Thread-Safety**: + - Process management may affect other threads in the application. + +- **Platform-Specific Behavior**: + - Works on all platforms supported by .NET, but behavior may vary depending on the operating system. + +- **Use Cases**: + - Automating the launch of system tools or scripts. + - Cleaning up orphaned or unnecessary processes in an automated environment. + +--- + +#### Example End-to-End Usage + +```csharp +// Example 1: Start a process +if (Processes.TryStart("notepad.exe", "", 10, false, out var startError)) { + Console.WriteLine("Notepad started successfully."); +} else { + Console.WriteLine($"Error starting Notepad: {startError}"); +} + +// Example 2: Kill processes matching a pattern +if (Processes.TryKill("notepad*", out var killError)) { + Console.WriteLine("Notepad processes killed successfully."); +} else { + Console.WriteLine($"Error killing Notepad processes: {killError}"); +} +``` + +--- + +#### Best Practices + +1. **Error Handling**: + - Always check the return value and error messages to handle exceptions gracefully. + +2. **Timeout Configuration**: + - Use a reasonable timeout when waiting for a process to exit to avoid indefinite blocking. + +3. **Wildcard Patterns**: + - Use precise patterns to avoid unintended process termination when using `TryKill`. + +4. **Permission Handling**: + - Ensure the application has appropriate permissions to start or terminate processes. + +--- + +The `Processes` class provides a robust utility for managing processes in .NET applications, suitable for both user-level and system-level automation tasks. + +--- \ No newline at end of file diff --git a/src/MaksIT.Core.Tests/Security/AESGCMUtilityTests.cs b/src/MaksIT.Core.Tests/Security/AESGCMUtilityTests.cs new file mode 100644 index 0000000..f134f38 --- /dev/null +++ b/src/MaksIT.Core.Tests/Security/AESGCMUtilityTests.cs @@ -0,0 +1,98 @@ +using MaksIT.Core.Security; + +namespace MaksIT.Core.Tests.Security { + public class AESGCMUtilityTests { + [Fact] + public void EncryptData_ValidData_ReturnsEncryptedData() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Sensitive data"); + var key = AESGCMUtility.GenerateKeyBase64(); + + // Act + var result = AESGCMUtility.TryEncryptData(data, key, out var encryptedData, out var errorMessage); + + // Assert + Assert.True(result); + Assert.NotNull(encryptedData); + Assert.Null(errorMessage); + } + + [Fact] + public void EncryptData_InvalidKey_ReturnsError() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Sensitive data"); + var invalidKey = "InvalidBase64Key"; + + // Act + var result = AESGCMUtility.TryEncryptData(data, invalidKey, out var encryptedData, out var errorMessage); + + // Assert + Assert.False(result); + Assert.Null(encryptedData); + Assert.NotNull(errorMessage); + } + + [Fact] + public void DecryptData_ValidData_ReturnsDecryptedData() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Sensitive data"); + var key = AESGCMUtility.GenerateKeyBase64(); + AESGCMUtility.TryEncryptData(data, key, out var encryptedData, out var encryptErrorMessage); + + // Act + var result = AESGCMUtility.TryDecryptData(encryptedData, key, out var decryptedData, out var errorMessage); + + // Assert + Assert.True(result); + Assert.NotNull(decryptedData); + Assert.Equal(data, decryptedData); + Assert.Null(errorMessage); + } + + [Fact] + public void DecryptData_InvalidKey_ReturnsError() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Sensitive data"); + var key = AESGCMUtility.GenerateKeyBase64(); + AESGCMUtility.TryEncryptData(data, key, out var encryptedData, out var encryptErrorMessage); + var invalidKey = AESGCMUtility.GenerateKeyBase64(); // Different key + + // Act + var result = AESGCMUtility.TryDecryptData(encryptedData, invalidKey, out var decryptedData, out var errorMessage); + + // Assert + Assert.False(result); + Assert.Null(decryptedData); + Assert.NotNull(errorMessage); + } + + [Fact] + public void DecryptData_ModifiedData_ReturnsError() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Sensitive data"); + var key = AESGCMUtility.GenerateKeyBase64(); + AESGCMUtility.TryEncryptData(data, key, out var encryptedData, out var encryptErrorMessage); + + // Modify the encrypted data + encryptedData[0] ^= 0xFF; + + // Act + var result = AESGCMUtility.TryDecryptData(encryptedData, key, out var decryptedData, out var errorMessage); + + // Assert + Assert.False(result); + Assert.Null(decryptedData); + Assert.NotNull(errorMessage); + } + + [Fact] + public void GenerateKeyBase64_ReturnsValidBase64String() { + // Act + var key = AESGCMUtility.GenerateKeyBase64(); + + // Assert + Assert.False(string.IsNullOrWhiteSpace(key)); + Assert.Equal(44, key.Length); // 32 bytes in Base64 is 44 characters + } + } +} diff --git a/src/MaksIT.Core.Tests/Security/Base32EncoderTests.cs b/src/MaksIT.Core.Tests/Security/Base32EncoderTests.cs index 52facca..9540378 100644 --- a/src/MaksIT.Core.Tests/Security/Base32EncoderTests.cs +++ b/src/MaksIT.Core.Tests/Security/Base32EncoderTests.cs @@ -1,53 +1,65 @@ using System.Text; - using MaksIT.Core.Security; +using Xunit; +namespace MaksIT.Core.Tests.Security { + public class Base32EncoderTests { + [Fact] + public void Encode_ValidInput_ReturnsExpectedBase32String() { + // Arrange + var input = Encoding.UTF8.GetBytes("Hello World"); + var expected = "JBSWY3DPEBLW64TMMQ======"; -namespace MaksIT.Core.Tests.Security; + // Act + var result = Base32Encoder.TryEncode(input, out var encoded, out var errorMessage); -public class Base32EncoderTests { - [Fact] - public void Encode_ValidInput_ReturnsExpectedBase32String() { - // Arrange - var input = Encoding.UTF8.GetBytes("Hello World"); - var expected = "JBSWY3DPEBLW64TMMQ======"; + // Assert + Assert.True(result); + Assert.Equal(expected, encoded); + Assert.Null(errorMessage); + } - // Act - var result = Base32Encoder.Encode(input); + [Fact] + public void Decode_ValidBase32String_ReturnsExpectedByteArray() { + // Arrange + var input = "JBSWY3DPEBLW64TMMQ======"; + var expected = Encoding.UTF8.GetBytes("Hello World"); - // Assert - Assert.Equal(expected, result); - } + // Act + var result = Base32Encoder.TryDecode(input, out var decoded, out var errorMessage); - [Fact] - public void Decode_ValidBase32String_ReturnsExpectedByteArray() { - // Arrange - var input = "JBSWY3DPEBLW64TMMQ======"; - var expected = Encoding.UTF8.GetBytes("Hello World"); + // Assert + Assert.True(result); + Assert.Equal(expected, decoded); + Assert.Null(errorMessage); + } - // Act - var result = Base32Encoder.Decode(input); + [Fact] + public void Decode_InvalidBase32String_ReturnsFalse() { + // Act + var result = Base32Encoder.TryDecode("InvalidBase32String", out var decoded, out var errorMessage); - // Assert - Assert.Equal(expected, result); - } + // Assert + Assert.False(result); + Assert.Null(decoded); + Assert.NotNull(errorMessage); + } - [Fact] - public void Decode_InvalidBase32String_ThrowsFormatException() { - // Act & Assert - Assert.Throws(() => Base32Encoder.Decode("InvalidBase32String")); - } + [Fact] + public void EncodeDecode_RoundTrip_ReturnsOriginalData() { + // Arrange + var originalData = Encoding.UTF8.GetBytes("RoundTripTest"); - [Fact] - public void EncodeDecode_RoundTrip_ReturnsOriginalData() { - // Arrange - var originalData = Encoding.UTF8.GetBytes("RoundTripTest"); + // Act + var encodeResult = Base32Encoder.TryEncode(originalData, out var encoded, out var encodeErrorMessage); + var decodeResult = Base32Encoder.TryDecode(encoded, out var decoded, out var decodeErrorMessage); - // Act - var encoded = Base32Encoder.Encode(originalData); - var decoded = Base32Encoder.Decode(encoded); - - // Assert - Assert.Equal(originalData, decoded); + // Assert + Assert.True(encodeResult); + Assert.True(decodeResult); + Assert.Equal(originalData, decoded); + Assert.Null(encodeErrorMessage); + Assert.Null(decodeErrorMessage); + } } } diff --git a/src/MaksIT.Core.Tests/Security/ChecksumUtilityTests.cs b/src/MaksIT.Core.Tests/Security/ChecksumUtilityTests.cs new file mode 100644 index 0000000..c50e993 --- /dev/null +++ b/src/MaksIT.Core.Tests/Security/ChecksumUtilityTests.cs @@ -0,0 +1,180 @@ +using System; +using System.IO; +using Xunit; +using MaksIT.Core.Security; + +namespace MaksIT.Core.Tests.Security { + public class ChecksumUtilityTests { + [Fact] + public void CalculateCRC32Checksum_ValidData_ReturnsChecksum() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Test data"); + + // Act + var result = ChecksumUtility.TryCalculateCRC32Checksum(data, out var checksum, out var errorMessage); + + // Assert + Assert.True(result); + Assert.NotNull(checksum); + Assert.Null(errorMessage); + } + + [Fact] + public void CalculateCRC32ChecksumFromFile_ValidFile_ReturnsChecksum() { + // Arrange + var filePath = Path.GetTempFileName(); + File.WriteAllText(filePath, "Test data"); + + // Act + var result = ChecksumUtility.TryCalculateCRC32ChecksumFromFile(filePath, out var checksum, out var errorMessage); + + // Assert + Assert.True(result); + Assert.NotNull(checksum); + Assert.Null(errorMessage); + + // Cleanup + File.Delete(filePath); + } + + [Fact] + public void CalculateCRC32ChecksumFromFile_FileNotFound_ReturnsError() { + // Arrange + var filePath = "nonexistentfile.txt"; + + // Act + var result = ChecksumUtility.TryCalculateCRC32ChecksumFromFile(filePath, out var checksum, out var errorMessage); + + // Assert + Assert.False(result); + Assert.Null(checksum); + Assert.NotNull(errorMessage); + } + + [Fact] + public void CalculateCRC32ChecksumFromFileInChunks_ValidFile_ReturnsChecksum() { + // Arrange + var filePath = Path.GetTempFileName(); + File.WriteAllText(filePath, "Test data"); + + // Act + var result = ChecksumUtility.TryCalculateCRC32ChecksumFromFileInChunks(filePath, out var checksum, out var errorMessage); + + // Assert + Assert.True(result); + Assert.NotNull(checksum); + Assert.Null(errorMessage); + + // Cleanup + File.Delete(filePath); + } + + [Fact] + public void CalculateCRC32ChecksumFromFileInChunks_FileNotFound_ReturnsError() { + // Arrange + var filePath = "nonexistentfile.txt"; + + // Act + var result = ChecksumUtility.TryCalculateCRC32ChecksumFromFileInChunks(filePath, out var checksum, out var errorMessage); + + // Assert + Assert.False(result); + Assert.Null(checksum); + Assert.NotNull(errorMessage); + } + + [Fact] + public void VerifyCRC32Checksum_ValidData_ReturnsTrue() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Test data"); + ChecksumUtility.TryCalculateCRC32Checksum(data, out var checksum, out var errorMessage); + + // Act + var result = ChecksumUtility.VerifyCRC32Checksum(data, checksum); + + // Assert + Assert.True(result); + } + + [Fact] + public void VerifyCRC32Checksum_InvalidChecksum_ReturnsFalse() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Test data"); + var invalidChecksum = "00000000"; + + // Act + var result = ChecksumUtility.VerifyCRC32Checksum(data, invalidChecksum); + + // Assert + Assert.False(result); + } + + [Fact] + public void VerifyCRC32ChecksumFromFile_ValidFile_ReturnsTrue() { + // Arrange + var filePath = Path.GetTempFileName(); + File.WriteAllText(filePath, "Test data"); + ChecksumUtility.TryCalculateCRC32ChecksumFromFile(filePath, out var checksum, out var errorMessage); + + // Act + var result = ChecksumUtility.VerifyCRC32ChecksumFromFile(filePath, checksum); + + // Assert + Assert.True(result); + + // Cleanup + File.Delete(filePath); + } + + [Fact] + public void VerifyCRC32ChecksumFromFile_InvalidChecksum_ReturnsFalse() { + // Arrange + var filePath = Path.GetTempFileName(); + File.WriteAllText(filePath, "Test data"); + var invalidChecksum = "00000000"; + + // Act + var result = ChecksumUtility.VerifyCRC32ChecksumFromFile(filePath, invalidChecksum); + + // Assert + Assert.False(result); + + // Cleanup + File.Delete(filePath); + } + + [Fact] + public void VerifyCRC32ChecksumFromFileInChunks_ValidFile_ReturnsTrue() { + // Arrange + var filePath = Path.GetTempFileName(); + File.WriteAllText(filePath, "Test data"); + ChecksumUtility.TryCalculateCRC32ChecksumFromFileInChunks(filePath, out var checksum, out var errorMessage); + + // Act + var result = ChecksumUtility.VerifyCRC32ChecksumFromFileInChunks(filePath, checksum); + + // Assert + Assert.True(result); + + // Cleanup + File.Delete(filePath); + } + + [Fact] + public void VerifyCRC32ChecksumFromFileInChunks_InvalidChecksum_ReturnsFalse() { + // Arrange + var filePath = Path.GetTempFileName(); + File.WriteAllText(filePath, "Test data"); + var invalidChecksum = "00000000"; + + // Act + var result = ChecksumUtility.VerifyCRC32ChecksumFromFileInChunks(filePath, invalidChecksum); + + // Assert + Assert.False(result); + + // Cleanup + File.Delete(filePath); + } + } +} diff --git a/src/MaksIT.Core.Tests/Security/Crc32Tests.cs b/src/MaksIT.Core.Tests/Security/Crc32Tests.cs new file mode 100644 index 0000000..ebb7a38 --- /dev/null +++ b/src/MaksIT.Core.Tests/Security/Crc32Tests.cs @@ -0,0 +1,107 @@ +using System; +using System.Security.Cryptography; +using Xunit; +using MaksIT.Core.Security; + +namespace MaksIT.Core.Tests.Security { + public class Crc32Tests { + [Fact] + public void Crc32_DefaultConstructor_InitializesCorrectly() { + // Arrange & Act + using var crc32 = new Crc32(); + + // Assert + Assert.NotNull(crc32); + Assert.Equal(32, crc32.HashSize); + } + + [Fact] + public void Crc32_ComputeHash_ReturnsExpectedHash() { + // Arrange + using var crc32 = new Crc32(); + var data = System.Text.Encoding.UTF8.GetBytes("Test data"); + + // Act + var hash = crc32.ComputeHash(data); + + // Assert + Assert.NotNull(hash); + Assert.Equal(4, hash.Length); // CRC32 hash length is 4 bytes + } + + [Fact] + public void Crc32_TryCompute_ValidData_ReturnsTrue() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Test data"); + + // Act + var result = Crc32.TryCompute(data, out var checksum, out var errorMessage); + + // Assert + Assert.True(result); + Assert.NotEqual(0u, checksum); + Assert.Null(errorMessage); + } + + [Fact] + public void Crc32_TryCompute_InvalidData_ReturnsFalse() { + // Arrange + byte[] data = null; + + // Act + var result = Crc32.TryCompute(data, out var checksum, out var errorMessage); + + // Assert + Assert.False(result); + Assert.Equal(0u, checksum); + Assert.NotNull(errorMessage); + } + + [Fact] + public void Crc32_TryCompute_WithSeed_ReturnsExpectedHash() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Test data"); + uint seed = 0x12345678; + + // Act + var result = Crc32.TryCompute(seed, data, out var checksum, out var errorMessage); + + // Assert + Assert.True(result); + Assert.NotEqual(0u, checksum); + Assert.Null(errorMessage); + } + + [Fact] + public void Crc32_TryCompute_WithPolynomialAndSeed_ReturnsExpectedHash() { + // Arrange + var data = System.Text.Encoding.UTF8.GetBytes("Test data"); + uint polynomial = 0x04C11DB7; + uint seed = 0x12345678; + + // Act + var result = Crc32.TryCompute(polynomial, seed, data, out var checksum, out var errorMessage); + + // Assert + Assert.True(result); + Assert.NotEqual(0u, checksum); + Assert.Null(errorMessage); + } + + [Fact] + public void Crc32_Initialize_ResetsHash() { + // Arrange + using var crc32 = new Crc32(); + var data = System.Text.Encoding.UTF8.GetBytes("Test data"); + crc32.ComputeHash(data); + + // Act + crc32.Initialize(); + var hash = crc32.ComputeHash(data); + + // Assert + Assert.NotNull(hash); + Assert.Equal(4, hash.Length); // CRC32 hash length is 4 bytes + } + } +} diff --git a/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs index ba5af52..61228ee 100644 --- a/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs +++ b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs @@ -1,70 +1,78 @@ -using MaksIT.Core.Security; +using System.Collections.Generic; +using Xunit; +using MaksIT.Core.Security; +namespace MaksIT.Core.Tests.Security { + public class JwtGeneratorTests { + private const string Secret = "supersecretkey12345678901234567890"; + private const string Issuer = "testIssuer"; + private const string Audience = "testAudience"; + private const double Expiration = 30; // 30 minutes + private const string Username = "testUser"; + private readonly List Roles = new List { "Admin", "User" }; -namespace MaksIT.Core.Tests.Security; + [Fact] + public void GenerateToken_ShouldReturnValidToken() { + // Act + var result = JwtGenerator.TryGenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles, out var tokenData, out var errorMessage); -public class JwtGeneratorTests { - private const string Secret = "supersecretkey12345678901234567890"; - private const string Issuer = "testIssuer"; - private const string Audience = "testAudience"; - private const double Expiration = 30; // 30 minutes - private const string Username = "testUser"; - private readonly List Roles = new List { "Admin", "User" }; + // Assert + Assert.True(result); + Assert.NotNull(tokenData); + Assert.False(string.IsNullOrEmpty(tokenData?.Item1)); + Assert.Null(errorMessage); + } - [Fact] - public void GenerateToken_ShouldReturnValidToken() { - // Act - var (token, jwtTokenClaims) = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles); + [Fact] + public void ValidateToken_ShouldReturnClaimsPrincipal_WhenTokenIsValid() { + // Arrange + JwtGenerator.TryGenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles, out var tokenData, out var generateErrorMessage); - // Assert - Assert.False(string.IsNullOrEmpty(token)); - } + // Act + var result = JwtGenerator.TryValidateToken(Secret, Issuer, Audience, tokenData?.Item1, out var jwtTokenClaims, out var validateErrorMessage); - [Fact] - public void ValidateToken_ShouldReturnClaimsPrincipal_WhenTokenIsValid() { - // Arrange - var (token, _) = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles); + // Assert + Assert.True(result); + Assert.NotNull(jwtTokenClaims); + Assert.Equal(Username, jwtTokenClaims?.Username); + Assert.Contains(jwtTokenClaims?.Roles ?? new List(), c => c == "Admin"); + Assert.Contains(jwtTokenClaims?.Roles ?? new List(), c => c == "User"); + Assert.Null(validateErrorMessage); + } - // Act - var jwtTokenClaims = JwtGenerator.ValidateToken(Secret, Issuer, Audience, token); + [Fact] + public void ValidateToken_ShouldReturnNull_WhenTokenIsInvalid() { + // Arrange + var invalidToken = "invalidToken"; - // Assert - Assert.NotNull(jwtTokenClaims); - Assert.Equal(Username, jwtTokenClaims.Username); - Assert.Contains(jwtTokenClaims.Roles ?? new List(), c => c == "Admin"); - Assert.Contains(jwtTokenClaims.Roles ?? new List(), c => c == "User"); - } + // Act + var result = JwtGenerator.TryValidateToken(Secret, Issuer, Audience, invalidToken, out var jwtTokenClaims, out var errorMessage); - [Fact] - public void ValidateToken_ShouldReturnNull_WhenTokenIsInvalid() { - // Arrange - var invalidToken = "invalidToken"; + // Assert + Assert.False(result); + Assert.Null(jwtTokenClaims); + Assert.NotNull(errorMessage); + } - // Act - var principal = JwtGenerator.ValidateToken(Secret, Issuer, Audience, invalidToken); + [Fact] + public void GenerateRefreshToken_ShouldReturnNonEmptyString() { + // Act + var refreshToken = JwtGenerator.GenerateRefreshToken(); - // Assert - Assert.Null(principal); - } + // Assert + Assert.False(string.IsNullOrEmpty(refreshToken)); + } - [Fact] - public void GenerateRefreshToken_ShouldReturnNonEmptyString() { - // Act - var refreshToken = JwtGenerator.GenerateRefreshToken(); + [Fact] + public void GenerateSecret_ShouldReturnDifferentValuesOnSubsequentCalls() { + // Act + string secret1 = JwtGenerator.GenerateSecret(); + string secret2 = JwtGenerator.GenerateSecret(); - // Assert - Assert.False(string.IsNullOrEmpty(refreshToken)); - } - - [Fact] - public void GenerateSecret_ShouldReturnDifferentValuesOnSubsequentCalls() { - // Act - string secret1 = JwtGenerator.GenerateSecret(); - string secret2 = JwtGenerator.GenerateSecret(); - - // Assert - Assert.False(string.IsNullOrEmpty(secret1)); - Assert.False(string.IsNullOrEmpty(secret2)); - Assert.NotEqual(secret1, secret2); // Ensure the secrets are unique + // Assert + Assert.False(string.IsNullOrEmpty(secret1)); + Assert.False(string.IsNullOrEmpty(secret2)); + Assert.NotEqual(secret1, secret2); // Ensure the secrets are unique + } } } diff --git a/src/MaksIT.Core.Tests/Security/PasswordHasherTests.cs b/src/MaksIT.Core.Tests/Security/PasswordHasherTests.cs index dfd3e6e..9425388 100644 --- a/src/MaksIT.Core.Tests/Security/PasswordHasherTests.cs +++ b/src/MaksIT.Core.Tests/Security/PasswordHasherTests.cs @@ -9,11 +9,14 @@ namespace MaksIT.Core.Tests.Security { var password = "SecurePassword123!"; // Act - var result = PasswordHasher.CreateSaltedHash(password); + var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage); // Assert - Assert.False(string.IsNullOrWhiteSpace(result.Salt)); - Assert.False(string.IsNullOrWhiteSpace(result.Hash)); + Assert.True(result); + Assert.NotNull(saltedHash); + Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Salt)); + Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Hash)); + Assert.Null(errorMessage); } [Fact] @@ -22,11 +25,14 @@ namespace MaksIT.Core.Tests.Security { var password = ""; // Act - var result = PasswordHasher.CreateSaltedHash(password); + var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage); // Assert - Assert.False(string.IsNullOrWhiteSpace(result.Salt)); - Assert.False(string.IsNullOrWhiteSpace(result.Hash)); + Assert.True(result); + Assert.NotNull(saltedHash); + Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Salt)); + Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Hash)); + Assert.Null(errorMessage); } [Fact] @@ -35,24 +41,29 @@ namespace MaksIT.Core.Tests.Security { var password = " "; // Act - var result = PasswordHasher.CreateSaltedHash(password); + var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage); // Assert - Assert.False(string.IsNullOrWhiteSpace(result.Salt)); - Assert.False(string.IsNullOrWhiteSpace(result.Hash)); + Assert.True(result); + Assert.NotNull(saltedHash); + Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Salt)); + Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Hash)); + Assert.Null(errorMessage); } [Fact] public void ValidateHash_CorrectPassword_ReturnsTrue() { // Arrange var password = "SecurePassword123!"; - var hashResult = PasswordHasher.CreateSaltedHash(password); + PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var createErrorMessage); // Act - var verifyResult = PasswordHasher.ValidateHash(password, hashResult.Salt, hashResult.Hash); + var result = PasswordHasher.TryValidateHash(password, saltedHash?.Salt, saltedHash?.Hash, out var isValid, out var validateErrorMessage); // Assert - Assert.True(verifyResult); + Assert.True(result); + Assert.True(isValid); + Assert.Null(validateErrorMessage); } [Fact] @@ -60,13 +71,15 @@ namespace MaksIT.Core.Tests.Security { // Arrange var password = "SecurePassword123!"; var wrongPassword = "WrongPassword456!"; - var hashResult = PasswordHasher.CreateSaltedHash(password); + PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var createErrorMessage); // Act - var verifyResult = PasswordHasher.ValidateHash(wrongPassword, hashResult.Salt, hashResult.Hash); + var result = PasswordHasher.TryValidateHash(wrongPassword, saltedHash?.Salt, saltedHash?.Hash, out var isValid, out var validateErrorMessage); // Assert - Assert.False(verifyResult); + Assert.True(result); + Assert.False(isValid); + Assert.Null(validateErrorMessage); } [Fact] @@ -77,10 +90,12 @@ namespace MaksIT.Core.Tests.Security { var salt = ""; // Assuming empty salt // Act - var verifyResult = PasswordHasher.ValidateHash(password, salt, storedHash); + var result = PasswordHasher.TryValidateHash(password, salt, storedHash, out var isValid, out var errorMessage); // Assert - Assert.False(verifyResult); + Assert.True(result); + Assert.False(isValid); + Assert.Null(errorMessage); } [Fact] @@ -91,10 +106,12 @@ namespace MaksIT.Core.Tests.Security { var salt = " "; // Act - var verifyResult = PasswordHasher.ValidateHash(password, salt, storedHash); + var result = PasswordHasher.TryValidateHash(password, salt, storedHash, out var isValid, out var errorMessage); // Assert - Assert.False(verifyResult); + Assert.True(result); + Assert.False(isValid); + Assert.Null(errorMessage); } [Fact] @@ -105,10 +122,12 @@ namespace MaksIT.Core.Tests.Security { var invalidSalt = "InvalidSaltValue"; // Act - var verifyResult = PasswordHasher.ValidateHash(password, invalidSalt, invalidStoredHash); + var result = PasswordHasher.TryValidateHash(password, invalidSalt, invalidStoredHash, out var isValid, out var errorMessage); // Assert - Assert.False(verifyResult); + Assert.True(result); + Assert.False(isValid); + Assert.Null(errorMessage); } [Fact] @@ -117,29 +136,33 @@ namespace MaksIT.Core.Tests.Security { var password = "SecurePassword123!"; // Act - var hashResult1 = PasswordHasher.CreateSaltedHash(password); - var hashResult2 = PasswordHasher.CreateSaltedHash(password); + PasswordHasher.TryCreateSaltedHash(password, out var hashResult1, out var errorMessage1); + PasswordHasher.TryCreateSaltedHash(password, out var hashResult2, out var errorMessage2); // Assert - Assert.NotEqual(hashResult1.Hash, hashResult2.Hash); + Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash); } [Fact] public void ValidateHash_ModifiedStoredHash_ReturnsFalse() { // Arrange var password = "SecurePassword123!"; - var hashResult = PasswordHasher.CreateSaltedHash(password); + PasswordHasher.TryCreateSaltedHash(password, out var hashResult, out var createErrorMessage); // Modify the stored hash - var hashChars = hashResult.Hash.ToCharArray(); - hashChars[10] = (hashChars[10] == 'A') ? 'B' : 'A'; // Change one character + var hashChars = hashResult?.Hash.ToCharArray(); + if (hashChars != null) { + hashChars[10] = (hashChars[10] == 'A') ? 'B' : 'A'; // Change one character + } var modifiedHash = new string(hashChars); // Act - var verifyResult = PasswordHasher.ValidateHash(password, hashResult.Salt, modifiedHash); + var result = PasswordHasher.TryValidateHash(password, hashResult?.Salt, modifiedHash, out var isValid, out var validateErrorMessage); // Assert - Assert.False(verifyResult); + Assert.True(result); + Assert.False(isValid); + Assert.Null(validateErrorMessage); } [Fact] @@ -149,11 +172,11 @@ namespace MaksIT.Core.Tests.Security { var password2 = "PasswordTwo"; // Act - var hashResult1 = PasswordHasher.CreateSaltedHash(password1); - var hashResult2 = PasswordHasher.CreateSaltedHash(password2); + PasswordHasher.TryCreateSaltedHash(password1, out var hashResult1, out var errorMessage1); + PasswordHasher.TryCreateSaltedHash(password2, out var hashResult2, out var errorMessage2); // Assert - Assert.NotEqual(hashResult1.Hash, hashResult2.Hash); + Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash); } [Fact] @@ -162,13 +185,13 @@ namespace MaksIT.Core.Tests.Security { var password = "SecurePassword123!"; // Act - var result = PasswordHasher.CreateSaltedHash(password); + var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage); // Assert // For 16 bytes salt, Base64 length is 24 characters - Assert.Equal(24, result.Salt.Length); + Assert.Equal(24, saltedHash?.Salt.Length); // For 32 bytes hash, Base64 length is 44 characters - Assert.Equal(44, result.Hash.Length); + Assert.Equal(44, saltedHash?.Hash.Length); } } } diff --git a/src/MaksIT.Core.Tests/Security/TtopGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/TtopGeneratorTests.cs index 984fcb0..67b6ee3 100644 --- a/src/MaksIT.Core.Tests/Security/TtopGeneratorTests.cs +++ b/src/MaksIT.Core.Tests/Security/TtopGeneratorTests.cs @@ -11,13 +11,15 @@ namespace MaksIT.Core.Tests.Security { public void Validate_ValidTotpCode_ReturnsTrue() { // Arrange var timestep = TotpGenerator.GetCurrentTimeStepNumber(); - var validTotpCode = TotpGenerator.Generate(Base32Secret, timestep); + TotpGenerator.TryGenerate(Base32Secret, timestep, out var validTotpCode, out var generateErrorMessage); // Act - var isValid = TotpGenerator.Validate(validTotpCode, Base32Secret); + var result = TotpGenerator.TryValidate(validTotpCode, Base32Secret, 0, out var isValid, out var validateErrorMessage); // Assert + Assert.True(result); Assert.True(isValid); + Assert.Null(validateErrorMessage); } [Fact] @@ -26,23 +28,27 @@ namespace MaksIT.Core.Tests.Security { var invalidTotpCode = "123456"; // Example invalid TOTP code // Act - var isValid = TotpGenerator.Validate(invalidTotpCode, Base32Secret); + var result = TotpGenerator.TryValidate(invalidTotpCode, Base32Secret, 0, out var isValid, out var errorMessage); // Assert + Assert.True(result); Assert.False(isValid); + Assert.Null(errorMessage); } [Fact] public void Validate_TotpCodeWithTimeTolerance_ReturnsTrue() { // Arrange var timestep = TotpGenerator.GetCurrentTimeStepNumber() - 1; // One timestep in the past - var validTotpCode = TotpGenerator.Generate(Base32Secret, timestep); + TotpGenerator.TryGenerate(Base32Secret, timestep, out var validTotpCode, out var generateErrorMessage); // Act - var isValid = TotpGenerator.Validate(validTotpCode, Base32Secret, timeTolerance: 1); + var result = TotpGenerator.TryValidate(validTotpCode, Base32Secret, 1, out var isValid, out var validateErrorMessage); // Assert + Assert.True(result); Assert.True(isValid); + Assert.Null(validateErrorMessage); } [Fact] @@ -51,11 +57,13 @@ namespace MaksIT.Core.Tests.Security { var timestep = TotpGenerator.GetCurrentTimeStepNumber(); // Act - var totpCode = TotpGenerator.Generate(Base32Secret, timestep); + var result = TotpGenerator.TryGenerate(Base32Secret, timestep, out var totpCode, out var errorMessage); // Assert + Assert.True(result); Assert.False(string.IsNullOrEmpty(totpCode)); Assert.Equal(6, totpCode.Length); + Assert.Null(errorMessage); } [Fact] @@ -70,13 +78,13 @@ namespace MaksIT.Core.Tests.Security { [Fact] public void GenerateSecret_ReturnsValidBase32String() { // Act - var secret = TotpGenerator.GenerateSecret(); + var result = TotpGenerator.TryGenerateSecret(out var secret, out var errorMessage); // Assert + Assert.True(result); Assert.False(string.IsNullOrEmpty(secret)); Assert.True(secret.IsBase32String()); + Assert.Null(errorMessage); } - - } } diff --git a/src/MaksIT.Core/Culture.cs b/src/MaksIT.Core/Culture.cs new file mode 100644 index 0000000..c67ff46 --- /dev/null +++ b/src/MaksIT.Core/Culture.cs @@ -0,0 +1,38 @@ +using System; +using System.Globalization; +using System.Threading; + +namespace MaksIT.Core; + +/// +/// The main Cultures class. +/// Contains all methods for performing basic Cultures management. +/// +public static class Culture { + /// + /// Sets the culture for the current thread. + /// + /// The culture to set. If null or empty, the invariant culture is used. + /// The error message if the operation fails. + /// True if the operation was successful; otherwise, false. + public static bool TrySet(string? culture, out string? errorMessage) { + try { + var threadCulture = CultureInfo.InvariantCulture; + + if (!string.IsNullOrEmpty(culture)) { + threadCulture = CultureInfo.CreateSpecificCulture(culture); + } + + Thread.CurrentThread.CurrentUICulture = threadCulture; + Thread.CurrentThread.CurrentCulture = threadCulture; + + errorMessage = null; + return true; + } + catch (Exception ex) { + errorMessage = ex.Message; + return false; + } + } +} + diff --git a/src/MaksIT.Core/EnvVar.cs b/src/MaksIT.Core/EnvVar.cs new file mode 100644 index 0000000..eb07235 --- /dev/null +++ b/src/MaksIT.Core/EnvVar.cs @@ -0,0 +1,93 @@ +using System; +using System.Runtime.InteropServices; + +namespace MaksIT.Core; + +/// +/// Allows to Set and Unset environment variables +/// +public static class EnvVar { + private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + /// + /// Adds a new path to the PATH environment variable. + /// + /// The new path to add. + /// The error message if the operation fails. + /// True if the operation was successful; otherwise, false. + public static bool TryAddToPath(string newPath, out string? errorMessage) { + try { + var pathEnvVar = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + char separator = IsWindows ? ';' : ':'; + + if (!pathEnvVar.Split(separator).Contains(newPath)) { + pathEnvVar = pathEnvVar.TrimEnd(separator) + separator + newPath; + Environment.SetEnvironmentVariable("PATH", pathEnvVar); + } + + errorMessage = null; + return true; + } + catch (Exception ex) { + errorMessage = ex.Message; + return false; + } + } + + /// + /// Sets an environment variable. + /// + /// The name of the environment variable. + /// The value of the environment variable. + /// The target of the environment variable (machine, user, process). + /// The error message if the operation fails. + /// True if the operation was successful; otherwise, false. + public static bool TrySet(string envName, string envValue, string envTarget, out string? errorMessage) { + try { + EnvironmentVariableTarget target = GetEnvironmentVariableTarget(envTarget); + if (target == EnvironmentVariableTarget.Machine && !IsWindows) { + throw new PlatformNotSupportedException("Setting machine-level environment variables is not supported on this platform."); + } + + Environment.SetEnvironmentVariable(envName, envValue, target); + errorMessage = null; + return true; + } + catch (Exception ex) { + errorMessage = ex.Message; + return false; + } + } + + /// + /// Unsets an environment variable. + /// + /// The name of the environment variable. + /// The target of the environment variable (machine, user, process). + /// The error message if the operation fails. + /// True if the operation was successful; otherwise, false. + public static bool TryUnSet(string envName, string envTarget, out string? errorMessage) { + try { + EnvironmentVariableTarget target = GetEnvironmentVariableTarget(envTarget); + if (target == EnvironmentVariableTarget.Machine && !IsWindows) { + throw new PlatformNotSupportedException("Unsetting machine-level environment variables is not supported on this platform."); + } + + Environment.SetEnvironmentVariable(envName, null, target); + errorMessage = null; + return true; + } + catch (Exception ex) { + errorMessage = ex.Message; + return false; + } + } + + private static EnvironmentVariableTarget GetEnvironmentVariableTarget(string envTarget) { + return envTarget.ToLower() switch { + "user" => EnvironmentVariableTarget.User, + "process" => EnvironmentVariableTarget.Process, + _ => EnvironmentVariableTarget.Machine, + }; + } +} diff --git a/src/MaksIT.Core/Extensions/ExpressionExtensions.cs b/src/MaksIT.Core/Extensions/ExpressionExtensions.cs index 58a98e1..eff7665 100644 --- a/src/MaksIT.Core/Extensions/ExpressionExtensions.cs +++ b/src/MaksIT.Core/Extensions/ExpressionExtensions.cs @@ -32,4 +32,3 @@ public static class ExpressionExtensions { } } } - diff --git a/src/MaksIT.Core/FileSystem.cs b/src/MaksIT.Core/FileSystem.cs new file mode 100644 index 0000000..4d23fd5 --- /dev/null +++ b/src/MaksIT.Core/FileSystem.cs @@ -0,0 +1,177 @@ +using System.Runtime.InteropServices; +using MaksIT.Core.Extensions; + +namespace MaksIT.Core; + +/// +/// Main FileSystem class. +/// Provides basic helper methods to work with the file system. +/// +public static class FileSystem { + private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + /// + /// Copies the file or folder's content to the specified folder. + /// + /// File or directory path. + /// Destination directory. + /// Whether to overwrite existing files. + /// The error message if the operation fails. + /// True if the copy operation was successful; otherwise, false. + public static bool TryCopyToFolder(string sourcePath, string destDirPath, bool overwrite, out string? errorMessage) { + try { + if (!Directory.Exists(destDirPath)) { + Directory.CreateDirectory(destDirPath); + } + + FileAttributes attr = File.GetAttributes(sourcePath); + + if (attr.HasFlag(FileAttributes.Directory)) { + foreach (var filePath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories)) { + var destFilePath = Path.Combine(destDirPath, filePath.Substring(sourcePath.Length).TrimStart(Path.DirectorySeparatorChar)); + var destDirectoryPath = Path.GetDirectoryName(destFilePath); + + if (destDirectoryPath != null && !Directory.Exists(destDirectoryPath)) { + Directory.CreateDirectory(destDirectoryPath); + } + + File.Copy(filePath, destFilePath, overwrite); + } + } + else { + // It's a file + File.Copy(sourcePath, Path.Combine(destDirPath, Path.GetFileName(sourcePath)), overwrite); + } + + errorMessage = null; + return true; + } + catch (Exception ex) { + errorMessage = ex.Message; + return false; + } + } + + /// + /// Deletes a file or directory at the specified path. + /// + /// File or directory path. + /// The error message if the operation fails. + /// True if the delete operation was successful; otherwise, false. + public static bool TryDeleteFileOrDirectory(string itemPath, out string? errorMessage) { + try { + if (File.Exists(itemPath)) { + File.Delete(itemPath); + } + else if (Directory.Exists(itemPath)) { + Directory.Delete(itemPath, true); + } + + errorMessage = null; + return true; + } + catch (Exception ex) { + errorMessage = ex.Message; + return false; + } + } + + /// + /// Resolves a path with wildcards and returns all possible variants found. + /// + /// Example - @"?:\Users\*\AppData\Roa*\" + /// Returns all possible, but existing path variants found. + public static List ResolveWildcardedPath(string wildcardedPath) { + var response = new List(); + + wildcardedPath = wildcardedPath.TrimEnd(Path.DirectorySeparatorChar); + + if (!wildcardedPath.Contains('*') && !wildcardedPath.Contains('?')) { + response.Add(wildcardedPath); + return response; + } + + var pathsCollection = new List { "" }; + + foreach (string item in wildcardedPath.Split(Path.DirectorySeparatorChar)) { + if (item == "?:") { + pathsCollection = DriveInfo.GetDrives() + .Where(drive => drive.Name.Like(item + Path.DirectorySeparatorChar)) + .Select(drive => drive.Name) + .ToList(); + } + else if (item.Contains('*') || item.Contains('?')) { + var temp = new List(); + + foreach (var path in pathsCollection) { + if (Directory.Exists(path)) { + try { + temp.AddRange(Directory.GetFiles(path).Where(file => Path.GetFileName(file).Like(item)).Select(file => file + Path.DirectorySeparatorChar)); + temp.AddRange(Directory.GetDirectories(path).Where(dir => Path.GetFileName(dir).Like(item)).Select(dir => dir + Path.DirectorySeparatorChar)); + } + catch { + // Handle exceptions if necessary + } + } + } + + pathsCollection = temp; + } + else { + if (pathsCollection.Count == 0) { + pathsCollection.Add(item + Path.DirectorySeparatorChar); + } + else { + for (var i = 0; i < pathsCollection.Count; i++) { + pathsCollection[i] += item + Path.DirectorySeparatorChar; + } + } + } + } + + pathsCollection = pathsCollection.Select(s => s.Trim(Path.DirectorySeparatorChar)).ToList(); + + var tempWildcardedPath = wildcardedPath.Split(Path.DirectorySeparatorChar); + + response = pathsCollection + .Where(path => { + var tempPath = path.Split(Path.DirectorySeparatorChar); + if (tempWildcardedPath.Length != tempPath.Length) return false; + + for (int i = 0; i < tempWildcardedPath.Length; i++) { + if (!tempWildcardedPath[i].Contains('*') && !tempWildcardedPath[i].Contains('?') && tempWildcardedPath[i] != tempPath[i]) { + return false; + } + } + + return Directory.Exists(path) || File.Exists(path); + }) + .ToList(); + + return response; + } + + /// + /// Tests a file name for duplicates, and if it is a duplicate, assigns a new name. + /// + /// File path to test for duplicates. + /// Returns the updated file name. + public static string DuplicateFileNameCheck(string fullPath) { + var fileNameOnly = Path.GetFileNameWithoutExtension(fullPath); + var extension = Path.GetExtension(fullPath); + var path = Path.GetDirectoryName(fullPath); + var newFullPath = fullPath; + + if (path == null) { + throw new ArgumentException("Invalid file path", nameof(fullPath)); + } + + var count = 1; + while (File.Exists(newFullPath)) { + var tempFileName = $"{fileNameOnly}({count++})"; + newFullPath = Path.Combine(path, tempFileName + extension); + } + + return newFullPath; + } +} diff --git a/src/MaksIT.Core/Logging/FileLogger.cs b/src/MaksIT.Core/Logging/FileLogger.cs new file mode 100644 index 0000000..e7eb61b --- /dev/null +++ b/src/MaksIT.Core/Logging/FileLogger.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Logging; + + +namespace MaksIT.Core.Logging; + +public class FileLogger : ILogger { + private readonly string _filePath; + private readonly object _lock = new object(); + + public FileLogger(string filePath) { + _filePath = filePath; + } + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) { + return logLevel != LogLevel.None; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { + if (!IsEnabled(logLevel)) + return; + + var message = formatter(state, exception); + if (string.IsNullOrEmpty(message)) + return; + + var logRecord = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {message}"; + if (exception != null) { + logRecord += Environment.NewLine + exception; + } + + lock (_lock) { + File.AppendAllText(_filePath, logRecord + Environment.NewLine); + } + } +} diff --git a/src/MaksIT.Core/Logging/FileLoggerProvider.cs b/src/MaksIT.Core/Logging/FileLoggerProvider.cs new file mode 100644 index 0000000..6e2e900 --- /dev/null +++ b/src/MaksIT.Core/Logging/FileLoggerProvider.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MaksIT.Core.Logging; + +[ProviderAlias("FileLogger")] +public class FileLoggerProvider : ILoggerProvider { + private readonly string _filePath; + + public FileLoggerProvider(string filePath) { + _filePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + } + + public ILogger CreateLogger(string categoryName) { + return new FileLogger(_filePath); + } + + public void Dispose() { } +} diff --git a/src/MaksIT.Core/Logging/LoggingBuilderExtensions.cs b/src/MaksIT.Core/Logging/LoggingBuilderExtensions.cs new file mode 100644 index 0000000..64b2742 --- /dev/null +++ b/src/MaksIT.Core/Logging/LoggingBuilderExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + + +namespace MaksIT.Core.Logging; + +public static class LoggingBuilderExtensions { + public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string filePath) { + builder.Services.AddSingleton(new FileLoggerProvider(filePath)); + return builder; + } +} diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index b8737c9..e2cc200 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.1.6 + 1.1.7 Maksym Sadovnychyy MAKS-IT MaksIT.Core @@ -26,6 +26,8 @@ + + diff --git a/src/MaksIT.Core/Networking/PingPort.cs b/src/MaksIT.Core/Networking/PingPort.cs new file mode 100644 index 0000000..dfe2369 --- /dev/null +++ b/src/MaksIT.Core/Networking/PingPort.cs @@ -0,0 +1,94 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace MaksIT.Core.Networking; + +/// +/// Provides network-related utility methods. +/// +public static class PingPort { + /// + /// Tries to ping a host on a specified TCP port. + /// + /// The host URI. + /// The port number. + /// The error message if the operation fails. + /// True if the host is reachable on the specified port; otherwise, false. + public static bool TryHostPort(string hostUri, int portNumber, out string? errorMessage) { + if (string.IsNullOrEmpty(hostUri)) { + errorMessage = "Host URI cannot be null or empty."; + return false; + } + + try { + using (var client = new TcpClient()) { + var result = client.BeginConnect(hostUri, portNumber, null, null); + var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(5)); + if (!success) { + errorMessage = "Connection timed out."; + return false; + } + + client.EndConnect(result); + errorMessage = null; + return true; + } + } + catch (SocketException ex) { + errorMessage = ex.Message; + return false; + } + catch (Exception ex) { + // Log or handle other exceptions as needed + errorMessage = ex.Message; + return false; + } + } + + /// + /// Tries to ping a host on a specified UDP port. + /// + /// The host URI. + /// The port number. + /// The error message if the operation fails. + /// True if the host is reachable on the specified port; otherwise, false. + public static bool TryUDPPort(string hostUri, int portNumber, out string? errorMessage) { + if (string.IsNullOrEmpty(hostUri)) { + errorMessage = "Host URI cannot be null or empty."; + return false; + } + + using (var udpClient = new UdpClient()) { + try { + udpClient.Connect(hostUri, portNumber); + + // Sends a message to the host to which you have connected. + byte[] sendBytes = Encoding.ASCII.GetBytes("Is anybody there?"); + udpClient.Send(sendBytes, sendBytes.Length); + + // IPEndPoint object will allow us to read datagrams sent from any source. + IPEndPoint remoteIpEndPoint = new IPEndPoint(IPAddress.Any, 0); + + // Set a receive timeout to avoid blocking indefinitely + udpClient.Client.ReceiveTimeout = 5000; + + // Blocks until a message returns on this socket from a remote host. + byte[] receiveBytes = udpClient.Receive(ref remoteIpEndPoint); + string returnData = Encoding.ASCII.GetString(receiveBytes); + + errorMessage = null; + return true; + } + catch (SocketException ex) { + errorMessage = ex.Message; + return false; + } + catch (Exception ex) { + // Log or handle other exceptions as needed + errorMessage = ex.Message; + return false; + } + } + } +} diff --git a/src/MaksIT.Core/Networking/Windows/NetworkConnection.cs b/src/MaksIT.Core/Networking/Windows/NetworkConnection.cs new file mode 100644 index 0000000..67799cb --- /dev/null +++ b/src/MaksIT.Core/Networking/Windows/NetworkConnection.cs @@ -0,0 +1,118 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using System.Runtime.InteropServices; + +namespace MaksIT.Core.Networking.Windows; + +public class NetworkConnection : IDisposable { + private readonly ILogger _logger; + private readonly string _networkName; + + private NetworkConnection(ILogger logger, string networkName) { + _logger = logger; + _networkName = networkName; + } + + public static bool TryCreate( + ILogger logger, + string networkName, + NetworkCredential credentials, + out NetworkConnection? networkConnection, + out string? errorMessage) { + try { + if (!OperatingSystem.IsWindows()) { + throw new PlatformNotSupportedException("NetworkConnection is only supported on Windows."); + } + + if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (networkName == null) throw new ArgumentNullException(nameof(networkName)); + if (credentials == null) throw new ArgumentNullException(nameof(credentials)); + + var netResource = new NetResource { + Scope = ResourceScope.GlobalNetwork, + ResourceType = ResourceType.Disk, + DisplayType = ResourceDisplayType.Share, + RemoteName = networkName + }; + + var result = WNetAddConnection2(netResource, credentials.Password, credentials.UserName, 0); + + if (result != 0) { + throw new InvalidOperationException($"Error connecting to remote share: {result}"); + } + + networkConnection = new NetworkConnection(logger, networkName); + errorMessage = null; + return true; + } + catch (Exception ex) { + networkConnection = null; + errorMessage = ex.Message; + return false; + } + } + + ~NetworkConnection() { + Dispose(false); + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) { + if (OperatingSystem.IsWindows()) { + WNetCancelConnection2(_networkName, 0, true); + } + } + + [DllImport("mpr.dll")] + private static extern int WNetAddConnection2(NetResource netResource, string? password, string? username, int flags); + + [DllImport("mpr.dll")] + private static extern int WNetCancelConnection2(string name, int flags, bool force); + + [StructLayout(LayoutKind.Sequential)] + public class NetResource { + public ResourceScope Scope; + public ResourceType ResourceType; + public ResourceDisplayType DisplayType; + public int Usage; + public string? LocalName; + public string RemoteName; + public string? Comment; + public string? Provider; + } + + public enum ResourceScope : int { + Connected = 1, + GlobalNetwork, + Remembered, + Recent, + Context + } + + public enum ResourceType : int { + Any = 0, + Disk = 1, + Print = 2, + Reserved = 8 + } + + public enum ResourceDisplayType : int { + Generic = 0x0, + Domain = 0x01, + Server = 0x02, + Share = 0x03, + File = 0x04, + Group = 0x05, + Network = 0x06, + Root = 0x07, + Shareadmin = 0x08, + Directory = 0x09, + Tree = 0x0a, + Ndscontainer = 0x0b + } +} diff --git a/src/MaksIT.Core/Processes.cs b/src/MaksIT.Core/Processes.cs new file mode 100644 index 0000000..938165b --- /dev/null +++ b/src/MaksIT.Core/Processes.cs @@ -0,0 +1,80 @@ +using MaksIT.Core.Extensions; +using System.Diagnostics; + +namespace MaksIT.Core; + +/// +/// Main CustomProcess class. +/// Provide helper methods to Start and Kill processes. +/// +/// +/// Start +/// Starts new process +/// +/// +/// Kill +/// Kills processes by name +/// +/// +/// +public static class Processes { + /// + /// Tries to start a new process. + /// + /// The name of the file to start. + /// The arguments to pass to the process. + /// The timeout in seconds to wait for the process to exit. + /// If true, the process will be started without creating a window. + /// The error message if the operation fails. + /// True if the process started successfully; otherwise, false. + public static bool TryStart(string fileName, string arguments, int timeout, bool silent, out string? errorMessage) { + try { + var processInfo = new ProcessStartInfo(fileName) { + Arguments = arguments, + UseShellExecute = !silent, + CreateNoWindow = silent + }; + + using (var proc = new System.Diagnostics.Process { StartInfo = processInfo }) { + proc.Start(); + if (timeout > 0) { + proc.WaitForExit(timeout * 1000); + } + else { + proc.WaitForExit(); + } + } + errorMessage = null; + return true; + } + catch (Exception ex) { + // Log the exception or handle it as needed + errorMessage = ex.Message; + return false; + } + } + + /// + /// Tries to kill processes by name. + /// + /// Process name. Accepts wildcards '*' or '?' + /// The error message if the operation fails. + /// True if at least one process was killed successfully; otherwise, false. + public static bool TryKill(string process, out string? errorMessage) { + bool success = false; + errorMessage = null; + foreach (var proc in System.Diagnostics.Process.GetProcesses()) { + try { + if (proc.ProcessName.Like(process)) { + proc.Kill(); + success = true; + } + } + catch (Exception ex) { + // Log the exception or handle it as needed + errorMessage = ex.Message; + } + } + return success; + } +} diff --git a/src/MaksIT.Core/Security/AESGCMUtility.cs b/src/MaksIT.Core/Security/AESGCMUtility.cs new file mode 100644 index 0000000..05d4987 --- /dev/null +++ b/src/MaksIT.Core/Security/AESGCMUtility.cs @@ -0,0 +1,75 @@ +using System.Security.Cryptography; + +namespace MaksIT.Core.Security; + +/// +/// +/// +public static class AESGCMUtility { + private const int IvLength = 12; // 12 bytes for AES-GCM IV + private const int TagLength = 16; // 16 bytes for AES-GCM Tag + + public static bool TryEncryptData(byte[] data, string base64Key, out byte[]? result, out string? errorMessage) { + try { + var key = Convert.FromBase64String(base64Key); + using (AesGcm aesGcm = new AesGcm(key, AesGcm.TagByteSizes.MaxSize)) { + var iv = new byte[IvLength]; + RandomNumberGenerator.Fill(iv); + + var cipherText = new byte[data.Length]; + var tag = new byte[TagLength]; + + aesGcm.Encrypt(iv, data, cipherText, tag); + + // Concatenate cipherText, tag, and iv + result = new byte[cipherText.Length + tag.Length + iv.Length]; + Buffer.BlockCopy(cipherText, 0, result, 0, cipherText.Length); + Buffer.BlockCopy(tag, 0, result, cipherText.Length, tag.Length); + Buffer.BlockCopy(iv, 0, result, cipherText.Length + tag.Length, iv.Length); + + errorMessage = null; + return true; + } + } + catch (Exception ex) when (ex is FormatException || ex is CryptographicException) { + result = null; + errorMessage = ex.Message; + return false; + } + } + + public static bool TryDecryptData(byte[] data, string base64Key, out byte[]? decryptedData, out string? errorMessage) { + try { + var key = Convert.FromBase64String(base64Key); + + // Extract cipherText, tag, and iv + var cipherTextLength = data.Length - IvLength - TagLength; + + var cipherText = new byte[cipherTextLength]; + var tag = new byte[TagLength]; + var iv = new byte[IvLength]; + + Buffer.BlockCopy(data, 0, cipherText, 0, cipherTextLength); + Buffer.BlockCopy(data, cipherTextLength, tag, 0, TagLength); + Buffer.BlockCopy(data, cipherTextLength + TagLength, iv, 0, IvLength); + + using (AesGcm aesGcm = new AesGcm(key, AesGcm.TagByteSizes.MaxSize)) { + decryptedData = new byte[cipherText.Length]; + aesGcm.Decrypt(iv, cipherText, tag, decryptedData); + errorMessage = null; + return true; + } + } + catch (Exception ex) when (ex is FormatException || ex is CryptographicException) { + decryptedData = null; + errorMessage = ex.Message; + return false; + } + } + + public static string GenerateKeyBase64() { + var key = new byte[32]; // 256-bit key for AES-256 + RandomNumberGenerator.Fill(key); + return Convert.ToBase64String(key); + } +} diff --git a/src/MaksIT.Core/Security/Base32Encoder.cs b/src/MaksIT.Core/Security/Base32Encoder.cs index 86a6603..3356d2f 100644 --- a/src/MaksIT.Core/Security/Base32Encoder.cs +++ b/src/MaksIT.Core/Security/Base32Encoder.cs @@ -9,75 +9,93 @@ public static class Base32Encoder { private static readonly char[] Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray(); private const string PaddingChar = "="; - public static string Encode(byte[] data) { - if (data == null || data.Length == 0) { - throw new ArgumentNullException(nameof(data)); - } + public static bool TryEncode(byte[] data, out string? encoded, out string? errorMessage) { + try { + if (data == null || data.Length == 0) { + throw new ArgumentNullException(nameof(data)); + } - var result = new StringBuilder(); - int buffer = data[0]; - int next = 1; - int bitsLeft = 8; - while (bitsLeft > 0 || next < data.Length) { - if (bitsLeft < 5) { - if (next < data.Length) { - buffer <<= 8; - buffer |= (data[next++] & 0xFF); - bitsLeft += 8; + var result = new StringBuilder(); + int buffer = data[0]; + int next = 1; + int bitsLeft = 8; + while (bitsLeft > 0 || next < data.Length) { + if (bitsLeft < 5) { + if (next < data.Length) { + buffer <<= 8; + buffer |= (data[next++] & 0xFF); + bitsLeft += 8; + } + else { + int pad = 5 - bitsLeft; + buffer <<= pad; + bitsLeft += pad; + } } - else { - int pad = 5 - bitsLeft; - buffer <<= pad; - bitsLeft += pad; + + int index = (buffer >> (bitsLeft - 5)) & 0x1F; + bitsLeft -= 5; + result.Append(Base32Alphabet[index]); + } + + // Padding for a complete block + int padding = result.Length % 8; + if (padding > 0) { + for (int i = padding; i < 8; i++) { + result.Append(PaddingChar); } } - int index = (buffer >> (bitsLeft - 5)) & 0x1F; - bitsLeft -= 5; - result.Append(Base32Alphabet[index]); + encoded = result.ToString(); + errorMessage = null; + return true; } - - // Padding for a complete block - int padding = result.Length % 8; - if (padding > 0) { - for (int i = padding; i < 8; i++) { - result.Append(PaddingChar); - } + catch (Exception ex) { + encoded = null; + errorMessage = ex.Message; + return false; } - - return result.ToString(); } - public static byte[] Decode(string base32) { - if (string.IsNullOrEmpty(base32)) { - throw new ArgumentNullException(nameof(base32)); - } - - base32 = base32.TrimEnd(PaddingChar.ToCharArray()); - int byteCount = base32.Length * 5 / 8; - byte[] result = new byte[byteCount]; - - int buffer = 0; - int bitsLeft = 0; - int index = 0; - - foreach (char c in base32) { - int charValue = CharToValue(c); - if (charValue < 0) { - throw new FormatException("Invalid base32 character."); + public static bool TryDecode(string base32, out byte[]? decoded, out string? errorMessage) { + try { + if (string.IsNullOrEmpty(base32)) { + throw new ArgumentNullException(nameof(base32)); } - buffer <<= 5; - buffer |= charValue & 0x1F; - bitsLeft += 5; + base32 = base32.TrimEnd(PaddingChar.ToCharArray()); + int byteCount = base32.Length * 5 / 8; + byte[] result = new byte[byteCount]; - if (bitsLeft >= 8) { - result[index++] = (byte)(buffer >> (bitsLeft - 8)); - bitsLeft -= 8; + int buffer = 0; + int bitsLeft = 0; + int index = 0; + + foreach (char c in base32) { + int charValue = CharToValue(c); + if (charValue < 0) { + throw new FormatException("Invalid base32 character."); + } + + buffer <<= 5; + buffer |= charValue & 0x1F; + bitsLeft += 5; + + if (bitsLeft >= 8) { + result[index++] = (byte)(buffer >> (bitsLeft - 8)); + bitsLeft -= 8; + } } - } - return result; + decoded = result; + errorMessage = null; + return true; + } + catch (Exception ex) { + decoded = null; + errorMessage = ex.Message; + return false; + } } private static int CharToValue(char c) { @@ -92,4 +110,3 @@ public static class Base32Encoder { return -1; // Invalid character } } - diff --git a/src/MaksIT.Core/Security/ChecksumUtility.cs b/src/MaksIT.Core/Security/ChecksumUtility.cs new file mode 100644 index 0000000..af5c180 --- /dev/null +++ b/src/MaksIT.Core/Security/ChecksumUtility.cs @@ -0,0 +1,65 @@ +namespace MaksIT.Core.Security; + +public static class ChecksumUtility { + public static bool TryCalculateCRC32Checksum(byte[] data, out string? checksum, out string? errorMessage) { + if (Crc32.TryCompute(data, out var result, out errorMessage)) { + checksum = BitConverter.ToString(BitConverter.GetBytes(result)).Replace("-", "").ToLower(); + return true; + } + checksum = null; + return false; + } + + public static bool TryCalculateCRC32ChecksumFromFile(string filePath, out string? checksum, out string? errorMessage) { + try { + using var crc32 = new Crc32(); + using var stream = File.OpenRead(filePath); + var hashBytes = crc32.ComputeHash(stream); + checksum = BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); + errorMessage = null; + return true; + } + catch (Exception ex) { + checksum = null; + errorMessage = ex.Message; + return false; + } + } + + public static bool TryCalculateCRC32ChecksumFromFileInChunks(string filePath, out string? checksum, out string? errorMessage, int chunkSize = 8192) { + try { + using var crc32 = new Crc32(); + using var stream = File.OpenRead(filePath); + var buffer = new byte[chunkSize]; + int bytesRead; + while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) { + crc32.TransformBlock(buffer, 0, bytesRead, null, 0); + } + crc32.TransformFinalBlock(buffer, 0, 0); + var hashBytes = crc32.Hash; + checksum = BitConverter.ToString(hashBytes ?? Array.Empty()).Replace("-", "").ToLower(); + errorMessage = null; + return true; + } + catch (Exception ex) { + checksum = null; + errorMessage = ex.Message; + return false; + } + } + + public static bool VerifyCRC32Checksum(byte[] data, string expectedChecksum) { + return TryCalculateCRC32Checksum(data, out var calculatedChecksum, out _) && + string.Equals(calculatedChecksum, expectedChecksum, StringComparison.OrdinalIgnoreCase); + } + + public static bool VerifyCRC32ChecksumFromFile(string filePath, string expectedChecksum) { + return TryCalculateCRC32ChecksumFromFile(filePath, out var calculatedChecksum, out _) && + string.Equals(calculatedChecksum, expectedChecksum, StringComparison.OrdinalIgnoreCase); + } + + public static bool VerifyCRC32ChecksumFromFileInChunks(string filePath, string expectedChecksum, int chunkSize = 8192) { + return TryCalculateCRC32ChecksumFromFileInChunks(filePath, out var calculatedChecksum, out _, chunkSize) && + string.Equals(calculatedChecksum, expectedChecksum, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/MaksIT.Core/Security/Crc32.cs b/src/MaksIT.Core/Security/Crc32.cs new file mode 100644 index 0000000..9d959a3 --- /dev/null +++ b/src/MaksIT.Core/Security/Crc32.cs @@ -0,0 +1,118 @@ +using System.Security.Cryptography; + +namespace MaksIT.Core.Security; + +public class Crc32 : HashAlgorithm { + public const uint DefaultPolynomial = 0xedb88320; + public const uint DefaultSeed = 0xffffffff; + + private static uint[]? defaultTable; + + private readonly uint seed; + private readonly uint[] table; + private uint hash; + + public Crc32() + : this(DefaultPolynomial, DefaultSeed) { + } + + public Crc32(uint polynomial, uint seed) { + table = InitializeTable(polynomial); + this.seed = hash = seed; + } + + public override void Initialize() { + hash = seed; + } + + protected override void HashCore(byte[] buffer, int start, int length) { + hash = CalculateHash(table, hash, buffer, start, length); + } + + protected override byte[] HashFinal() { + var hashBuffer = UInt32ToBigEndianBytes(~hash); + HashValue = hashBuffer; + return hashBuffer; + } + + public override int HashSize => 32; + + public static bool TryCompute(byte[] buffer, out uint result, out string? errorMessage) { + try { + result = Compute(DefaultPolynomial, DefaultSeed, buffer); + errorMessage = null; + return true; + } + catch (Exception ex) { + result = 0; + errorMessage = ex.Message; + return false; + } + } + + public static bool TryCompute(uint seed, byte[] buffer, out uint result, out string? errorMessage) { + try { + result = Compute(DefaultPolynomial, seed, buffer); + errorMessage = null; + return true; + } + catch (Exception ex) { + result = 0; + errorMessage = ex.Message; + return false; + } + } + + public static bool TryCompute(uint polynomial, uint seed, byte[] buffer, out uint result, out string? errorMessage) { + try { + result = Compute(polynomial, seed, buffer); + errorMessage = null; + return true; + } + catch (Exception ex) { + result = 0; + errorMessage = ex.Message; + return false; + } + } + + private static uint Compute(uint polynomial, uint seed, byte[] buffer) { + return ~CalculateHash(InitializeTable(polynomial), seed, buffer, 0, buffer.Length); + } + + private static uint[] InitializeTable(uint polynomial) { + if (polynomial == DefaultPolynomial && defaultTable != null) + return defaultTable; + + var createTable = new uint[256]; + for (var i = 0; i < 256; i++) { + var entry = (uint)i; + for (var j = 0; j < 8; j++) + if ((entry & 1) == 1) + entry = (entry >> 1) ^ polynomial; + else + entry >>= 1; + createTable[i] = entry; + } + + if (polynomial == DefaultPolynomial) + defaultTable = createTable; + + return createTable; + } + + private static uint CalculateHash(uint[] table, uint seed, byte[] buffer, int start, int size) { + var crc = seed; + for (var i = start; i < size - start; i++) + crc = (crc >> 8) ^ table[buffer[i] ^ crc & 0xff]; + return crc; + } + + private static byte[] UInt32ToBigEndianBytes(uint x) => new byte[] + { + (byte)((x >> 24) & 0xff), + (byte)((x >> 16) & 0xff), + (byte)((x >> 8) & 0xff), + (byte)(x & 0xff) + }; +} diff --git a/src/MaksIT.Core/Security/JwtGenerator.cs b/src/MaksIT.Core/Security/JwtGenerator.cs index ea7907c..1eba54b 100644 --- a/src/MaksIT.Core/Security/JwtGenerator.cs +++ b/src/MaksIT.Core/Security/JwtGenerator.cs @@ -1,6 +1,4 @@ -using System; -using System.Text; -using System.Linq; +using System.Text; using System.Security.Claims; using System.Security.Cryptography; using System.IdentityModel.Tokens.Jwt; @@ -16,46 +14,55 @@ public class JWTTokenClaims { } public static class JwtGenerator { - public static (string, JWTTokenClaims) GenerateToken(string secret, string issuer, string audience, double expiration, string username, List roles) { - var secretKey = GetSymmetricSecurityKey(secret); - var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); + public static bool TryGenerateToken(string secret, string issuer, string audience, double expiration, string username, List roles, out (string, JWTTokenClaims)? tokenData, out string? errorMessage) { + try { + var secretKey = GetSymmetricSecurityKey(secret); + var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); - var issuedAt = DateTime.UtcNow; - var expiresAt = issuedAt.AddMinutes(expiration); + var issuedAt = DateTime.UtcNow; + var expiresAt = issuedAt.AddMinutes(expiration); - var claims = new List - { - new Claim(ClaimTypes.Name, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(issuedAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), - new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(expiresAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) - }; + var claims = new List + { + new Claim(ClaimTypes.Name, username), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(issuedAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(expiresAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) + }; - claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); - var tokenDescriptor = new JwtSecurityToken( - issuer: issuer, - audience: audience, - claims: claims, - expires: expiresAt, - signingCredentials: credentials - ); + var tokenDescriptor = new JwtSecurityToken( + issuer: issuer, + audience: audience, + claims: claims, + expires: expiresAt, + signingCredentials: credentials + ); - var jwtToken = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); + var jwtToken = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); - var tokenClaims = new JWTTokenClaims { - Username = username, - Roles = roles, - IssuedAt = issuedAt, - ExpiresAt = expiresAt - }; + var tokenClaims = new JWTTokenClaims { + Username = username, + Roles = roles, + IssuedAt = issuedAt, + ExpiresAt = expiresAt + }; - return (jwtToken, tokenClaims); + tokenData = (jwtToken, tokenClaims); + errorMessage = null; + return true; + } + catch (Exception ex) { + tokenData = null; + errorMessage = ex.Message; + return false; + } } public static string GenerateSecret(int keySize = 32) => Convert.ToBase64String(GetRandomBytes(keySize)); - public static JWTTokenClaims? ValidateToken(string secret, string issuer, string audience, string token) { + public static bool TryValidateToken(string secret, string issuer, string audience, string token, out JWTTokenClaims? tokenClaims, out string? errorMessage) { try { var key = Encoding.UTF8.GetBytes(secret); var tokenHandler = new JwtSecurityTokenHandler(); @@ -77,10 +84,14 @@ public static class JwtGenerator { if (validatedToken is JwtSecurityToken jwtToken && jwtToken.Header.Alg != SecurityAlgorithms.HmacSha256) throw new SecurityTokenException("Invalid token algorithm"); - return ExtractClaims(principal); + tokenClaims = ExtractClaims(principal); + errorMessage = null; + return true; } - catch { - return null; + catch (Exception ex) { + tokenClaims = null; + errorMessage = ex.Message; + return false; } } diff --git a/src/MaksIT.Core/Security/PasswordHasher.cs b/src/MaksIT.Core/Security/PasswordHasher.cs index bf06eb2..d714039 100644 --- a/src/MaksIT.Core/Security/PasswordHasher.cs +++ b/src/MaksIT.Core/Security/PasswordHasher.cs @@ -23,21 +23,40 @@ public static class PasswordHasher { return Convert.ToBase64String(valueBytes); } - public static (string Salt, string Hash) CreateSaltedHash(string value) { - var saltBytes = CreateSaltBytes(); - var hash = CreateHash(value, saltBytes); - var salt = Convert.ToBase64String(saltBytes); + public static bool TryCreateSaltedHash(string value, out (string Salt, string Hash)? saltedHash, out string? errorMessage) { + try { + var saltBytes = CreateSaltBytes(); + var hash = CreateHash(value, saltBytes); + var salt = Convert.ToBase64String(saltBytes); - return (salt, hash); + saltedHash = (salt, hash); + errorMessage = null; + return true; + } + catch (Exception ex) { + saltedHash = null; + errorMessage = ex.Message; + return false; + } } - public static bool ValidateHash(string value, string salt, string hash) { - var saltBytes = Convert.FromBase64String(salt); - var hashToCompare = CreateHash(value, saltBytes); + public static bool TryValidateHash(string value, string salt, string hash, out bool isValid, out string? errorMessage) { + try { + var saltBytes = Convert.FromBase64String(salt); + var hashToCompare = CreateHash(value, saltBytes); - return CryptographicOperations.FixedTimeEquals( - Convert.FromBase64String(hashToCompare), - Convert.FromBase64String(hash) - ); + isValid = CryptographicOperations.FixedTimeEquals( + Convert.FromBase64String(hashToCompare), + Convert.FromBase64String(hash) + ); + + errorMessage = null; + return true; + } + catch (Exception ex) { + isValid = false; + errorMessage = ex.Message; + return false; + } } -} \ No newline at end of file +} diff --git a/src/MaksIT.Core/Security/TotpGenerator.cs b/src/MaksIT.Core/Security/TotpGenerator.cs index 303fb0e..f1690d4 100644 --- a/src/MaksIT.Core/Security/TotpGenerator.cs +++ b/src/MaksIT.Core/Security/TotpGenerator.cs @@ -1,34 +1,60 @@ using System.Security.Cryptography; - namespace MaksIT.Core.Security; public static class TotpGenerator { private const int Timestep = 30; // Time step in seconds (standard is 30 seconds) private const int TotpDigits = 6; // Standard TOTP length is 6 digits - public static bool Validate(string totpCode, string base32Secret, int timeTolerance = 1) { - // Convert the Base32 encoded secret to a byte array - byte[] secretBytes = Base32Encoder.Decode(base32Secret); - - // Get current timestamp - long timeStepWindow = GetCurrentTimeStepNumber(); - - // Validate the TOTP code against the valid time windows (current and around it) - for (int i = -timeTolerance; i <= timeTolerance; i++) { - var generatedTotp = Generate(secretBytes, timeStepWindow + i); - if (generatedTotp == totpCode) { - return true; + public static bool TryValidate(string totpCode, string base32Secret, int timeTolerance, out bool isValid, out string? errorMessage) { + try { + // Convert the Base32 encoded secret to a byte array + if (!Base32Encoder.TryDecode(base32Secret, out byte[]? secretBytes, out errorMessage)) { + isValid = false; + return false; } - } - return false; + // Get current timestamp + long timeStepWindow = GetCurrentTimeStepNumber(); + + // Validate the TOTP code against the valid time windows (current and around it) + for (int i = -timeTolerance; i <= timeTolerance; i++) { + var generatedTotp = Generate(secretBytes, timeStepWindow + i); + if (generatedTotp == totpCode) { + isValid = true; + errorMessage = null; + return true; + } + } + + isValid = false; + errorMessage = null; + return true; + } + catch (Exception ex) { + isValid = false; + errorMessage = ex.Message; + return false; + } } - public static string Generate(string base32Secret, long timestep) { - // Convert the Base32 encoded secret to a byte array - byte[] secretBytes = Base32Encoder.Decode(base32Secret); - return Generate(secretBytes, timestep); + public static bool TryGenerate(string base32Secret, long timestep, out string? totpCode, out string? errorMessage) { + try { + // Convert the Base32 encoded secret to a byte array + if (!Base32Encoder.TryDecode(base32Secret, out byte[]? secretBytes, out errorMessage)) { + totpCode = null; + return false; + } + + totpCode = Generate(secretBytes, timestep); + errorMessage = null; + return true; + } + catch (Exception ex) { + totpCode = null; + errorMessage = ex.Message; + return false; + } } private static string Generate(byte[] secretBytes, long timestep) { @@ -60,51 +86,76 @@ public static class TotpGenerator { return unixTimestamp / Timestep; } - public static string GenerateSecret() { - // Example of generating a 32-character base32 secret for TOTP - var random = new byte[20]; - using (var rng = RandomNumberGenerator.Create()) { - rng.GetBytes(random); - } + public static bool TryGenerateSecret(out string? secret, out string? errorMessage) { + try { + // Example of generating a 32-character base32 secret for TOTP + var random = new byte[20]; + using (var rng = RandomNumberGenerator.Create()) { + rng.GetBytes(random); + } - return Base32Encoder.Encode(random); // You can use a Base32 encoder to generate the secret. + if (!Base32Encoder.TryEncode(random, out secret, out errorMessage)) { + return false; + } + + errorMessage = null; + return true; + } + catch (Exception ex) { + secret = null; + errorMessage = ex.Message; + return false; + } } - public static List GenerateRecoveryCodes(int defaultCodeCount = 6) { - var recoveryCodes = new List(); + public static bool TryGenerateRecoveryCodes(int defaultCodeCount, out List? recoveryCodes, out string? errorMessage) { + try { + recoveryCodes = new List(); - for (int i = 0; i < defaultCodeCount; i++) { - var code = Guid.NewGuid().ToString("N").Substring(0, 8); // Generate an 8-character code - var formattedCode = $"{code.Substring(0, 4)}-{code.Substring(4, 4)}"; // Format as XXXX-XXXX - recoveryCodes.Add(formattedCode); + for (int i = 0; i < defaultCodeCount; i++) { + var code = Guid.NewGuid().ToString("N").Substring(0, 8); // Generate an 8-character code + var formattedCode = $"{code.Substring(0, 4)}-{code.Substring(4, 4)}"; // Format as XXXX-XXXX + recoveryCodes.Add(formattedCode); + } + + errorMessage = null; + return true; + } + catch (Exception ex) { + recoveryCodes = null; + errorMessage = ex.Message; + return false; } - - return recoveryCodes; } - public static string GenerateTotpAuthLink(string label, string username, string twoFactoSharedKey, string issuer, string? algorithm = null, int? digits = null, int? period = null) { + public static bool TryGenerateTotpAuthLink(string label, string username, string twoFactoSharedKey, string issuer, string? algorithm, int? digits, int? period, out string? authLink, out string? errorMessage) { + try { + var queryParams = new List { + $"secret={Uri.EscapeDataString(twoFactoSharedKey)}", + $"issuer={Uri.EscapeDataString(issuer)}" + }; - var queryParams = new List { - $"secret={Uri.EscapeDataString(twoFactoSharedKey)}", - $"issuer={Uri.EscapeDataString(issuer)}" - }; + if (algorithm != null) { + queryParams.Add($"algorithm={Uri.EscapeDataString(algorithm)}"); + } - if (algorithm != null) { - queryParams.Add($"algorithm={Uri.EscapeDataString(algorithm)}"); + if (digits != null) { + queryParams.Add($"digits={digits}"); + } + + if (period != null) { + queryParams.Add($"period={period}"); + } + + var queryString = string.Join("&", queryParams); + authLink = $"otpauth://totp/{Uri.EscapeDataString(label)}:{Uri.EscapeDataString(username)}?{queryString}"; + errorMessage = null; + return true; } - - if (digits != null) { - queryParams.Add($"digits={digits}"); + catch (Exception ex) { + authLink = null; + errorMessage = ex.Message; + return false; } - - if (period != null) { - queryParams.Add($"period={period}"); - } - - var queryString = string.Join("&", queryParams); - var authLink = $"otpauth://totp/{Uri.EscapeDataString(label)}:{Uri.EscapeDataString(username)}?{queryString}"; - - return authLink; } } -