food-market/src/food-market.application/Common/PinHasher.cs
nns 6b959ac59d
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
feat(pos): быстрый вход кассира по PIN (grant_type=pin) + lockout
- Сервер: Employee.PinHash (PBKDF2, миграция Phase36); POST
  /api/organization/employees/{id}/pin — админ задаёт/снимает PIN (4–6 цифр,
  уникален среди активных сотрудников на пересекающихся точках). Кастомный
  OpenIddict-грант grant_type=pin (retail_point_id + pin): находит кассира,
  привязанного к точке, по PIN и выдаёт токены. Брутфорс-защита — in-memory по
  точке (5 неверных → блок 5 мин). Клиент-сидер досоздаёт gt:pin существующим
  клиентам (без пересоздания). Пароль остаётся запасным.
- Касса: PIN-пад на зарегистрированном устройстве (маскированный ввод, 0–9/⌫/
  Войти), переключение на вход по логину; AuthService.PinLoginAsync.
- Тесты: unit PinHasher (9), integration (3): вход по PIN выдаёт рабочий токен;
  5 неверных → блок; дубль PIN на точке → 409. + скриншот PIN-пада в галерее.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:38:36 +05:00

38 lines
1.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Security.Cryptography;
using System.Text;
namespace foodmarket.Application.Common;
/// <summary>Хэширование PIN-кода кассира (PBKDF2-HMAC-SHA256 со случайной солью).
/// PIN низкоэнтропийный (4–6 цифр), поэтому защита — хэш + серверный lockout
/// (5 неверных → блок). Формат хранения: «base64(salt).base64(key)».</summary>
public static class PinHasher
{
private const int SaltSize = 16;
private const int KeySize = 32;
private const int Iterations = 100_000;
public static string Hash(string pin)
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var key = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(pin), salt, Iterations, HashAlgorithmName.SHA256, KeySize);
return $"{Convert.ToBase64String(salt)}.{Convert.ToBase64String(key)}";
}
public static bool Verify(string pin, string? stored)
{
if (string.IsNullOrEmpty(stored)) return false;
var parts = stored.Split('.');
if (parts.Length != 2) return false;
byte[] salt, key;
try { salt = Convert.FromBase64String(parts[0]); key = Convert.FromBase64String(parts[1]); }
catch { return false; }
var test = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(pin), salt, Iterations, HashAlgorithmName.SHA256, key.Length);
return CryptographicOperations.FixedTimeEquals(test, key);
}
/// <summary>PIN — 4–6 цифр.</summary>
public static bool IsValidPin(string? pin)
=> pin is not null && pin.Length is >= 4 and <= 6 && pin.All(char.IsDigit);
}