(feature): codebase and documentation update

This commit is contained in:
Maksym Sadovnychyy 2024-11-21 22:04:11 +01:00
parent fe37531300
commit 594789f44e
26 changed files with 4270 additions and 595 deletions

2802
README.md

File diff suppressed because it is too large Load Diff

View File

@ -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
}
}
}

View File

@ -1,10 +1,8 @@
using System.Text;
using MaksIT.Core.Security;
using Xunit;
namespace MaksIT.Core.Tests.Security;
namespace MaksIT.Core.Tests.Security {
public class Base32EncoderTests {
[Fact]
public void Encode_ValidInput_ReturnsExpectedBase32String() {
@ -13,10 +11,12 @@ public class Base32EncoderTests {
var expected = "JBSWY3DPEBLW64TMMQ======";
// Act
var result = Base32Encoder.Encode(input);
var result = Base32Encoder.TryEncode(input, out var encoded, out var errorMessage);
// Assert
Assert.Equal(expected, result);
Assert.True(result);
Assert.Equal(expected, encoded);
Assert.Null(errorMessage);
}
[Fact]
@ -26,16 +26,23 @@ public class Base32EncoderTests {
var expected = Encoding.UTF8.GetBytes("Hello World");
// Act
var result = Base32Encoder.Decode(input);
var result = Base32Encoder.TryDecode(input, out var decoded, out var errorMessage);
// Assert
Assert.Equal(expected, result);
Assert.True(result);
Assert.Equal(expected, decoded);
Assert.Null(errorMessage);
}
[Fact]
public void Decode_InvalidBase32String_ThrowsFormatException() {
// Act & Assert
Assert.Throws<FormatException>(() => Base32Encoder.Decode("InvalidBase32String"));
public void Decode_InvalidBase32String_ReturnsFalse() {
// Act
var result = Base32Encoder.TryDecode("InvalidBase32String", out var decoded, out var errorMessage);
// Assert
Assert.False(result);
Assert.Null(decoded);
Assert.NotNull(errorMessage);
}
[Fact]
@ -44,10 +51,15 @@ public class Base32EncoderTests {
var originalData = Encoding.UTF8.GetBytes("RoundTripTest");
// Act
var encoded = Base32Encoder.Encode(originalData);
var decoded = Base32Encoder.Decode(encoded);
var encodeResult = Base32Encoder.TryEncode(originalData, out var encoded, out var encodeErrorMessage);
var decodeResult = Base32Encoder.TryDecode(encoded, out var decoded, out var decodeErrorMessage);
// Assert
Assert.True(encodeResult);
Assert.True(decodeResult);
Assert.Equal(originalData, decoded);
Assert.Null(encodeErrorMessage);
Assert.Null(decodeErrorMessage);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}
}

View File

@ -1,8 +1,8 @@
using MaksIT.Core.Security;
namespace MaksIT.Core.Tests.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";
@ -14,25 +14,30 @@ public class JwtGeneratorTests {
[Fact]
public void GenerateToken_ShouldReturnValidToken() {
// Act
var (token, jwtTokenClaims) = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles);
var result = JwtGenerator.TryGenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles, out var tokenData, out var errorMessage);
// Assert
Assert.False(string.IsNullOrEmpty(token));
Assert.True(result);
Assert.NotNull(tokenData);
Assert.False(string.IsNullOrEmpty(tokenData?.Item1));
Assert.Null(errorMessage);
}
[Fact]
public void ValidateToken_ShouldReturnClaimsPrincipal_WhenTokenIsValid() {
// Arrange
var (token, _) = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles);
JwtGenerator.TryGenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles, out var tokenData, out var generateErrorMessage);
// Act
var jwtTokenClaims = JwtGenerator.ValidateToken(Secret, Issuer, Audience, token);
var result = JwtGenerator.TryValidateToken(Secret, Issuer, Audience, tokenData?.Item1, out var jwtTokenClaims, out var validateErrorMessage);
// Assert
Assert.True(result);
Assert.NotNull(jwtTokenClaims);
Assert.Equal(Username, jwtTokenClaims.Username);
Assert.Contains(jwtTokenClaims.Roles ?? new List<string>(), c => c == "Admin");
Assert.Contains(jwtTokenClaims.Roles ?? new List<string>(), c => c == "User");
Assert.Equal(Username, jwtTokenClaims?.Username);
Assert.Contains(jwtTokenClaims?.Roles ?? new List<string>(), c => c == "Admin");
Assert.Contains(jwtTokenClaims?.Roles ?? new List<string>(), c => c == "User");
Assert.Null(validateErrorMessage);
}
[Fact]
@ -41,10 +46,12 @@ public class JwtGeneratorTests {
var invalidToken = "invalidToken";
// Act
var principal = JwtGenerator.ValidateToken(Secret, Issuer, Audience, invalidToken);
var result = JwtGenerator.TryValidateToken(Secret, Issuer, Audience, invalidToken, out var jwtTokenClaims, out var errorMessage);
// Assert
Assert.Null(principal);
Assert.False(result);
Assert.Null(jwtTokenClaims);
Assert.NotNull(errorMessage);
}
[Fact]
@ -68,3 +75,4 @@ public class JwtGeneratorTests {
Assert.NotEqual(secret1, secret2); // Ensure the secrets are unique
}
}
}

