Skip to content

Instantly share code, notes, and snippets.

@ddjerqq
Last active July 25, 2023 12:34
Show Gist options
  • Save ddjerqq/8772efba8f1dedb9216ed58512a6e463 to your computer and use it in GitHub Desktop.
Save ddjerqq/8772efba8f1dedb9216ed58512a6e463 to your computer and use it in GitHub Desktop.
digital signature for web API security
using System.Security.Claims;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
namespace Diamond;
public static class JwtTokenManager
{
private static readonly string Secret;
static JwtTokenManager() => Secret = Guid.NewGuid().ToString();
private static SymmetricSecurityKey Key =>
new(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("DIAMOND__KEY") ?? Secret));
private static SigningCredentials Creds => new(Key, SecurityAlgorithms.HmacSha256);
private static JwtSecurityTokenHandler Handler => new();
public static JwtSecurityToken GenerateToken(ClaimsPrincipal principal, TimeSpan? expires = null)
{
expires ??= TimeSpan.FromDays(1);
return new(
issuer: "Diamond",
audience: "Diamond",
claims: principal.Claims,
expires: DateTime.Now.Add(expires.Value),
signingCredentials: Creds
);
}
public static string WriteTokenToString(JwtSecurityToken token) => Handler.WriteToken(token);
public static bool TryExtractPrincipal(string token, out ClaimsPrincipal principal)
{
principal = null!;
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "Diamond",
ValidateAudience = true,
ValidAudience = "Diamond",
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = Key
};
try
{
principal = Handler.ValidateToken(token, validationParameters, out _);
return principal is not null;
}
catch
{
return false;
}
}
}
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
namespace Diamond;
/// <summary>
/// Salt - random Guid md5 hashed
/// HashHead - random int in range: [0x1000, 0xffff]
/// HashTail - random int in range: [0x10000000, 0x7fffffff] but in hex
/// ChecksumStart - random byte full range
/// ChecksumIndexes - 32 uints all in range [0, 64)
/// HashFormat - {HashHead}:{Sha1 of payload}:{checksum hex}:{HashTail}
/// AppToken - two random longs bit shifted, converted to hex, and combined
///
/// the payload is in the format: var payload {Salt}{timestampMs}{extractedUri}{userId}
/// </summary>
public sealed class Rules
{
private readonly Guid _salt;
private readonly int _hashHead;
private readonly int _hashTail;
private readonly byte _checksumStart;
private readonly int[] _checksumIndexes;
private readonly UInt128 _appToken;
private string HashFormat => $"{_hashHead:x4}:{{0}}:{{1:D3}}:{_hashTail:x8}";
private Rules(Guid salt, int hashHead, int hashTail, byte checksumStart, IEnumerable<int> checksumIndexes,
UInt128 appToken)
{
_salt = salt;
_hashHead = hashHead;
_hashTail = hashTail;
_checksumIndexes = checksumIndexes.ToArray();
_checksumStart = checksumStart;
_appToken = appToken;
}
public static Rules Random()
{
var salt = Guid.NewGuid();
int hashHead = RandomNumberGenerator.GetInt32(0x0, 0xffff);
int hashTail = RandomNumberGenerator.GetInt32(0x0, 0x7fffffff);
byte checksumStart = RandomNumberGenerator.GetBytes(1).First();
var checksumIndexes = Enumerable
.Range(0, 32)
.Select(_ => RandomNumberGenerator.GetInt32(0, 40));
var appToken = new UInt128(
(ulong) System.Random.Shared.NextInt64(),
(ulong) System.Random.Shared.NextInt64());
return new Rules(salt, hashHead, hashTail, checksumStart, checksumIndexes, appToken);
}
private string SignUri(string uri, long userId, DateTime timestamp)
{
var parsedUri = new Uri(uri);
var parsedUrl = $"{parsedUri.AbsolutePath}{parsedUri.Query}";
var payload = $"{_salt}:{timestamp:O}:{parsedUrl}:{userId}";
byte[] hash = SHA1.HashData(Encoding.UTF8.GetBytes(payload));
string digest = string.Concat(hash.Select(b => b.ToString("x2")));
byte[] digestBytes = Encoding.UTF8.GetBytes(digest);
var sum = (int) _checksumIndexes.Aggregate(_checksumStart, (sum, idx) => (byte) (sum + digestBytes[idx]));
sum = Math.Abs(sum);
return string.Format(HashFormat, digest, sum);
}
public JwtSecurityToken GenerateToken(string uri, long userId, DateTime timestamp)
{
string signature = SignUri(uri, userId, timestamp);
var claims = new List<Claim>
{
new("uri", uri),
new("app-token", _appToken.ToString("x16")),
new("user-id", userId.ToString()),
new("timestamp", $"{timestamp:O}"),
new("signature", signature)
};
var identity = new ClaimsIdentity(claims, "Diamond");
var principal = new ClaimsPrincipal(identity);
JwtSecurityToken token = JwtTokenManager.GenerateToken(principal);
return token;
}
public bool VerifyToken(string token)
{
if (!JwtTokenManager.TryExtractPrincipal(token, out ClaimsPrincipal principal)) return false;
var claims = principal.Claims.ToList();
string? uri = claims.FirstOrDefault(x => x.Type == "uri")?.Value;
string? appToken = claims.FirstOrDefault(x => x.Type == "app-token")?.Value;
string? userId = claims.FirstOrDefault(x => x.Type == "user-id")?.Value;
string? timestamp = claims.FirstOrDefault(x => x.Type == "timestamp")?.Value;
string? signature = claims.FirstOrDefault(x => x.Type == "signature")?.Value;
var conditions = new List<Func<bool>>
{
() => uri is not null,
() => appToken == _appToken.ToString("x16"),
() => long.TryParse(userId, out _),
() => DateTime.TryParse(timestamp, out _),
() => !string.IsNullOrEmpty(signature),
() => signature == SignUri(uri!, long.Parse(userId!), DateTime.Parse(timestamp!).ToUniversalTime())
};
// we want to stop at the first error.
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var condition in conditions)
if (!condition()) return false;
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment