Newer
Older
Sunfish / src / Sunfish.Core / Web / CsrfTokens.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 1 KB 3.0
using System.Security.Cryptography;
using System.Text;

namespace Sunfish.Web;

/// <summary>
/// Tokens anti-CSRF sin cookies (algunos navegadores antiguos no las tienen): HMAC-SHA256 de
/// (accion, carpeta, hora) con una clave aleatoria de esta ejecucion. Otra web no puede leer
/// la pagina, asi que no puede conocer el token ni falsificar el formulario. Vale durante la
/// hora en curso y la anterior.
/// </summary>
public sealed class CsrfTokens
{
    private readonly byte[] _key = RandomNumberGenerator.GetBytes(32);

    internal Func<DateTime> Now { get; init; } = () => DateTime.UtcNow;

    public string Create(string action, string folder) => Compute(action, folder, Hour(Now()));

    public bool Validate(string? token, string action, string folder)
    {
        if (string.IsNullOrEmpty(token))
            return false;
        long hour = Hour(Now());
        return Equal(token, Compute(action, folder, hour)) || Equal(token, Compute(action, folder, hour - 1));
    }

    private string Compute(string action, string folder, long hour)
    {
        byte[] mac = HMACSHA256.HashData(_key, Encoding.UTF8.GetBytes($"{action}\n{folder}\n{hour}"));
        return Convert.ToBase64String(mac, 0, 16).TrimEnd('=').Replace('+', '-').Replace('/', '_');
    }

    private static long Hour(DateTime utc) => utc.Ticks / TimeSpan.TicksPerHour;

    private static bool Equal(string a, string b) =>
        CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(a), Encoding.ASCII.GetBytes(b));
}