View File

@ -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();
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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Globalization;
using System.Threading;
namespace MaksIT.Core;
/// <summary>
/// The main <c>Cultures</c> class.
/// Contains all methods for performing basic Cultures management.
/// </summary>
public static class Culture {
/// <summary>
/// Sets the culture for the current thread.
/// </summary>
/// <param name="culture">The culture to set. If null or empty, the invariant culture is used.</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if the operation was successful; otherwise, false.</returns>
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;
}
}
}

93
src/MaksIT.Core/EnvVar.cs Normal file
View File

@ -0,0 +1,93 @@
using System;
using System.Runtime.InteropServices;
namespace MaksIT.Core;
/// <summary>
/// Allows to Set and Unset environment variables
/// </summary>
public static class EnvVar {
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
/// <summary>
/// Adds a new path to the PATH environment variable.
/// </summary>
/// <param name="newPath">The new path to add.</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if the operation was successful; otherwise, false.</returns>
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;
}
}
/// <summary>
/// Sets an environment variable.
/// </summary>
/// <param name="envName">The name of the environment variable.</param>
/// <param name="envValue">The value of the environment variable.</param>
/// <param name="envTarget">The target of the environment variable (machine, user, process).</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if the operation was successful; otherwise, false.</returns>
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;
}
}
/// <summary>
/// Unsets an environment variable.
/// </summary>
/// <param name="envName">The name of the environment variable.</param>
/// <param name="envTarget">The target of the environment variable (machine, user, process).</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if the operation was successful; otherwise, false.</returns>
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,
};
}
}

View File

@ -32,4 +32,3 @@ public static class ExpressionExtensions {
}
}
}

View File

@ -0,0 +1,177 @@
using System.Runtime.InteropServices;
using MaksIT.Core.Extensions;
namespace MaksIT.Core;
/// <summary>
/// Main <c>FileSystem</c> class.
/// Provides basic helper methods to work with the file system.
/// </summary>
public static class FileSystem {
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
/// <summary>
/// Copies the file or folder's content to the specified folder.
/// </summary>
/// <param name="sourcePath">File or directory path.</param>
/// <param name="destDirPath">Destination directory.</param>
/// <param name="overwrite">Whether to overwrite existing files.</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if the copy operation was successful; otherwise, false.</returns>
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;
}
}
/// <summary>
/// Deletes a file or directory at the specified path.
/// </summary>
/// <param name="itemPath">File or directory path.</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if the delete operation was successful; otherwise, false.</returns>
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;
}
}
/// <summary>
/// Resolves a path with wildcards and returns all possible variants found.
/// </summary>
/// <param name="wildcardedPath">Example - @"?:\Users\*\AppData\Roa*\"</param>
/// <returns>Returns all possible, but existing path variants found.</returns>
public static List<string> ResolveWildcardedPath(string wildcardedPath) {
var response = new List<string>();
wildcardedPath = wildcardedPath.TrimEnd(Path.DirectorySeparatorChar);
if (!wildcardedPath.Contains('*') && !wildcardedPath.Contains('?')) {
response.Add(wildcardedPath);
return response;
}
var pathsCollection = new List<string> { "" };
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<string>();
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;
}
/// <summary>
/// Tests a file name for duplicates, and if it is a duplicate, assigns a new name.
/// </summary>
/// <param name="fullPath">File path to test for duplicates.</param>
/// <returns>Returns the updated file name.</returns>
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;
}
}

