(feature): codebase and documentation update
This commit is contained in:
parent
fe37531300
commit
594789f44e
98
src/MaksIT.Core.Tests/Security/AESGCMUtilityTests.cs
Normal file
98
src/MaksIT.Core.Tests/Security/AESGCMUtilityTests.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,53 +1,65 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
using MaksIT.Core.Security;
|
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 {
|
// Assert
|
||||||
[Fact]
|
Assert.True(result);
|
||||||
public void Encode_ValidInput_ReturnsExpectedBase32String() {
|
Assert.Equal(expected, encoded);
|
||||||
// Arrange
|
Assert.Null(errorMessage);
|
||||||
var input = Encoding.UTF8.GetBytes("Hello World");
|
}
|
||||||
var expected = "JBSWY3DPEBLW64TMMQ======";
|
|
||||||
|
|
||||||
// Act
|
[Fact]
|
||||||
var result = Base32Encoder.Encode(input);
|
public void Decode_ValidBase32String_ReturnsExpectedByteArray() {
|
||||||
|
// Arrange
|
||||||
|
var input = "JBSWY3DPEBLW64TMMQ======";
|
||||||
|
var expected = Encoding.UTF8.GetBytes("Hello World");
|
||||||
|
|
||||||
// Assert
|
// Act
|
||||||
Assert.Equal(expected, result);
|
var result = Base32Encoder.TryDecode(input, out var decoded, out var errorMessage);
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
// Assert
|
||||||
public void Decode_ValidBase32String_ReturnsExpectedByteArray() {
|
Assert.True(result);
|
||||||
// Arrange
|
Assert.Equal(expected, decoded);
|
||||||
var input = "JBSWY3DPEBLW64TMMQ======";
|
Assert.Null(errorMessage);
|
||||||
var expected = Encoding.UTF8.GetBytes("Hello World");
|
}
|
||||||
|
|
||||||
// Act
|
[Fact]
|
||||||
var result = Base32Encoder.Decode(input);
|
public void Decode_InvalidBase32String_ReturnsFalse() {
|
||||||
|
// Act
|
||||||
|
var result = Base32Encoder.TryDecode("InvalidBase32String", out var decoded, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(expected, result);
|
Assert.False(result);
|
||||||
}
|
Assert.Null(decoded);
|
||||||
|
Assert.NotNull(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Decode_InvalidBase32String_ThrowsFormatException() {
|
public void EncodeDecode_RoundTrip_ReturnsOriginalData() {
|
||||||
// Act & Assert
|
// Arrange
|
||||||
Assert.Throws<FormatException>(() => Base32Encoder.Decode("InvalidBase32String"));
|
var originalData = Encoding.UTF8.GetBytes("RoundTripTest");
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
// Act
|
||||||
public void EncodeDecode_RoundTrip_ReturnsOriginalData() {
|
var encodeResult = Base32Encoder.TryEncode(originalData, out var encoded, out var encodeErrorMessage);
|
||||||
// Arrange
|
var decodeResult = Base32Encoder.TryDecode(encoded, out var decoded, out var decodeErrorMessage);
|
||||||
var originalData = Encoding.UTF8.GetBytes("RoundTripTest");
|
|
||||||
|
|
||||||
// Act
|
// Assert
|
||||||
var encoded = Base32Encoder.Encode(originalData);
|
Assert.True(encodeResult);
|
||||||
var decoded = Base32Encoder.Decode(encoded);
|
Assert.True(decodeResult);
|
||||||
|
Assert.Equal(originalData, decoded);
|
||||||
// Assert
|
Assert.Null(encodeErrorMessage);
|
||||||
Assert.Equal(originalData, decoded);
|
Assert.Null(decodeErrorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
180
src/MaksIT.Core.Tests/Security/ChecksumUtilityTests.cs
Normal file
180
src/MaksIT.Core.Tests/Security/ChecksumUtilityTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/MaksIT.Core.Tests/Security/Crc32Tests.cs
Normal file
107
src/MaksIT.Core.Tests/Security/Crc32Tests.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<string> Roles = new List<string> { "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 {
|
// Assert
|
||||||
private const string Secret = "supersecretkey12345678901234567890";
|
Assert.True(result);
|
||||||
private const string Issuer = "testIssuer";
|
Assert.NotNull(tokenData);
|
||||||
private const string Audience = "testAudience";
|
Assert.False(string.IsNullOrEmpty(tokenData?.Item1));
|
||||||
private const double Expiration = 30; // 30 minutes
|
Assert.Null(errorMessage);
|
||||||
private const string Username = "testUser";
|
}
|
||||||
private readonly List<string> Roles = new List<string> { "Admin", "User" };
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GenerateToken_ShouldReturnValidToken() {
|
public void ValidateToken_ShouldReturnClaimsPrincipal_WhenTokenIsValid() {
|
||||||
// Act
|
// Arrange
|
||||||
var (token, jwtTokenClaims) = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles);
|
JwtGenerator.TryGenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles, out var tokenData, out var generateErrorMessage);
|
||||||
|
|
||||||
// Assert
|
// Act
|
||||||
Assert.False(string.IsNullOrEmpty(token));
|
var result = JwtGenerator.TryValidateToken(Secret, Issuer, Audience, tokenData?.Item1, out var jwtTokenClaims, out var validateErrorMessage);
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
// Assert
|
||||||
public void ValidateToken_ShouldReturnClaimsPrincipal_WhenTokenIsValid() {
|
Assert.True(result);
|
||||||
// Arrange
|
Assert.NotNull(jwtTokenClaims);
|
||||||
var (token, _) = JwtGenerator.GenerateToken(Secret, Issuer, Audience, Expiration, Username, Roles);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// Act
|
[Fact]
|
||||||
var jwtTokenClaims = JwtGenerator.ValidateToken(Secret, Issuer, Audience, token);
|
public void ValidateToken_ShouldReturnNull_WhenTokenIsInvalid() {
|
||||||
|
// Arrange
|
||||||
|
var invalidToken = "invalidToken";
|
||||||
|
|
||||||
// Assert
|
// Act
|
||||||
Assert.NotNull(jwtTokenClaims);
|
var result = JwtGenerator.TryValidateToken(Secret, Issuer, Audience, invalidToken, out var jwtTokenClaims, out var errorMessage);
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
// Assert
|
||||||
public void ValidateToken_ShouldReturnNull_WhenTokenIsInvalid() {
|
Assert.False(result);
|
||||||
// Arrange
|
Assert.Null(jwtTokenClaims);
|
||||||
var invalidToken = "invalidToken";
|
Assert.NotNull(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
// Act
|
[Fact]
|
||||||
var principal = JwtGenerator.ValidateToken(Secret, Issuer, Audience, invalidToken);
|
public void GenerateRefreshToken_ShouldReturnNonEmptyString() {
|
||||||
|
// Act
|
||||||
|
var refreshToken = JwtGenerator.GenerateRefreshToken();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Null(principal);
|
Assert.False(string.IsNullOrEmpty(refreshToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GenerateRefreshToken_ShouldReturnNonEmptyString() {
|
public void GenerateSecret_ShouldReturnDifferentValuesOnSubsequentCalls() {
|
||||||
// Act
|
// Act
|
||||||
var refreshToken = JwtGenerator.GenerateRefreshToken();
|
string secret1 = JwtGenerator.GenerateSecret();
|
||||||
|
string secret2 = JwtGenerator.GenerateSecret();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(string.IsNullOrEmpty(refreshToken));
|
Assert.False(string.IsNullOrEmpty(secret1));
|
||||||
}
|
Assert.False(string.IsNullOrEmpty(secret2));
|
||||||
|
Assert.NotEqual(secret1, secret2); // Ensure the secrets are unique
|
||||||
[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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,11 +9,14 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var password = "SecurePassword123!";
|
var password = "SecurePassword123!";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = PasswordHasher.CreateSaltedHash(password);
|
var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(string.IsNullOrWhiteSpace(result.Salt));
|
Assert.True(result);
|
||||||
Assert.False(string.IsNullOrWhiteSpace(result.Hash));
|
Assert.NotNull(saltedHash);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Salt));
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Hash));
|
||||||
|
Assert.Null(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -22,11 +25,14 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var password = "";
|
var password = "";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = PasswordHasher.CreateSaltedHash(password);
|
var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(string.IsNullOrWhiteSpace(result.Salt));
|
Assert.True(result);
|
||||||
Assert.False(string.IsNullOrWhiteSpace(result.Hash));
|
Assert.NotNull(saltedHash);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Salt));
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Hash));
|
||||||
|
Assert.Null(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -35,24 +41,29 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var password = " ";
|
var password = " ";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = PasswordHasher.CreateSaltedHash(password);
|
var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(string.IsNullOrWhiteSpace(result.Salt));
|
Assert.True(result);
|
||||||
Assert.False(string.IsNullOrWhiteSpace(result.Hash));
|
Assert.NotNull(saltedHash);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Salt));
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(saltedHash?.Hash));
|
||||||
|
Assert.Null(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ValidateHash_CorrectPassword_ReturnsTrue() {
|
public void ValidateHash_CorrectPassword_ReturnsTrue() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var password = "SecurePassword123!";
|
var password = "SecurePassword123!";
|
||||||
var hashResult = PasswordHasher.CreateSaltedHash(password);
|
PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var createErrorMessage);
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.True(verifyResult);
|
Assert.True(result);
|
||||||
|
Assert.True(isValid);
|
||||||
|
Assert.Null(validateErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -60,13 +71,15 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
// Arrange
|
// Arrange
|
||||||
var password = "SecurePassword123!";
|
var password = "SecurePassword123!";
|
||||||
var wrongPassword = "WrongPassword456!";
|
var wrongPassword = "WrongPassword456!";
|
||||||
var hashResult = PasswordHasher.CreateSaltedHash(password);
|
PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var createErrorMessage);
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.False(verifyResult);
|
Assert.True(result);
|
||||||
|
Assert.False(isValid);
|
||||||
|
Assert.Null(validateErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -77,10 +90,12 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var salt = ""; // Assuming empty salt
|
var salt = ""; // Assuming empty salt
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var verifyResult = PasswordHasher.ValidateHash(password, salt, storedHash);
|
var result = PasswordHasher.TryValidateHash(password, salt, storedHash, out var isValid, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(verifyResult);
|
Assert.True(result);
|
||||||
|
Assert.False(isValid);
|
||||||
|
Assert.Null(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -91,10 +106,12 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var salt = " ";
|
var salt = " ";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var verifyResult = PasswordHasher.ValidateHash(password, salt, storedHash);
|
var result = PasswordHasher.TryValidateHash(password, salt, storedHash, out var isValid, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(verifyResult);
|
Assert.True(result);
|
||||||
|
Assert.False(isValid);
|
||||||
|
Assert.Null(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -105,10 +122,12 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var invalidSalt = "InvalidSaltValue";
|
var invalidSalt = "InvalidSaltValue";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var verifyResult = PasswordHasher.ValidateHash(password, invalidSalt, invalidStoredHash);
|
var result = PasswordHasher.TryValidateHash(password, invalidSalt, invalidStoredHash, out var isValid, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(verifyResult);
|
Assert.True(result);
|
||||||
|
Assert.False(isValid);
|
||||||
|
Assert.Null(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -117,29 +136,33 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var password = "SecurePassword123!";
|
var password = "SecurePassword123!";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var hashResult1 = PasswordHasher.CreateSaltedHash(password);
|
PasswordHasher.TryCreateSaltedHash(password, out var hashResult1, out var errorMessage1);
|
||||||
var hashResult2 = PasswordHasher.CreateSaltedHash(password);
|
PasswordHasher.TryCreateSaltedHash(password, out var hashResult2, out var errorMessage2);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotEqual(hashResult1.Hash, hashResult2.Hash);
|
Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ValidateHash_ModifiedStoredHash_ReturnsFalse() {
|
public void ValidateHash_ModifiedStoredHash_ReturnsFalse() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var password = "SecurePassword123!";
|
var password = "SecurePassword123!";
|
||||||
var hashResult = PasswordHasher.CreateSaltedHash(password);
|
PasswordHasher.TryCreateSaltedHash(password, out var hashResult, out var createErrorMessage);
|
||||||
|
|
||||||
// Modify the stored hash
|
// Modify the stored hash
|
||||||
var hashChars = hashResult.Hash.ToCharArray();
|
var hashChars = hashResult?.Hash.ToCharArray();
|
||||||
hashChars[10] = (hashChars[10] == 'A') ? 'B' : 'A'; // Change one character
|
if (hashChars != null) {
|
||||||
|
hashChars[10] = (hashChars[10] == 'A') ? 'B' : 'A'; // Change one character
|
||||||
|
}
|
||||||
var modifiedHash = new string(hashChars);
|
var modifiedHash = new string(hashChars);
|
||||||
|
|
||||||
// Act
|
// 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
|
||||||
Assert.False(verifyResult);
|
Assert.True(result);
|
||||||
|
Assert.False(isValid);
|
||||||
|
Assert.Null(validateErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -149,11 +172,11 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var password2 = "PasswordTwo";
|
var password2 = "PasswordTwo";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var hashResult1 = PasswordHasher.CreateSaltedHash(password1);
|
PasswordHasher.TryCreateSaltedHash(password1, out var hashResult1, out var errorMessage1);
|
||||||
var hashResult2 = PasswordHasher.CreateSaltedHash(password2);
|
PasswordHasher.TryCreateSaltedHash(password2, out var hashResult2, out var errorMessage2);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotEqual(hashResult1.Hash, hashResult2.Hash);
|
Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -162,13 +185,13 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var password = "SecurePassword123!";
|
var password = "SecurePassword123!";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = PasswordHasher.CreateSaltedHash(password);
|
var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// For 16 bytes salt, Base64 length is 24 characters
|
// 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
|
// For 32 bytes hash, Base64 length is 44 characters
|
||||||
Assert.Equal(44, result.Hash.Length);
|
Assert.Equal(44, saltedHash?.Hash.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,13 +11,15 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
public void Validate_ValidTotpCode_ReturnsTrue() {
|
public void Validate_ValidTotpCode_ReturnsTrue() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var timestep = TotpGenerator.GetCurrentTimeStepNumber();
|
var timestep = TotpGenerator.GetCurrentTimeStepNumber();
|
||||||
var validTotpCode = TotpGenerator.Generate(Base32Secret, timestep);
|
TotpGenerator.TryGenerate(Base32Secret, timestep, out var validTotpCode, out var generateErrorMessage);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var isValid = TotpGenerator.Validate(validTotpCode, Base32Secret);
|
var result = TotpGenerator.TryValidate(validTotpCode, Base32Secret, 0, out var isValid, out var validateErrorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
Assert.True(isValid);
|
Assert.True(isValid);
|
||||||
|
Assert.Null(validateErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -26,23 +28,27 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var invalidTotpCode = "123456"; // Example invalid TOTP code
|
var invalidTotpCode = "123456"; // Example invalid TOTP code
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var isValid = TotpGenerator.Validate(invalidTotpCode, Base32Secret);
|
var result = TotpGenerator.TryValidate(invalidTotpCode, Base32Secret, 0, out var isValid, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
Assert.False(isValid);
|
Assert.False(isValid);
|
||||||
|
Assert.Null(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_TotpCodeWithTimeTolerance_ReturnsTrue() {
|
public void Validate_TotpCodeWithTimeTolerance_ReturnsTrue() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var timestep = TotpGenerator.GetCurrentTimeStepNumber() - 1; // One timestep in the past
|
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
|
// Act
|
||||||
var isValid = TotpGenerator.Validate(validTotpCode, Base32Secret, timeTolerance: 1);
|
var result = TotpGenerator.TryValidate(validTotpCode, Base32Secret, 1, out var isValid, out var validateErrorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
Assert.True(isValid);
|
Assert.True(isValid);
|
||||||
|
Assert.Null(validateErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -51,11 +57,13 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
var timestep = TotpGenerator.GetCurrentTimeStepNumber();
|
var timestep = TotpGenerator.GetCurrentTimeStepNumber();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var totpCode = TotpGenerator.Generate(Base32Secret, timestep);
|
var result = TotpGenerator.TryGenerate(Base32Secret, timestep, out var totpCode, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
Assert.False(string.IsNullOrEmpty(totpCode));
|
Assert.False(string.IsNullOrEmpty(totpCode));
|
||||||
Assert.Equal(6, totpCode.Length);
|
Assert.Equal(6, totpCode.Length);
|
||||||
|
Assert.Null(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -70,13 +78,13 @@ namespace MaksIT.Core.Tests.Security {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void GenerateSecret_ReturnsValidBase32String() {
|
public void GenerateSecret_ReturnsValidBase32String() {
|
||||||
// Act
|
// Act
|
||||||
var secret = TotpGenerator.GenerateSecret();
|
var result = TotpGenerator.TryGenerateSecret(out var secret, out var errorMessage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
Assert.False(string.IsNullOrEmpty(secret));
|
Assert.False(string.IsNullOrEmpty(secret));
|
||||||
Assert.True(secret.IsBase32String());
|
Assert.True(secret.IsBase32String());
|
||||||
|
Assert.Null(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
src/MaksIT.Core/Culture.cs
Normal file
38
src/MaksIT.Core/Culture.cs
Normal 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
93
src/MaksIT.Core/EnvVar.cs
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,4 +32,3 @@ public static class ExpressionExtensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
177
src/MaksIT.Core/FileSystem.cs
Normal file
177
src/MaksIT.Core/FileSystem.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/MaksIT.Core/Logging/FileLogger.cs
Normal file
37
src/MaksIT.Core/Logging/FileLogger.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/MaksIT.Core/Logging/FileLoggerProvider.cs
Normal file
23
src/MaksIT.Core/Logging/FileLoggerProvider.cs
Normal 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() { }
|
||||||
|
}
|
||||||
12
src/MaksIT.Core/Logging/LoggingBuilderExtensions.cs
Normal file
12
src/MaksIT.Core/Logging/LoggingBuilderExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<!-- NuGet package metadata -->
|
<!-- NuGet package metadata -->
|
||||||
<PackageId>MaksIT.Core</PackageId>
|
<PackageId>MaksIT.Core</PackageId>
|
||||||
<Version>1.1.6</Version>
|
<Version>1.1.7</Version>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
<Company>MAKS-IT</Company>
|
<Company>MAKS-IT</Company>
|
||||||
<Product>MaksIT.Core</Product>
|
<Product>MaksIT.Core</Product>
|
||||||
@ -26,6 +26,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.8" />
|
<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="Microsoft.IdentityModel.Tokens" Version="8.1.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.1.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
94
src/MaksIT.Core/Networking/PingPort.cs
Normal file
94
src/MaksIT.Core/Networking/PingPort.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/MaksIT.Core/Networking/Windows/NetworkConnection.cs
Normal file
118
src/MaksIT.Core/Networking/Windows/NetworkConnection.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/MaksIT.Core/Processes.cs
Normal file
80
src/MaksIT.Core/Processes.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/MaksIT.Core/Security/AESGCMUtility.cs
Normal file
75
src/MaksIT.Core/Security/AESGCMUtility.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,75 +9,93 @@ public static class Base32Encoder {
|
|||||||
private static readonly char[] Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
|
private static readonly char[] Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
|
||||||
private const string PaddingChar = "=";
|
private const string PaddingChar = "=";
|
||||||
|
|
||||||
public static string Encode(byte[] data) {
|
public static bool TryEncode(byte[] data, out string? encoded, out string? errorMessage) {
|
||||||
if (data == null || data.Length == 0) {
|
try {
|
||||||
throw new ArgumentNullException(nameof(data));
|
if (data == null || data.Length == 0) {
|
||||||
}
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
}
|
||||||
|
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
int buffer = data[0];
|
int buffer = data[0];
|
||||||
int next = 1;
|
int next = 1;
|
||||||
int bitsLeft = 8;
|
int bitsLeft = 8;
|
||||||
while (bitsLeft > 0 || next < data.Length) {
|
while (bitsLeft > 0 || next < data.Length) {
|
||||||
if (bitsLeft < 5) {
|
if (bitsLeft < 5) {
|
||||||
if (next < data.Length) {
|
if (next < data.Length) {
|
||||||
buffer <<= 8;
|
buffer <<= 8;
|
||||||
buffer |= (data[next++] & 0xFF);
|
buffer |= (data[next++] & 0xFF);
|
||||||
bitsLeft += 8;
|
bitsLeft += 8;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int pad = 5 - bitsLeft;
|
||||||
|
buffer <<= pad;
|
||||||
|
bitsLeft += pad;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
int pad = 5 - bitsLeft;
|
int index = (buffer >> (bitsLeft - 5)) & 0x1F;
|
||||||
buffer <<= pad;
|
bitsLeft -= 5;
|
||||||
bitsLeft += pad;
|
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;
|
encoded = result.ToString();
|
||||||
bitsLeft -= 5;
|
errorMessage = null;
|
||||||
result.Append(Base32Alphabet[index]);
|
return true;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
// Padding for a complete block
|
encoded = null;
|
||||||
int padding = result.Length % 8;
|
errorMessage = ex.Message;
|
||||||
if (padding > 0) {
|
return false;
|
||||||
for (int i = padding; i < 8; i++) {
|
|
||||||
result.Append(PaddingChar);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] Decode(string base32) {
|
public static bool TryDecode(string base32, out byte[]? decoded, out string? errorMessage) {
|
||||||
if (string.IsNullOrEmpty(base32)) {
|
try {
|
||||||
throw new ArgumentNullException(nameof(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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer <<= 5;
|
base32 = base32.TrimEnd(PaddingChar.ToCharArray());
|
||||||
buffer |= charValue & 0x1F;
|
int byteCount = base32.Length * 5 / 8;
|
||||||
bitsLeft += 5;
|
byte[] result = new byte[byteCount];
|
||||||
|
|
||||||
if (bitsLeft >= 8) {
|
int buffer = 0;
|
||||||
result[index++] = (byte)(buffer >> (bitsLeft - 8));
|
int bitsLeft = 0;
|
||||||
bitsLeft -= 8;
|
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) {
|
private static int CharToValue(char c) {
|
||||||
@ -92,4 +110,3 @@ public static class Base32Encoder {
|
|||||||
return -1; // Invalid character
|
return -1; // Invalid character
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
src/MaksIT.Core/Security/ChecksumUtility.cs
Normal file
65
src/MaksIT.Core/Security/ChecksumUtility.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/MaksIT.Core/Security/Crc32.cs
Normal file
118
src/MaksIT.Core/Security/Crc32.cs
Normal 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System.Text;
|
||||||
using System.Text;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
@ -16,46 +14,55 @@ public class JWTTokenClaims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class JwtGenerator {
|
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) {
|
||||||
var secretKey = GetSymmetricSecurityKey(secret);
|
try {
|
||||||
var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
|
var secretKey = GetSymmetricSecurityKey(secret);
|
||||||
|
var credentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
var issuedAt = DateTime.UtcNow;
|
var issuedAt = DateTime.UtcNow;
|
||||||
var expiresAt = issuedAt.AddMinutes(expiration);
|
var expiresAt = issuedAt.AddMinutes(expiration);
|
||||||
|
|
||||||
var claims = new List<Claim>
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.Name, username),
|
new Claim(ClaimTypes.Name, username),
|
||||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||||
new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(issuedAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
|
new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(issuedAt).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
|
||||||
new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(expiresAt).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(
|
var tokenDescriptor = new JwtSecurityToken(
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
audience: audience,
|
audience: audience,
|
||||||
claims: claims,
|
claims: claims,
|
||||||
expires: expiresAt,
|
expires: expiresAt,
|
||||||
signingCredentials: credentials
|
signingCredentials: credentials
|
||||||
);
|
);
|
||||||
|
|
||||||
var jwtToken = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
|
var jwtToken = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
|
||||||
|
|
||||||
var tokenClaims = new JWTTokenClaims {
|
var tokenClaims = new JWTTokenClaims {
|
||||||
Username = username,
|
Username = username,
|
||||||
Roles = roles,
|
Roles = roles,
|
||||||
IssuedAt = issuedAt,
|
IssuedAt = issuedAt,
|
||||||
ExpiresAt = expiresAt
|
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 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 {
|
try {
|
||||||
var key = Encoding.UTF8.GetBytes(secret);
|
var key = Encoding.UTF8.GetBytes(secret);
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
@ -77,10 +84,14 @@ public static class JwtGenerator {
|
|||||||
if (validatedToken is JwtSecurityToken jwtToken && jwtToken.Header.Alg != SecurityAlgorithms.HmacSha256)
|
if (validatedToken is JwtSecurityToken jwtToken && jwtToken.Header.Alg != SecurityAlgorithms.HmacSha256)
|
||||||
throw new SecurityTokenException("Invalid token algorithm");
|
throw new SecurityTokenException("Invalid token algorithm");
|
||||||
|
|
||||||
return ExtractClaims(principal);
|
tokenClaims = ExtractClaims(principal);
|
||||||
|
errorMessage = null;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch {
|
catch (Exception ex) {
|
||||||
return null;
|
tokenClaims = null;
|
||||||
|
errorMessage = ex.Message;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,21 +23,40 @@ public static class PasswordHasher {
|
|||||||
return Convert.ToBase64String(valueBytes);
|
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) {
|
||||||
var saltBytes = CreateSaltBytes();
|
try {
|
||||||
var hash = CreateHash(value, saltBytes);
|
var saltBytes = CreateSaltBytes();
|
||||||
var salt = Convert.ToBase64String(saltBytes);
|
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) {
|
||||||
var saltBytes = Convert.FromBase64String(salt);
|
try {
|
||||||
var hashToCompare = CreateHash(value, saltBytes);
|
var saltBytes = Convert.FromBase64String(salt);
|
||||||
|
var hashToCompare = CreateHash(value, saltBytes);
|
||||||
|
|
||||||
return CryptographicOperations.FixedTimeEquals(
|
isValid = CryptographicOperations.FixedTimeEquals(
|
||||||
Convert.FromBase64String(hashToCompare),
|
Convert.FromBase64String(hashToCompare),
|
||||||
Convert.FromBase64String(hash)
|
Convert.FromBase64String(hash)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
errorMessage = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = ex.Message;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +1,60 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.Core.Security;
|
namespace MaksIT.Core.Security;
|
||||||
|
|
||||||
public static class TotpGenerator {
|
public static class TotpGenerator {
|
||||||
private const int Timestep = 30; // Time step in seconds (standard is 30 seconds)
|
private const int Timestep = 30; // Time step in seconds (standard is 30 seconds)
|
||||||
private const int TotpDigits = 6; // Standard TOTP length is 6 digits
|
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) {
|
||||||
// Convert the Base32 encoded secret to a byte array
|
try {
|
||||||
byte[] secretBytes = Base32Encoder.Decode(base32Secret);
|
// Convert the Base32 encoded secret to a byte array
|
||||||
|
if (!Base32Encoder.TryDecode(base32Secret, out byte[]? secretBytes, out errorMessage)) {
|
||||||
// Get current timestamp
|
isValid = false;
|
||||||
long timeStepWindow = GetCurrentTimeStepNumber();
|
return false;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
public static bool TryGenerate(string base32Secret, long timestep, out string? totpCode, out string? errorMessage) {
|
||||||
// Convert the Base32 encoded secret to a byte array
|
try {
|
||||||
byte[] secretBytes = Base32Encoder.Decode(base32Secret);
|
// Convert the Base32 encoded secret to a byte array
|
||||||
return Generate(secretBytes, timestep);
|
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) {
|
private static string Generate(byte[] secretBytes, long timestep) {
|
||||||
@ -60,51 +86,76 @@ public static class TotpGenerator {
|
|||||||
return unixTimestamp / Timestep;
|
return unixTimestamp / Timestep;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GenerateSecret() {
|
public static bool TryGenerateSecret(out string? secret, out string? errorMessage) {
|
||||||
// Example of generating a 32-character base32 secret for TOTP
|
try {
|
||||||
var random = new byte[20];
|
// Example of generating a 32-character base32 secret for TOTP
|
||||||
using (var rng = RandomNumberGenerator.Create()) {
|
var random = new byte[20];
|
||||||
rng.GetBytes(random);
|
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<string> GenerateRecoveryCodes(int defaultCodeCount = 6) {
|
public static bool TryGenerateRecoveryCodes(int defaultCodeCount, out List<string>? recoveryCodes, out string? errorMessage) {
|
||||||
var recoveryCodes = new List<string>();
|
try {
|
||||||
|
recoveryCodes = new List<string>();
|
||||||
|
|
||||||
for (int i = 0; i < defaultCodeCount; i++) {
|
for (int i = 0; i < defaultCodeCount; i++) {
|
||||||
var code = Guid.NewGuid().ToString("N").Substring(0, 8); // Generate an 8-character code
|
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
|
var formattedCode = $"{code.Substring(0, 4)}-{code.Substring(4, 4)}"; // Format as XXXX-XXXX
|
||||||
recoveryCodes.Add(formattedCode);
|
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<string> {
|
||||||
|
$"secret={Uri.EscapeDataString(twoFactoSharedKey)}",
|
||||||
|
$"issuer={Uri.EscapeDataString(issuer)}"
|
||||||
|
};
|
||||||
|
|
||||||
var queryParams = new List<string> {
|
if (algorithm != null) {
|
||||||
$"secret={Uri.EscapeDataString(twoFactoSharedKey)}",
|
queryParams.Add($"algorithm={Uri.EscapeDataString(algorithm)}");
|
||||||
$"issuer={Uri.EscapeDataString(issuer)}"
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (algorithm != null) {
|
if (digits != null) {
|
||||||
queryParams.Add($"algorithm={Uri.EscapeDataString(algorithm)}");
|
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;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
if (digits != null) {
|
authLink = null;
|
||||||
queryParams.Add($"digits={digits}");
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user