Implemented the HashService and its service registration code
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
namespace LiteCharms.Features.MidrandBooks.Abstractions;
|
|
||||||
|
|
||||||
public interface IService;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
|
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Authors.Models;
|
using LiteCharms.Features.MidrandBooks.Authors.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Orders.Models;
|
using LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace LiteCharms.Features.Abstractions;
|
||||||
|
|
||||||
|
public interface IService;
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
namespace LiteCharms.Features.Extensions;
|
using LiteCharms.Features.Hasher;
|
||||||
|
using LiteCharms.Features.Hasher.Configuration;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
public static class Hash
|
public static class Hash
|
||||||
{
|
{
|
||||||
public static readonly Func<string?, string?> StringToSha256Hash = (input) =>
|
public const string HasherConfigSectionName = "HasherSettings";
|
||||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
|
|
||||||
|
|
||||||
public static readonly Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
public static IServiceCollection AddHashServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
Convert.ToHexString(SHA256.HashData(stream));
|
{
|
||||||
|
services.Configure<HasherSettings>(configuration.GetSection(HasherConfigSectionName));
|
||||||
|
|
||||||
public static readonly Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
var settings = configuration.GetSection(HasherConfigSectionName).Get<HasherSettings>();
|
||||||
Convert.ToHexString(SHA256.HashData(bytes));
|
|
||||||
|
services.AddSingleton<IHashids>(_ =>
|
||||||
|
new Hashids(settings!.Salt, minHashLength: settings.MinHashLength));
|
||||||
|
|
||||||
|
services.AddSingleton<HashService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace LiteCharms.Features.Hasher.Configuration;
|
||||||
|
|
||||||
|
public sealed class HasherSettings
|
||||||
|
{
|
||||||
|
public string? Salt { get; set; }
|
||||||
|
|
||||||
|
public int MinHashLength { get; set; }
|
||||||
|
|
||||||
|
public string? PayfastPassphrase { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using LiteCharms.Features.Abstractions;
|
||||||
|
using LiteCharms.Features.Hasher.Configuration;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Hasher;
|
||||||
|
|
||||||
|
public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings> options) : IService
|
||||||
|
{
|
||||||
|
private readonly HasherSettings settings = options.Value;
|
||||||
|
|
||||||
|
[System.Text.RegularExpressions.GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
||||||
|
private static partial System.Text.RegularExpressions.Regex HexHashRegex();
|
||||||
|
|
||||||
|
public static readonly Func<string?, string?> StringToSha256Hash = (input) =>
|
||||||
|
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
|
||||||
|
|
||||||
|
public static readonly Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
||||||
|
stream is null ? null : Convert.ToHexString(SHA256.HashData(stream));
|
||||||
|
|
||||||
|
public static readonly Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
||||||
|
bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes));
|
||||||
|
|
||||||
|
public static Result<string> ComputeMd5Hash(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
|
||||||
|
|
||||||
|
byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
||||||
|
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<bool> VerifyPayfastWebhookSignature(IDictionary<string, string> incomingFormData, string incomingSignature)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(incomingSignature))
|
||||||
|
return Result.Fail<bool>("Validation failed: Missing signature string parameter.");
|
||||||
|
|
||||||
|
// 1. Sort the parameters alphabetically and exclude the signature parameter to prevent recursive checking
|
||||||
|
var sortedFields = incomingFormData
|
||||||
|
.Where(field => field.Key != "signature")
|
||||||
|
.OrderBy(field => field.Key)
|
||||||
|
.Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}");
|
||||||
|
|
||||||
|
string payload = string.Join("&", sortedFields);
|
||||||
|
|
||||||
|
// 2. Append the secure, passphrase injected into the container pod from your environment variables
|
||||||
|
if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase))
|
||||||
|
{
|
||||||
|
payload += $"&passphrase={Uri.EscapeDataString(settings.PayfastPassphrase).Replace("%20", "+")}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Compute localized hex token
|
||||||
|
var localHashResult = ComputeMd5Hash(payload);
|
||||||
|
|
||||||
|
if (!localHashResult.IsSuccess)
|
||||||
|
return Result.Fail<bool>(localHashResult.Errors);
|
||||||
|
|
||||||
|
// 4. Constant-time secure text comparison to fully block timing analysis attacks
|
||||||
|
bool isValid = string.Equals(localHashResult.Value, incomingSignature, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return Result.Ok(isValid);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<bool>(new Error("An error occurred during MD5 verification loop.").CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input)
|
||||||
|
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
||||||
|
: Result.Ok(hasher.EncodeHex(input));
|
||||||
|
|
||||||
|
public Result<string> HashEncodeIntId(int id) => id < 0
|
||||||
|
? Result.Fail<string>("Id cannot be negative.")
|
||||||
|
: Result.Ok(hasher.Encode(id));
|
||||||
|
|
||||||
|
public Result<string> HashEncodeLongId(long id) => id < 0
|
||||||
|
? Result.Fail<string>("Id cannot be negative.")
|
||||||
|
: Result.Ok(hasher.EncodeLong(id));
|
||||||
|
|
||||||
|
public Result<int> DecodeIntIdHash(string hash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<int>("Invalid token layout.");
|
||||||
|
|
||||||
|
int[] decoded = hasher.Decode(hash);
|
||||||
|
|
||||||
|
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<int>("Invalid or modified Int hash token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<long> DecodeLongIdHash(string hash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<long>("Invalid token layout.");
|
||||||
|
|
||||||
|
long[] decoded = hasher.DecodeLong(hash);
|
||||||
|
|
||||||
|
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<long>("Invalid or modified Long hash token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<string> DecodeHexHash(string hex)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string decoded = hasher.DecodeHex(hex);
|
||||||
|
|
||||||
|
return string.IsNullOrEmpty(decoded)
|
||||||
|
? Result.Fail<string>("Invalid or corrupted hex hash.")
|
||||||
|
: Result.Ok(decoded);
|
||||||
|
}
|
||||||
|
catch (FormatException fex)
|
||||||
|
{
|
||||||
|
return Result.Fail<string>(new Error("Invalid hash structure.").CausedBy(fex));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<string>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
<!-- Quartz Scheduler-->
|
<!-- Quartz Scheduler-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Hashids.net" Version="1.7.0" />
|
||||||
<PackageReference Include="Meziantou.Analyzer" Version="3.0.96">
|
<PackageReference Include="Meziantou.Analyzer" Version="3.0.96">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -146,6 +147,7 @@
|
|||||||
|
|
||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Using Include="HashidsNet" />
|
||||||
<Using Include="System.Globalization" />
|
<Using Include="System.Globalization" />
|
||||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||||
<Using Include="Microsoft.Extensions.Hosting" />
|
<Using Include="Microsoft.Extensions.Hosting" />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using static LiteCharms.Features.Extensions.Hash;
|
using LiteCharms.Features.Hasher;
|
||||||
|
|
||||||
namespace LiteCharms.Features.S3.Abstractions;
|
namespace LiteCharms.Features.S3.Abstractions;
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
|||||||
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
var fileHash = StreamToSha256Hash(stream);
|
var fileHash = HashService.StreamToSha256Hash(stream);
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(fileHash))
|
if(string.IsNullOrWhiteSpace(fileHash))
|
||||||
return Result.Fail<string>("Failed to compute file hash.");
|
return Result.Fail<string>("Failed to compute file hash.");
|
||||||
@@ -39,7 +39,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
|||||||
Key = fileKey,
|
Key = fileKey,
|
||||||
InputStream = stream,
|
InputStream = stream,
|
||||||
ContentType = contentType,
|
ContentType = contentType,
|
||||||
UseChunkEncoding = false
|
UseChunkEncoding = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|||||||
Reference in New Issue
Block a user