View File

@ -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>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) {
return logLevel != LogLevel.None;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> 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);
}
}
}

View File

@ -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() { }
}

View File

@ -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<ILoggerProvider>(new FileLoggerProvider(filePath));
return builder;
}
}

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata -->
<PackageId>MaksIT.Core</PackageId>
<Version>1.1.6</Version>
<Version>1.1.7</Version>
<Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company>
<Product>MaksIT.Core</Product>
@ -26,6 +26,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.1.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.1.0" />
</ItemGroup>

View File

@ -0,0 +1,94 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace MaksIT.Core.Networking;
/// <summary>
/// Provides network-related utility methods.
/// </summary>
public static class PingPort {
/// <summary>
/// Tries to ping a host on a specified TCP port.
/// </summary>
/// <param name="hostUri">The host URI.</param>
/// <param name="portNumber">The port number.</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if the host is reachable on the specified port; otherwise, false.</returns>
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;
}
}
/// <summary>
/// Tries to ping a host on a specified UDP port.
/// </summary>
/// <param name="hostUri">The host URI.</param>
/// <param name="portNumber">The port number.</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if the host is reachable on the specified port; otherwise, false.</returns>
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;
}
}
}
}

View File

@ -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<NetworkConnection> _logger;
private readonly string _networkName;
private NetworkConnection(ILogger<NetworkConnection> logger, string networkName) {
_logger = logger;
_networkName = networkName;
}
public static bool TryCreate(
ILogger<NetworkConnection> 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
}
}

View File

@ -0,0 +1,80 @@
using MaksIT.Core.Extensions;
using System.Diagnostics;
namespace MaksIT.Core;
/// <summary>
/// Main <c>CustomProcess</c> class.
/// Provide helper methods to Start and Kill processes.
/// <list type="bullet">
/// <item>
/// <term>Start</term>
/// <description>Starts new process</description>
/// </item>
/// <item>
/// <term>Kill</term>
/// <description>Kills processes by name</description>
/// </item>
/// </list>
/// </summary>
public static class Processes {
/// <summary>
/// Tries to start a new process.
/// </summary>
/// <param name="fileName">The name of the file to start.</param>
/// <param name="arguments">The arguments to pass to the process.</param>
/// <param name="timeout">The timeout in seconds to wait for the process to exit.</param>
/// <param name="silent">If true, the process will be started without creating a window.</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if the process started successfully; otherwise, false.</returns>
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;
}
}
/// <summary>
/// Tries to kill processes by name.
/// </summary>
/// <param name="process">Process name. Accepts wildcards '*' or '?'</param>
/// <param name="errorMessage">The error message if the operation fails.</param>
/// <returns>True if at least one process was killed successfully; otherwise, false.</returns>
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;
}
}

View File

@ -0,0 +1,75 @@
using System.Security.Cryptography;
namespace MaksIT.Core.Security;
/// <summary>
///
/// </summary>
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);
}
}

View File

