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
- Сервер: 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>
38 lines
1.6 KiB
C#
38 lines
1.6 KiB
C#
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);
|
||
}
|