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