@ -9,7 +9,8 @@ public static class Base32Encoder {
private static readonly char[] Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
private const string PaddingChar = "=";
public static string Encode(byte[] 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));
}
@ -45,10 +46,19 @@ public static class Base32Encoder {
}
}
return result.ToString();
encoded = result.ToString();
errorMessage = null;
return true;
}
catch (Exception ex) {
encoded = null;
errorMessage = ex.Message;
return false;
}
}
public static byte[] Decode(string base32) {
public static bool TryDecode(string base32, out byte[]? decoded, out string? errorMessage) {
try {
if (string.IsNullOrEmpty(base32)) {
throw new ArgumentNullException(nameof(base32));
}
@ -77,7 +87,15 @@ public static class Base32Encoder {
}
}
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
}
}

View File

@ -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<byte>()).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);
}
}

View File

@ -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)
};
}

View File

@ -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,7 +14,8 @@ public class JWTTokenClaims {
}
public static class JwtGenerator {
public static (string, JWTTokenClaims) GenerateToken(string secret, string issuer, string audience, double expiration, string username, List<string> roles) {
public static bool TryGenerateToken(string secret, string issuer, string audience, double expiration, string username, List<string> roles, out (string, JWTTokenClaims)? tokenData, out string? errorMessage) {
try {
var secretKey = GetSymmetricSecurityKey(secret);
var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
@ -50,12 +49,20 @@ public static class JwtGenerator {
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;
}
}

View File

@ -23,21 +23,40 @@ public static class PasswordHasher {
return Convert.ToBase64String(valueBytes);
}
public static (string Salt, string Hash) CreateSaltedHash(string value) {
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) {
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(
isValid = CryptographicOperations.FixedTimeEquals(
Convert.FromBase64String(hashToCompare),
Convert.FromBase64String(hash)
);
errorMessage = null;
return true;
}
catch (Exception ex) {
isValid = false;
errorMessage = ex.Message;
return false;
}
}
}

View File

@ -1,15 +1,18 @@
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) {
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
byte[] secretBytes = Base32Encoder.Decode(base32Secret);
if (!Base32Encoder.TryDecode(base32Secret, out byte[]? secretBytes, out errorMessage)) {
isValid = false;
return false;
}
// Get current timestamp
long timeStepWindow = GetCurrentTimeStepNumber();
@ -18,17 +21,40 @@ public static class TotpGenerator {
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 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;
}
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);
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,18 +86,31 @@ public static class TotpGenerator {
return unixTimestamp / Timestep;
}
public static string GenerateSecret() {
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;
}
public static List<string> GenerateRecoveryCodes(int defaultCodeCount = 6) {
var recoveryCodes = new List<string>();
errorMessage = null;
return true;
}
catch (Exception ex) {
secret = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryGenerateRecoveryCodes(int defaultCodeCount, out List<string>? recoveryCodes, out string? errorMessage) {
try {
recoveryCodes = new List<string>();
for (int i = 0; i < defaultCodeCount; i++) {
var code = Guid.NewGuid().ToString("N").Substring(0, 8); // Generate an 8-character code
@ -79,11 +118,18 @@ public static class TotpGenerator {
recoveryCodes.Add(formattedCode);
}
return recoveryCodes;
errorMessage = null;
return true;
}
catch (Exception ex) {
recoveryCodes = null;
errorMessage = ex.Message;
return false;
}
}
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<string> {
$"secret={Uri.EscapeDataString(twoFactoSharedKey)}",
$"issuer={Uri.EscapeDataString(issuer)}"
@ -102,9 +148,14 @@ public static class TotpGenerator {
}
var queryString = string.Join("&", queryParams);
var authLink = $"otpauth://totp/{Uri.EscapeDataString(label)}:{Uri.EscapeDataString(username)}?{queryString}";
return authLink;
authLink = $"otpauth://totp/{Uri.EscapeDataString(label)}:{Uri.EscapeDataString(username)}?{queryString}";
errorMessage = null;
return true;
}
catch (Exception ex) {
authLink = null;
errorMessage = ex.Message;
return false;
}
}
}